Compare commits

..
Author SHA1 Message Date
Robin Bygrave 31f6e56333 [maven-release-plugin] prepare release avaje-ebeanorm-6.11.1 2015-11-24 01:04:04 +13:00
Robin Bygrave 0efbe061ab #461 - ENH: Add @Draftable support - optimise publish query 2015-11-24 01:01:37 +13:00
Robin Bygrave 555e5ba6fd #461 - ENH: Add @Draftable support - API sans transaction 2015-11-24 01:01:14 +13:00
Robin Bygrave 5d3da60828 #461 - ENH: Add @Draftable support - delete 2015-11-24 00:46:45 +13:00
Robin Bygrave 2ac0d136e6 Bump pom to 6.11.1-SNAPSHOT 2015-11-24 00:02:35 +13:00
Robin Bygrave 97462cb21a #461 - ENH: Add @Draftable support - delete 2015-11-24 00:01:28 +13:00
Robin Bygrave afaacd4eeb #461 - ENH: Add @Draftable support - javadoc 2015-11-23 23:26:09 +13:00
Robin Bygrave 5e8afbe50d #461 - ENH: Add @Draftable support - @DraftDirty 2015-11-23 23:11:15 +13:00
Robin Bygrave db371cbc6a #461 - ENH: Add @Draftable support - publish 2015-11-23 21:28:58 +13:00
Robin Bygrave fd9a5167b1 #461 - ENH: Add @Draftable support - load context 2015-11-23 10:07:09 +13:00
Robin Bygrave 7b76e13bce #461 - ENH: Add @Draftable support - phase 1 2015-11-19 20:44:25 +13:00
Robin Bygrave 42e7103aa6 #458 - Support use of test-ebean.properties without an existing ebean.properties 2015-11-12 10:17:30 +13:00
Robin Bygrave 29ca678f01 No effective change - remove unused code 2015-11-12 09:59:36 +13:00
Robin Bygrave cca6143420 No effective change - code format 2015-11-12 09:48:18 +13:00
Robin Bygrave fe7b110577 #457 - When ebean.properties does not exist do not log error message 2015-11-10 09:47:33 +13:00
Robin Bygrave 16df55eafa [maven-release-plugin] prepare for next development iteration 2015-11-09 14:44:45 +13:00
Robin Bygrave 905a0ce9ec [maven-release-plugin] prepare release avaje-ebeanorm-6.10.4 2015-11-09 14:44:05 +13:00
Robin Bygrave bfa8837e61 #451 - avaje-ebeanorm-jackson - com.fasterxml.jackson.core.JsonParseException: Expecting start_array event but got START_OBJECT at 2015-11-05 16:38:22 +13:00
Robin Bygrave 8d5845b9a2 [maven-release-plugin] prepare for next development iteration 2015-11-04 22:10:38 +13:00
Robin Bygrave 87a7d4958e [maven-release-plugin] prepare release avaje-ebeanorm-6.10.3 2015-11-04 22:10:13 +13:00
Robin Bygrave 511ebd8a29 #450 - @ManyToMany with cascade and jdbc batch - ensure flush occurs 2015-11-03 16:31:27 +13:00
Robin Bygrave 11f4c8d7a3 #449 - Remove whitespace from generated Postgres trigger DDL (as Play evolution not parsing it) 2015-11-03 16:28:00 +13:00
Robin Bygrave d417a92843 [maven-release-plugin] prepare for next development iteration 2015-11-02 16:42:33 +13:00
Robin Bygrave 4bd5e52df3 [maven-release-plugin] prepare release avaje-ebeanorm-6.10.2 2015-11-02 16:42:10 +13:00
Robin Bygrave 788a200d16 #445 - Multiple @*ToMany relations referencing the same type 2015-11-02 14:11:23 +13:00
Robin Bygrave e18d2004dc #448 - ENH: Postgres Support for UUID mapping to native DB UUID type (relating to #430) 2015-11-02 12:57:47 +13:00
Robin Bygrave b326724139 #443 - ENH: Enum mapping - Add annotation @DbEnumValue that can be used to on a enum method which provides DB values 2015-10-30 21:42:21 +13:00
Robin Bygrave fc0b47342b #436 - @DiscriminatorColumn mapped also as @Column leads to NullPointerException 2015-10-29 22:01:28 +13:00
Robin Bygrave ec1a68a133 #435 - Duplicate foreign key name with ManyToMany relations 2015-10-29 17:44:29 +13:00
Robin Bygrave 8ca12521d7 No effective change - reduce debug level 2015-10-29 17:43:54 +13:00
Robin Bygrave 2362ef4130 [maven-release-plugin] prepare for next development iteration 2015-10-29 09:01:17 +13:00
Robin Bygrave 7e233edcc3 [maven-release-plugin] prepare release avaje-ebeanorm-6.10.1 2015-10-29 09:00:55 +13:00
Robin Bygrave a8b98282a1 Bump pom to 6.10.1-SNAPSHOT 2015-10-29 09:00:00 +13:00
Robin Bygrave b4506aa718 #444 - Spring Boot classpath scanning not finding entity beans - part2 - embedded jar scanning 2015-10-29 08:59:06 +13:00
Robin Bygrave 848a3b8888 #444 - Spring Boot classpath scanning not finding entity beans 2015-10-28 15:45:09 +13:00
Robin Bygrave 67c711d75f #442 - @JsonIgnore / @Expose ... not working on @OneToMany or @ManyToMany - was - @Transient field on @MappedSuperclass is not ignored by toJson() 2015-10-28 09:10:19 +13:00
Robin Bygrave 3d62f58930 #439 - @EnumValue("01") stored as integer rather than string - ENum stores strings as numerics? 2015-10-28 08:53:14 +13:00
Robin Bygrave a30948d38d #438 - findPagedList and getTotalRowCount, the query.getAlias() is null 2015-10-28 08:15:15 +13:00
Robin Bygrave a8232d2d8b #441 - ENH: Add support for test-ebean.properties ... for mechanism to configure ebean to run tests 2015-10-27 00:17:47 +13:00
Robin Bygrave d4b8f23a5a Fix test - MySql limitations with delete query syntax 2015-10-26 21:52:35 +13:00
Robin Bygrave bcf2a44187 #430 - ENH: Support for UUID mapping to native DB UUID type (Postgres, H2) 2015-10-26 20:52:16 +13:00
Robin Bygrave f3102ba02c #432 - Delete using Query does not commit 2015-10-22 13:29:21 +13:00
Robin Bygrave 0351eab3a7 #431 - Annotation PrivateOwned seems not enough to update an change in the list of children (fix) 2015-10-22 11:10:41 +13:00
Robin Bygrave 409e2b75ce #431 - Add failing test ... Annotation PrivateOwned seems not enough to update an change in the list of children 2015-10-22 09:27:32 +13:00
Robin Bygrave dc968f16bf Q: Ebean @ManyToMany(cascade=REMOVE) described #429 2015-10-06 14:38:05 +13:00
Robin Bygrave bbbb6482fb [maven-release-plugin] prepare for next development iteration 2015-10-02 13:03:34 +13:00
Robin Bygrave 001968fcf8 [maven-release-plugin] prepare release avaje-ebeanorm-6.9.1 2015-10-02 13:03:07 +13:00
Robin Bygrave 6abcb186a4 Bump pom to 6.9.1-SNAPSHOT 2015-10-02 13:02:13 +13:00
Robin Bygrave f250447fbe Add test for ManyToMany delete scenario 2015-10-01 22:21:47 +13:00
Robin Bygrave bfa86f241b #427 - ENH: Add support for Removing ShutdownManager.shutdownHook ... manually control entire shutdown. 2015-10-01 10:33:09 +13:00
Robin Bygrave 19459b71b4 Fix for #425 - Extra instance of root level bean with findIterate() and fetch of OneToMany relationship - Ebean parses the first bean correctly and the rest not 2015-09-30 23:35:10 +13:00
Robin Bygrave 5e97c3bbbc #425 - Extra instance of root level bean with findIterate() and fetch of OneToMany relationship - Ebean parses the first bean correctly and the rest not 2015-09-30 23:24:53 +13:00
Robin Bygrave 986eadd023 #424 - DDL generation - support turning off lower casing of table and column names DbConstraintNaming, DbConstraintNormalise 2015-09-30 22:44:47 +13:00
Robin Bygrave c6787d0901 Update javadoc 2015-09-30 17:26:30 +13:00
Robin Bygrave 3792e06859 Update javadoc 2015-09-30 17:26:05 +13:00
Robin Bygrave c77446ba24 Update javadoc 2015-09-30 17:25:26 +13:00
Robin Bygrave dba245c517 #421 - ENH: Add support for unsetting the loaded state of a bean property (via BeanState) 2015-09-22 13:21:19 +12:00
Robin Bygrave 489eb483c0 No effective change - parameter name from val -> value 2015-09-22 12:34:30 +12:00
Robin Bygrave 11301846be #422 - ENH: Add jsonBetween() expression 2015-09-22 12:32:46 +12:00
Robin Bygrave d022e4aec8 No effective change - parameter name from val -> value 2015-09-22 11:48:41 +12:00
Robin Bygrave aa788edb68 #421 - ENH: Add support for unsetting the loaded state of a bean property (via BeanState) ... initial test 2015-09-22 09:00:06 +12:00
Robin Bygrave 288adcdf68 Update README 2015-09-21 18:01:31 +12:00
Robin Bygrave ddb07ca202 Update README 2015-09-21 17:59:45 +12:00
Robin Bygrave 524b592e82 [maven-release-plugin] prepare for next development iteration 2015-09-21 17:45:47 +12:00
Robin Bygrave 0124f0f254 [maven-release-plugin] prepare release avaje-ebeanorm-6.8.1 2015-09-21 17:45:21 +12:00
Robin Bygrave eddd6a36d6 No effective change - javadoc only 2015-09-21 17:36:13 +12:00
Robin Bygrave 91e309d770 Bump pom version to 6.8.1-SNAPSHOT 2015-09-21 17:03:22 +12:00
Robin Bygrave 5726410f6f #417 - ENH: Add JSON expressions ... path exists and value at path equals value 2015-09-21 16:20:14 +12:00
Robin Bygrave 054f7dc86f #415 - ENH: Add ExplicitJdbcTransaction ... so like AutoCommitJdbcTransaction but using explicit statements to commit and rollback 2015-09-18 22:25:19 +12:00
Robin Bygrave 06b5ebc14e #419 - Support running History DDL scripts in full drop and create ddl execution - detect $$ procedures/triggers in ddl script 2015-09-18 22:19:52 +12:00
Robin Bygrave f6ee82428f Change test - disable this test for MySql - it does not like trying to delete using sub query select from same table 2015-09-18 22:18:37 +12:00
Robin Bygrave e1d45dd79d #419 - Support running History DDL scripts in full drop and create ddl execution - detect $$ procedures/triggers in ddl script 2015-09-18 22:03:26 +12:00
Robin Bygrave 08c455279f #418 - Postgres History DDL generation - invalid update table statement to initialise the sys_period 2015-09-18 21:30:59 +12:00
Robin Bygrave 32dcbc6703 Change test to only run with H2 (but it would also run with db's that use history table views/triggers but not with oracle total recall) 2015-09-18 21:13:19 +12:00
Robin Bygrave c9e733df4e Fix test - incorrect raw sql syntax (breaks in Oracle) 2015-09-18 21:06:54 +12:00
Robin Bygrave 606e1e6d3a #414 - Enh info: @History support for H2 2015-09-18 17:16:46 +12:00
Robin Bygrave fcb50ea13a No effective change - remove whitespace 2015-09-18 11:45:19 +12:00
Robin Bygrave 5b26e77cfd #413 - ENH: Add ability to clear the OrderBy ... so that a default order by can be set in code and then cleared / overwritten 2015-09-18 11:39:47 +12:00
Robin Bygrave 9fc5d8a201 Pom - bump test Postgres JDBC driver version 2015-09-17 11:33:09 +12:00
Robin Bygrave ba78c5e904 [maven-release-plugin] prepare for next development iteration 2015-09-14 16:19:59 +12:00
184 changed files with 4896 additions and 956 deletions
+1 -10
View File
@@ -5,16 +5,7 @@
Ebean ORM
==============
[http://ebean-orm.github.io/](http://ebean-orm.github.io/ "Ebean ORM's Website")
Maven Dependency
----------------
<dependency>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>4.7.3</version>
</dependency>
[https://ebean-orm.github.io/](http://ebean-orm.github.io/ "Ebean ORM's Website")
[Maven central - avaje-ebeanorm](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.avaje.ebeanorm%22%20AND%20a%3A%22avaje-ebeanorm%22 "maven central ebeanorm")
+18 -8
View File
@@ -9,7 +9,7 @@
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>6.7.1</version>
<version>6.11.1</version>
<packaging>jar</packaging>
<name>avaje-ebeanorm</name>
@@ -61,6 +61,14 @@
<version>1.7.7</version>
</dependency>
<!-- provided scope for Spring boot loader classpath scanning support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>1.2.7.RELEASE</version>
<scope>provided</scope>
</dependency>
<!-- provided scope for JsonNode support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -109,7 +117,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version>
<version>9.4-1202-jdbc41</version>
<scope>provided</scope>
</dependency>
@@ -147,12 +155,14 @@
<version>4.7.1</version>
<scope>test</scope>
</dependency>
<!-- Provided scope so that the H2HistoryTrigger can live in Ebean core
and not require a separate module for it -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.182</version>
<scope>test</scope>
<version>1.4.189</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -218,7 +228,7 @@
<configuration>
<classSource>target/test-classes</classSource>
<packages>com.avaje.tests.**</packages>
<transformArgs>debug=2</transformArgs>
<transformArgs>debug=1</transformArgs>
</configuration>
<goals>
<goal>enhance</goal>
@@ -263,11 +273,11 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<doctitle>Ebean 4</doctitle>
<doctitle>Ebean 6</doctitle>
<overview>src/main/java/com/avaje/ebean/overview.html</overview>
<source>1.8</source>
<doclet>org.avaje.doclet.PygmentsDoclet</doclet>
<excludePackageNames>com.avaje.ebeaninternal.*:com.avaje.ebean.util</excludePackageNames>
<excludePackageNames>com.avaje.ebeaninternal.*:com.avaje.ebean.util:com.avaje.ebean.dbmigration.ddlgeneration:com.avaje.ebean.dbmigration.migration:com.avaje.ebean.dbmigration.migrationreader:com.avaje.ebean.dbmigration.model</excludePackageNames>
<docletArtifact>
<groupId>org.avaje</groupId>
<artifactId>pygments-doclet</artifactId>
@@ -38,6 +38,33 @@ public interface BeanState {
*/
void setDisableLazyLoad(boolean disableLazyLoading);
/**
* Set the loaded state of the property given it's name.
*
* <p>
* Typically this would be used to set the loaded state of a property
* to false to ensure that the specific property is excluded from a
* stateless update.
* </p>
*
* <pre>{@code
*
* // populate a bean via say JSON
* User user = ...;
*
* // set loaded state on the email property to false so that
* // the email property is not included in a stateless update
* Ebean.getBeanState(user).setPropertyLoaded("email", false);
*
* user.update();
*
* }</pre>
*
*
* This will throw an IllegalArgumentException if the property is unknown.
*/
void setPropertyLoaded(String propertyName, boolean loaded);
/**
* For partially populated beans returns the properties that are loaded on the
* bean.
@@ -1819,4 +1819,54 @@ public interface EbeanServer {
*/
JsonContext json();
/**
* Publish a single bean given its type and id returning the resulting live bean.
* <p>
* The values are published from the draft to the live bean.
* </p>
*
* @param <T> the type of the entity bean
* @param beanType the type of the entity bean
* @param id the id of the entity bean
* @param transaction the transaction the publish process should use (can be null)
*/
<T> T publish(Class<T> beanType, Object id, Transaction transaction);
/**
* Publish a single bean given its type and id returning the resulting live bean.
* This will use the current transaction or create one if required.
* <p>
* The values are published from the draft to the live bean.
* </p>
*
* @param <T> the type of the entity bean
* @param beanType the type of the entity bean
* @param id the id of the entity bean
*/
<T> T publish(Class<T> beanType, Object id);
/**
* Publish the beans that match the query returning the resulting published beans.
* <p>
* The values are published from the draft beans to the live beans.
* </p>
*
* @param <T> the type of the entity bean
* @param query the query used to select the draft beans to publish
* @param transaction the transaction the publish process should use (can be null)
*/
<T> List<T> publish(Query<T> query, Transaction transaction);
/**
* Publish the beans that match the query returning the resulting published beans.
* This will use the current transaction or create one if required.
* <p>
* The values are published from the draft beans to the live beans.
* </p>
*
* @param <T> the type of the entity bean
* @param query the query used to select the draft beans to publish
*/
<T> List<T> publish(Query<T> query);
}
@@ -36,6 +36,51 @@ import java.util.Map;
*/
public interface ExpressionFactory {
/**
* Path exists - for the given path in a JSON document.
*/
Expression jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*/
Expression jsonNotExists(String propertyName, String path);
/**
* Equal to - for the given path in a JSON document.
*/
Expression jsonEqualTo(String propertyName, String path, Object val);
/**
* Not Equal to - for the given path in a JSON document.
*/
Expression jsonNotEqualTo(String propertyName, String path, Object val);
/**
* Greater than - for the given path in a JSON document.
*/
Expression jsonGreaterThan(String propertyName, String path, Object val);
/**
* Greater than or equal to - for the given path in a JSON document.
*/
Expression jsonGreaterOrEqual(String propertyName, String path, Object val);
/**
* Less than - for the given path in a JSON document.
*/
Expression jsonLessThan(String propertyName, String path, Object val);
/**
* Less than or equal to - for the given path in a JSON document.
*/
Expression jsonLessOrEqualTo(String propertyName, String path, Object val);
/**
* Between - for the given path in a JSON document.
*/
Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue);
/**
* Equal To - property equal to the given value.
*/
@@ -345,12 +345,125 @@ public interface ExpressionList<T> extends Serializable {
*/
ExpressionList<T> where();
/**
* Path exists - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonExists("content", "path.other")
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonNotExists("content", "path.other")
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonNotExists(String propertyName, String path);
/**
* Equal to expression for the value at the given path in the JSON document.
*
* <pre>{@code
*
* where().jsonEqualTo("content", "path.other", 34)
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test against the document path's value
*/
ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value);
/**
* Not Equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonNotEqualTo("content", "path.other", 34)
*
* }</pre>
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test against the document path's value
*/
ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object value);
/**
* Greater than - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonGreaterThan("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object value);
/**
* Greater than or equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonGreaterOrEqual("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object value);
/**
* Less than - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonLessThan("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonLessThan(String propertyName, String path, Object value);
/**
* Less than or equal to - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonLessOrEqualTo("content", "path.other", 34)
*
* }</pre>
*/
ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object value);
/**
* Between - for the given path in a JSON document.
*
* <pre>{@code
*
* where().jsonBetween("content", "orderDate", lowerDateTime, upperDateTime)
*
* }</pre>
*/
ExpressionList<T> jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue);
/**
* Add an Expression to the list.
* <p>
* This returns the list so that add() can be chained.
* </p>
*
*
* <pre>{@code
*
* Query<Customer> query = Ebean.find(Customer.class);
+26 -1
View File
@@ -1,5 +1,6 @@
package com.avaje.ebean;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebean.util.ClassUtil;
import org.jetbrains.annotations.Nullable;
@@ -193,7 +194,31 @@ public abstract class Model {
public void markAsDirty() {
db().markAsDirty(this);
}
/**
* Mark the property as unset or 'not loaded'.
* <p>
* This would be used to specify a property that we did not wish to include in a stateless update.
* </p>
* <pre>{@code
*
* // populate an entity bean from JSON or whatever
* User user = ...;
*
* // mark the email property as 'unset' so that it is not
* // included in a 'stateless update'
* user.markPropertyUnset("email");
*
* user.update();
*
* }</pre>
*
* @param propertyName the name of the property on the bean to be marked as 'unset'
*/
public void markPropertyUnset(String propertyName) {
((EntityBean)this)._ebean_getIntercept().setPropertyLoaded(propertyName, false);
}
/**
* Insert or update this entity depending on its state.
*
@@ -198,6 +198,18 @@ public final class OrderBy<T> implements Serializable {
return list.hashCode();
}
/**
* Clear the orderBy removing any current order by properties.
* <p>
* This is intended to be used when some code creates a query with a
* 'default' order by clause and some other code may clear the 'default'
* order by clause and replace.
* </p>
*/
public void clear() {
list.clear();
}
/**
* A property and its ascending descending order.
*/
+5
View File
@@ -297,6 +297,11 @@ public interface Query<T> extends Serializable {
*/
Query<T> asOf(Timestamp asOf);
/**
* Execute the query against the draft set of tables.
*/
Query<T> asDraft();
/**
* Cancel the query execution if supported by the underlying database and
* driver.
@@ -0,0 +1,17 @@
package com.avaje.ebean.annotation;
/**
* Specify the DB storage type used to with <code>@DbEnumValue</code>.
*/
public enum DbEnumType {
/**
* Store values as database INTEGER.
*/
INTEGER,
/**
* Store values as database VARCHAR.
*/
VARCHAR
}
@@ -0,0 +1,44 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify a method on an Enum that returns the value that should be stored in the DB.
* <p>
* This is the preferred option for mapping Enum's to DB values (preferred over the JPA
* standard @Enumerated and Ebean's @EnumValue annotations).
* </p>
* <h3>Example:</h3>
* <pre>{@code
*
* public enum Status {
* NEW("N"),
* ACTIVE("A"),
* INACTIVE("I");
*
* String dbValue;
* Status(String dbValue) {
* this.dbValue = dbValue;
* }
*
* @DbEnumValue
* public String getValue() {
* return dbValue;
* }
* }
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DbEnumValue {
/**
* Specify the database type used to store the values (VARCHAR or INTEGER).
*/
DbEnumType storage() default DbEnumType.VARCHAR;
}
@@ -0,0 +1,20 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a boolean property on a @Draftable bean that only exists on the 'draft' table
* and is used to detect when a draft has unpublished changes.
* <p>
* This property will automatically have it's value set to true when a draft is saved and
* automatically have it's value set to false when the bean is published.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DraftDirty {
}
@@ -0,0 +1,19 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a property on a @Draftable bean that only exists on the 'draft' and not the 'live' table.
* <p>
* Typically this would be used on a property that is used as part of application 'workflow' such as
* a publish workflow status or when publish timestamp.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DraftOnly {
}
@@ -0,0 +1,29 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Used to indicate an entity bean that has 'draftable' support.
* <p>
* This means that a second set of tables is created to hold draft versions of
* the rows and that these can then be published which effectively copies/transfers
* the values from the 'draft' table to the 'live' table.
* </p>
* <p>
* Ebean Query supports 'find as draft' which builds the resulting object graph using
* the draft tables. This object graph is typically edited, approved in some application
* specific manor and then published.
* </p>
* <p>
* EbeanServer has a publish method which transfers/copies the draft object graph to
* the 'live' tables.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Draftable {
}
@@ -0,0 +1,20 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Used to indicate an entity bean that has 'draftable' support but it not a 'top level'
* (or root level) bean but instead child related to another @Draftable entity bean.
* <p>
* Relationships to @DraftableElements (@OneToMany, @ManyToMany etc) are automatically
* deemed to have Cascade.ALL for save and delete (as well as orphan removal mode).
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DraftableElement {
}
@@ -25,7 +25,7 @@ import java.lang.annotation.Target;
* }</pre>
* <p>
* This is an alternative to using the JPA standard approach or Ebean's
* {@link EnumMapping} annotation.
* {@link DbEnumValue} annotation.
* </p>
* <p>
* Note that if all the EnumValue values are parsable as Integers then Ebean
@@ -31,6 +31,16 @@ public interface BeanCollection<E> extends Serializable {
ALL
}
/**
* Add a bean to the list/set with modifyListen notification.
*/
void addBean(E bean);
/**
* Remove a bean to the list/set with modifyListen notification.
*/
void removeBean(E bean);
/**
* Reset the collection back to an empty state ready for reloading.
* <p>
@@ -12,5 +12,5 @@ public interface BeanCollectionAdd {
/**
* Add a loaded bean to the collection.
*/
void addBean(EntityBean bean);
void addEntityBean(EntityBean bean);
}
@@ -449,7 +449,18 @@ public final class EntityBeanIntercept implements Serializable {
public int getPropertyLength() {
return owner._ebean_getPropertyNames().length;
}
/**
* Set the loaded state of the property given it's name.
*/
public void setPropertyLoaded(String propertyName, boolean loaded) {
int position = findProperty(propertyName);
if (position == -1) {
throw new IllegalArgumentException("Property "+propertyName+" not found");
}
loadedProps[position] = loaded;
}
/**
* Set the property to be treated as unloaded. Used for properties initialised in default
* constructor.
@@ -60,7 +60,7 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
}
@SuppressWarnings("unchecked")
public void addBean(EntityBean bean) {
public void addEntityBean(EntityBean bean) {
list.add((E) bean);
}
@@ -206,6 +206,11 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
list.add(index, element);
}
@Override
public void addBean(E bean) {
add(bean);
}
public boolean add(E o) {
checkReadOnly();
init();
@@ -319,6 +324,13 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
return list.listIterator(index);
}
@Override
public void removeBean(E bean) {
if (list.remove(bean)) {
getModifyHolder().modifyRemoval(bean);
}
}
public E remove(int index) {
checkReadOnly();
init();
@@ -272,6 +272,16 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
map.putAll(puts);
}
@Override
public void addBean(E bean) {
throw new IllegalStateException("Method not allowed on Map. Please use List instead.");
}
@Override
public void removeBean(E bean) {
throw new IllegalStateException("Method not allowed on Map. Please use List instead.");
}
public E remove(Object key) {
checkReadOnly();
init();
@@ -53,7 +53,7 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
}
@SuppressWarnings("unchecked")
public void addBean(EntityBean bean) {
public void addEntityBean(EntityBean bean) {
set.add((E) bean);
}
@@ -174,6 +174,18 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
return set.hashCode();
}
@Override
public void addBean(E bean) {
add(bean);
}
@Override
public void removeBean(E bean) {
if (set.remove(bean)) {
getModifyHolder().modifyRemoval(bean);
}
}
// -----------------------------------------------------//
// proxy method for map
// -----------------------------------------------------//
@@ -45,13 +45,29 @@ public class DbConstraintNaming {
protected String ckPrefix = "ck_";
protected String ckSuffix = "";
protected boolean lowerCaseNames = true;
protected MaxLength maxLength;
protected DbConstraintNormalise normalise = new DbConstraintNormalise();
protected DbConstraintNormalise normalise;
/**
* Construct using default of lower case for both table and column names.
*/
public DbConstraintNaming() {
this(true, true);
}
/**
* Construct specifying if lower case should be used (for both table and column names).
*/
public DbConstraintNaming(boolean lowerCase) {
this(lowerCase, lowerCase);
}
/**
* Construct specifying if lower case should be used for both table and column names.
*/
public DbConstraintNaming(boolean lowerCaseTableNames, boolean lowerCaseColumnNames) {
this.normalise = new DbConstraintNormalise(lowerCaseTableNames, lowerCaseColumnNames);
}
/**
@@ -167,7 +183,6 @@ public class DbConstraintNaming {
* quoted identifier characters (",',[,] etc).
*/
public String normaliseTable(String tableName) {
return normalise.normaliseTable(tableName);
}
@@ -175,18 +190,20 @@ public class DbConstraintNaming {
* Normalise the column name by removing any quoted identifier characters (",',[,] etc).
*/
public String normaliseColumn(String tableName) {
return normalise.normaliseColumn(tableName);
}
/**
* Lower case the table or column name checking for quoted identifiers.
* Lower case the table name checking for quoted identifiers.
*/
public String lowerName(String tableName) {
if (lowerCaseNames && normalise.notQuoted(tableName)) {
return tableName.toLowerCase();
}
return tableName;
public String lowerTableName(String tableName) {
return normalise.lowerTableName(tableName);
}
/**
* Lower case the column name checking for quoted identifiers.
*/
public String lowerColumnName(String name) {
return normalise.lowerColumnName(name);
}
}
@@ -8,11 +8,17 @@ public class DbConstraintNormalise {
protected final String[] quotedIdentifiers;
protected boolean lowerCaseTables = true;
protected final boolean lowerCaseTables;
protected boolean lowerCaseColumns = true;
protected final boolean lowerCaseColumns;
public DbConstraintNormalise() {
this(true, true);
}
public DbConstraintNormalise(boolean lowerCaseTables, boolean lowerCaseColumns) {
this.lowerCaseTables = lowerCaseTables;
this.lowerCaseColumns = lowerCaseColumns;
this.quotedIdentifiers = new String[]{"\"", "'", "[", "]", "`"};
}
@@ -46,6 +52,26 @@ public class DbConstraintNormalise {
return columnName;
}
/**
* Lower case the table name checking for quoted identifiers.
*/
public String lowerTableName(String tableName) {
if (lowerCaseTables && notQuoted(tableName)) {
return tableName.toLowerCase();
}
return tableName;
}
/**
* Lower case the column name checking for quoted identifiers.
*/
public String lowerColumnName(String name) {
if (lowerCaseColumns && notQuoted(name)) {
return name.toLowerCase();
}
return name;
}
/**
* Trim off the platform quoted identifier quotes like [ ' and ".
*/
@@ -18,6 +18,14 @@ public final class PropertyMap implements Serializable {
private final LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
/**
* Return properties loaded from test-ebean.properties.
*/
public static Properties testProperties() {
PropertyMap propertyMap = PropertyMapLoader.loadTestProperties();
return (propertyMap == null) ? new Properties() : propertyMap.asProperties();
}
public static Properties defaultProperties() {
PropertyMap propertyMap = PropertyMapLoader.loadGlobalProperties();
return (propertyMap == null) ? new Properties() : propertyMap.asProperties();
@@ -14,17 +14,34 @@ final class PropertyMapLoader {
private static final Logger logger = LoggerFactory.getLogger(PropertyMapLoader.class);
/**
* Load the <code>test-ebean.properties</code>.
*/
public static PropertyMap loadTestProperties() {
return load(null, "test-ebean.properties");
}
/**
* Load the ebean.properties (and test-ebean.properties if present).
*/
public static PropertyMap loadGlobalProperties() {
boolean loadTestProperties = false;
String fileName = System.getenv("EBEAN_PROPS_FILE");
if (fileName == null) {
fileName = System.getProperty("ebean.props.file");
if (fileName == null) {
loadTestProperties = true;
fileName = "ebean.properties";
}
}
return load(null, fileName);
PropertyMap map = load(null, fileName);
if (loadTestProperties) {
// load test properties if present in classpath
map = load(map, "test-ebean.properties");
}
return map;
}
/**
@@ -39,7 +56,6 @@ final class PropertyMapLoader {
InputStream is = findInputStream(fileName);
if (is == null) {
logger.error(fileName + " not found");
return p;
} else {
return load(p, is);
@@ -47,7 +63,7 @@ final class PropertyMapLoader {
}
/**
* Load the inputstream returning the property map.
* Load the InputStream returning the property map.
*
* @param p
* an existing property map to load into.
@@ -234,7 +234,12 @@ public class ServerConfig {
* Indicates that Ebean should use autoCommit friendly Transactions and TransactionManager.
*/
private boolean autoCommitMode;
/**
* Set to true if transaction begin should be started with explicit statement.
*/
private boolean explicitTransactionBeginMode;
/**
* The data source JNDI name if using a JNDI DataSource.
*/
@@ -276,9 +281,9 @@ public class ServerConfig {
private boolean updatesDeleteMissingChildren = true;
/**
* Setting to indicate if UUID should be stored as binary(16) or varchar(40).
* Setting to indicate if UUID should be stored as binary(16) or varchar(40) or native DB type (for H2 and Postgres).
*/
private boolean uuidStoreAsBinary;
private DbUuid dbUuid = DbUuid.AUTO;
private List<BeanFindController> findControllers = new ArrayList<BeanFindController>();
@@ -1220,6 +1225,28 @@ public class ServerConfig {
this.autoCommitMode = autoCommitMode;
}
/**
* Return true if transaction begin should be started with explicit statement.
*/
public boolean isExplicitTransactionBeginMode() {
return explicitTransactionBeginMode;
}
/**
* Set to true if transaction begin should be started with explicit statement.
* <p>
* This works for H2 and Postgres but not for Oracle - only use this if you first name
* is Daryl or you have explicitly talked to Rob about this feature.
* </p>
* <p>
* This is generally not expected to be turned on but instead allow transactions to start
* implicitly which is generally the standard approach.
* </p>
*/
public void setExplicitTransactionBeginMode(boolean explicitTransactionBeginMode) {
this.explicitTransactionBeginMode = explicitTransactionBeginMode;
}
/**
* Return a value used to represent TRUE in the database.
* <p>
@@ -1439,19 +1466,18 @@ public class ServerConfig {
this.dbEncrypt = dbEncrypt;
}
/**
* Return true if UUID should be stored as binary(16) (as opposed to varchar(40)).
* Return the DB type used to store UUID.
*/
public boolean isUuidStoreAsBinary() {
return uuidStoreAsBinary;
public DbUuid getDbUuid() {
return dbUuid;
}
/**
* Set to true if UUID should be stored as binary(16) (as opposed to varchar(40)).
* Set the DB type used to store UUID.
*/
public void setUuidStoreAsBinary(boolean uuidStoreAsBinary) {
this.uuidStoreAsBinary = uuidStoreAsBinary;
public void setDbUuid(DbUuid dbUuid) {
this.dbUuid = dbUuid;
}
/**
@@ -2014,6 +2040,21 @@ public class ServerConfig {
loadSettings(p);
}
/**
* Load settings from test-ebean.properties and do nothing if the properties is not found.
* <p>
* This is typically used when test-ebean.properties is put into the test class path and used
* to configure Ebean for running tests.
* </p>
*/
public void loadTestProperties() {
Properties properties = PropertyMap.testProperties();
if (!properties.isEmpty()) {
PropertiesWrapper p = new PropertiesWrapper("ebean", name, properties);
loadSettings(p);
}
}
/**
* Return the properties that we used for configuration and were set via a call to loadFromProperties().
*/
@@ -2022,8 +2063,11 @@ public class ServerConfig {
}
@SuppressWarnings("unchecked")
private <T> T createInstance(PropertiesWrapper p, Class<T> pluginType, String key) {
private <T> T createInstance(PropertiesWrapper p, Class<T> pluginType, String key, T instance) {
if (instance != null) {
return instance;
}
String classname = p.get(key, null);
return classname == null ? null : (T) ClassUtil.newInstance(classname);
}
@@ -2067,18 +2111,19 @@ public class ServerConfig {
}
loadDataSourceSettings(p);
explicitTransactionBeginMode = p.getBoolean("explicitTransactionBeginMode", explicitTransactionBeginMode);
autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode);
useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", useJtaTransactionManager);
currentUserProvider = createInstance(p, CurrentUserProvider.class, "currentUserProvider");
disableClasspathSearch = p.getBoolean("disableClasspathSearch", disableClasspathSearch);
databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform");
encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager");
encryptDeployManager = createInstance(p, EncryptDeployManager.class, "encryptDeployManager");
encryptor = createInstance(p, Encryptor.class, "encryptor");
dbEncrypt = createInstance(p, DbEncrypt.class, "dbEncrypt");
serverCacheFactory = createInstance(p, ServerCacheFactory.class, "serverCacheFactory");
serverCacheManager = createInstance(p, ServerCacheManager.class, "serverCacheManager");
currentUserProvider = createInstance(p, CurrentUserProvider.class, "currentUserProvider", currentUserProvider);
databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform", databasePlatform);
encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager", encryptKeyManager);
encryptDeployManager = createInstance(p, EncryptDeployManager.class, "encryptDeployManager", encryptDeployManager);
encryptor = createInstance(p, Encryptor.class, "encryptor", encryptor);
dbEncrypt = createInstance(p, DbEncrypt.class, "dbEncrypt", dbEncrypt);
serverCacheFactory = createInstance(p, ServerCacheFactory.class, "serverCacheFactory", serverCacheFactory);
serverCacheManager = createInstance(p, ServerCacheManager.class, "serverCacheManager", serverCacheManager);
cacheWarmingDelay = p.getInt("cacheWarmingDelay", cacheWarmingDelay);
classPathReaderClassName = p.get("classpathreader");
@@ -2124,7 +2169,10 @@ public class ServerConfig {
databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue);
databaseBooleanFalse = p.get("databaseBooleanFalse", databaseBooleanFalse);
databasePlatformName = p.get("databasePlatformName", databasePlatformName);
uuidStoreAsBinary = p.getBoolean("uuidStoreAsBinary", uuidStoreAsBinary);
dbUuid = p.getEnum(DbUuid.class, "dbuuid", dbUuid);
if (p.getBoolean("uuidStoreAsBinary", false)) {
dbUuid = DbUuid.BINARY;
}
localTimeWithNanos = p.getBoolean("localTimeWithNanos", localTimeWithNanos);
lazyLoadBatchSize = p.getInt("lazyLoadBatchSize", lazyLoadBatchSize);
@@ -2146,7 +2194,7 @@ public class ServerConfig {
private NamingConvention createNamingConvention(PropertiesWrapper properties, NamingConvention namingConvention) {
NamingConvention nc = createInstance(properties, NamingConvention.class, "namingconvention");
NamingConvention nc = createInstance(properties, NamingConvention.class, "namingconvention", null);
return (nc != null) ? nc : namingConvention;
}
@@ -2246,4 +2294,25 @@ public class ServerConfig {
public void setExpressionEqualsWithNullAsNoop(boolean expressionEqualsWithNullAsNoop) {
this.expressionEqualsWithNullAsNoop = expressionEqualsWithNullAsNoop;
}
/**
* Specify how UUID is stored.
*/
public enum DbUuid {
/**
* Store using native UUID in H2 and Postgres.
*/
AUTO,
/**
* Store using DB VARCHAR.
*/
VARCHAR,
/**
* Store using DB BINARY.
*/
BINARY
}
}
@@ -1,18 +1,16 @@
package com.avaje.ebean.config.dbplatform;
import java.sql.Types;
import javax.sql.DataSource;
import com.avaje.ebean.BackgroundExecutor;
import com.avaje.ebean.Query;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
import com.avaje.ebean.dbmigration.ddlgeneration.platform.PlatformDdl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Types;
/**
* Database platform specific settings.
*/
@@ -54,28 +52,33 @@ public class DatabasePlatform {
*/
protected OnQueryOnly onQueryOnly = OnQueryOnly.ROLLBACK;
/**
* The open quote used by quoted identifiers.
/**
* The open quote used by quoted identifiers.
*/
protected String openQuote = "\"";
/**
* The close quote used by quoted identifiers.
* The close quote used by quoted identifiers.
*/
protected String closeQuote = "\"";
/**
* For limit/offset, row_number etc limiting of SQL queries.
/**
* For limit/offset, row_number etc limiting of SQL queries.
*/
protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter();
/**
* Mapping of JDBC to Database types.
/**
* Mapping of JDBC to Database types.
*/
protected DbTypeMap dbTypeMap = new DbTypeMap();
/**
* Defines DB identity/sequence features.
* Set to true if the DB has native UUID type support.
*/
protected boolean nativeUuidType;
/**
* Defines DB identity/sequence features.
*/
protected DbIdentity dbIdentity = new DbIdentity();
@@ -84,28 +87,28 @@ public class DatabasePlatform {
*/
protected DbHistorySupport historySupport;
/**
* The JDBC type to map booleans to (by default).
/**
* The JDBC type to map booleans to (by default).
*/
protected int booleanDbType = Types.BOOLEAN;
/**
* The JDBC type to map Blob to.
/**
* The JDBC type to map Blob to.
*/
protected int blobDbType = Types.BLOB;
/**
* The JDBC type to map Clob to.
/**
* The JDBC type to map Clob to.
*/
protected int clobDbType = Types.CLOB;
/**
* For Oracle treat empty strings as null.
/**
* For Oracle treat empty strings as null.
*/
protected boolean treatEmptyStringsAsNull;
/**
* The database platform name.
/**
* The database platform name.
*/
protected String name = "generic";
@@ -124,7 +127,7 @@ public class DatabasePlatform {
* The like clause. Can be overridden to disable default escape character.
*/
protected String likeClause = "like ?";
protected DbEncrypt dbEncrypt;
protected boolean idInExpandedForm;
@@ -219,16 +222,12 @@ public class DatabasePlatform {
/**
* Return a DB Sequence based IdGenerator.
*
* @param be
* the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds
* the DataSource
* @param seqName
* the name of the sequence
* @param batchSize
* the number of sequences that should be loaded
*
* @param be the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds the DataSource
* @param seqName the name of the sequence
* @param batchSize the number of sequences that should be loaded
*/
public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) {
return null;
@@ -276,9 +275,16 @@ public class DatabasePlatform {
this.historySupport = historySupport;
}
/**
* Return true if the DB supports native UUID.
*/
public boolean isNativeUuidType() {
return nativeUuidType;
}
/**
* Return the mapping of JDBC to DB types.
*
*
* @return the db type map
*/
public DbTypeMap getDbTypeMap() {
@@ -315,7 +321,7 @@ public class DatabasePlatform {
/**
* Return the close quote for quoted identifiers.
*
*
* @return the close quote
*/
public String getCloseQuote() {
@@ -324,7 +330,7 @@ public class DatabasePlatform {
/**
* Return the open quote for quoted identifiers.
*
*
* @return the open quote
*/
public String getOpenQuote() {
@@ -333,7 +339,7 @@ public class DatabasePlatform {
/**
* Return the JDBC type used to store booleans.
*
*
* @return the boolean db type
*/
public int getBooleanDbType() {
@@ -363,7 +369,7 @@ public class DatabasePlatform {
/**
* Return true if empty strings should be treated as null.
*
*
* @return true, if checks if is treat empty strings as null
*/
public boolean isTreatEmptyStringsAsNull() {
@@ -399,7 +405,7 @@ public class DatabasePlatform {
/**
* Return the DB identity/sequence features for this platform.
*
*
* @return the db identity
*/
public DbIdentity getDbIdentity() {
@@ -412,7 +418,7 @@ public class DatabasePlatform {
* <p>
* Basically add the clauses for limit/offset, rownum, row_number().
* </p>
*
*
* @return the sql limiter
*/
public SqlLimiter getSqlLimiter() {
@@ -421,15 +427,12 @@ public class DatabasePlatform {
/**
* Convert backticks to the platform specific open quote and close quote
*
* <p>
* Specific plugins may implement this method to cater for platform specific
* naming rules.
* </p>
*
* @param dbName
* the db name
*
*
* @param dbName the db name
* @return the string
*/
public String convertQuotedIdentifiers(String dbName) {
@@ -8,6 +8,11 @@ package com.avaje.ebean.config.dbplatform;
*/
public class DbType {
/**
* DB native UUID type (H2 and Postgres).
*/
public static final int UUID = 5010;
/**
* Type to map Map content to Postgres HSTORE.
*/
@@ -49,6 +49,8 @@ public class DbTypeMap {
lookup.put("TIME", Types.TIME);
lookup.put("TIMESTAMP", Types.TIMESTAMP);
lookup.put("UUID", DbType.UUID);
// Not standard java.sql.Types
// logical JSON storage types
lookup.put("JSON", DbType.JSON);
@@ -101,6 +103,9 @@ public class DbTypeMap {
put(Types.BLOB, new DbType("blob"));
put(Types.CLOB, new DbType("clob"));
// DB native UUID support (H2 and Postgres)
put(DbType.UUID, new DbType("uuid"));
if (logicalTypes) {
// keep it logical for 2 layer DDL generation
put(DbType.HSTORE, new DbType("hstore"));
@@ -55,7 +55,7 @@ public abstract class DbViewHistorySupport implements DbHistorySupport {
public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) {
// (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?));
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " < ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " <= ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
}
/**
@@ -0,0 +1,8 @@
package com.avaje.ebean.config.dbplatform;
/**
* Runtime support for @History with H2.
*/
public class H2HistorySupport extends DbViewHistorySupport {
}
@@ -0,0 +1,127 @@
package com.avaje.ebean.config.dbplatform;
import org.h2.api.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* H2 database trigger used to populate history tables to support the @History feature.
*/
public class H2HistoryTrigger implements Trigger {
private static final Logger logger = LoggerFactory.getLogger(H2HistoryTrigger.class);
/**
* Hardcoding the column and history table suffix for now. Not sure how to get that
* configuration into the trigger instance nicely as it is instantiated by H2.
*/
private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
private static final String HISTORY_SUFFIX = "_history";
/**
* SQL to insert into the history table.
*/
private String insertHistorySql;
/**
* Position of SYS_PERIOD_START column in the Object[].
*/
private int effectStartPosition;
/**
* Position of SYS_PERIOD_END column in the Object[].
*/
private int effectEndPosition;
@Override
public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
// get the columns for the table
ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
// build the insert into history table SQL
StringBuilder insertSql = new StringBuilder(150);
insertSql.append("insert into ").append(tableName).append(HISTORY_SUFFIX).append(" (");
int count = 0;
List<String> columns = new ArrayList<String>();
while (rs.next()) {
if (++count > 1) {
insertSql.append(",");
}
String columnName = rs.getString("COLUMN_NAME");
if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
this.effectStartPosition = count - 1;
} else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
this.effectEndPosition = count - 1;
}
insertSql.append(columnName);
columns.add(columnName);
}
insertSql.append(") values (");
for (int i = 0; i < count; i++) {
if (i > 0) {
insertSql.append(",");
}
insertSql.append("?");
}
insertSql.append(");");
this.insertHistorySql = insertSql.toString();
logger.debug("History table insert sql: {}", insertHistorySql);
}
@Override
public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
if (oldRow != null) {
// a delete or update event
Timestamp now = new Timestamp(System.currentTimeMillis());
oldRow[effectEndPosition] = now;
if (newRow != null) {
// update event. Set the effective start timestamp to now.
newRow[effectStartPosition] = now;
}
if (logger.isDebugEnabled()) {
logger.debug("History insert: {}", Arrays.toString(oldRow));
}
insertIntoHistory(connection, oldRow);
}
}
/**
* Insert the data into the history table.
*/
private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
PreparedStatement stmt = connection.prepareStatement(insertHistorySql);
try {
for (int i = 0; i < oldRow.length; i++) {
stmt.setObject(i + 1, oldRow[i]);
}
stmt.executeUpdate();
} finally {
stmt.close();
}
}
@Override
public void close() throws SQLException {
}
@Override
public void remove() throws SQLException {
}
}
@@ -15,6 +15,8 @@ public class H2Platform extends DatabasePlatform {
this.name = "h2";
this.dbEncrypt = new H2DbEncrypt();
this.platformDdl = new H2Ddl(this.dbTypeMap, dbIdentity);
this.historySupport = new H2HistorySupport();
this.nativeUuidType = true;
// only support getGeneratedKeys with non-batch JDBC
// so generally use SEQUENCE instead of IDENTITY for H2
@@ -5,5 +5,4 @@ package com.avaje.ebean.config.dbplatform;
*/
public class MySqlHistorySupport extends DbViewHistorySupport {
}
@@ -23,10 +23,10 @@ public class PostgresPlatform extends DatabasePlatform {
// OnQueryOnly.CLOSE as a performance optimisation on Postgres
this.onQueryOnly = OnQueryOnly.CLOSE;
this.likeClause = "like ? escape''";
this.selectCountWithAlias = true;
this.blobDbType = Types.LONGVARBINARY;
this.clobDbType = Types.VARCHAR;
this.nativeUuidType = true;
this.dbEncrypt = new PostgresDbEncrypt();
this.historySupport = new PostgresHistorySupport();
@@ -263,6 +263,74 @@ public class DdlGenerator implements SpiEbeanPlugin {
}
}
/**
* Local utility used to detect the end of statements / separate statements.
* This is often just the semicolon character but for trigger/procedures this
* detects the $$ demarcation used in the history DDL generation for MySql and
* Postgres.
*/
static class StatementsSeparator {
ArrayList<String> statements = new ArrayList<String>();
boolean trimDelimiter;
boolean inDbProcedure;
StringBuilder sb = new StringBuilder();
void lineContainsDollars(String line) {
if (inDbProcedure) {
if (trimDelimiter) {
line = line.replace("$$","");
}
endOfStatement(line);
} else {
// MySql style delimiter needs to be trimmed/removed
trimDelimiter = line.equals("delimiter $$");
if (!trimDelimiter) {
sb.append(line).append(" ");
}
}
inDbProcedure = !inDbProcedure;
}
void endOfStatement(String line) {
// end of Db procedure
sb.append(line);
statements.add(sb.toString().trim());
sb = new StringBuilder();
}
void nextLine(String line) {
if (line.contains("$$")) {
lineContainsDollars(line);
return;
}
if (inDbProcedure) {
sb.append(line).append(" ");
return;
}
int semiPos = line.indexOf(';');
if (semiPos == -1) {
sb.append(line).append(" ");
} else if (semiPos == line.length() - 1) {
// semicolon at end of line
endOfStatement(line);
} else {
// semicolon in middle of line
String preSemi = line.substring(0, semiPos);
endOfStatement(preSemi);
sb.append(line.substring(semiPos + 1));
}
}
}
/**
* Break up the sql in reader into a list of statements using the semi-colon
* character;
@@ -271,35 +339,16 @@ public class DdlGenerator implements SpiEbeanPlugin {
try {
BufferedReader br = new BufferedReader(reader);
StatementsSeparator statements = new StatementsSeparator();
ArrayList<String> statements = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
String s;
while ((s = br.readLine()) != null) {
s = s.trim();
int semiPos = s.indexOf(';');
if (semiPos == -1) {
sb.append(s).append(" ");
} else if (semiPos == s.length() - 1) {
// semicolon at end of line
sb.append(s);
statements.add(sb.toString().trim());
sb = new StringBuilder();
} else {
// semicolon in middle of line
String preSemi = s.substring(0, semiPos);
sb.append(preSemi);
statements.add(sb.toString().trim());
sb = new StringBuilder();
sb.append(s.substring(semiPos + 1));
}
statements.nextLine(s);
}
return statements;
return statements.statements;
} catch (IOException e) {
throw new PersistenceException(e);
}
@@ -98,7 +98,7 @@ public class BaseTableDdl implements TableDdl {
reset();
String tableName = lowerName(createTable.getName());
String tableName = lowerTableName(createTable.getName());
List<Column> columns = createTable.getColumn();
List<Column> pk = determinePrimaryKeyColumns(columns);
@@ -248,7 +248,7 @@ public class BaseTableDdl implements TableDdl {
protected void writeForeignKey(DdlWrite write, String fkName, String tableName, String[] columns, String refTable, String[] refColumns, String indexName) throws IOException {
tableName = lowerName(tableName);
tableName = lowerTableName(tableName);
DdlBuffer fkeyBuffer = write.applyForeignKeys();
alterTableAddForeignKey(fkeyBuffer, fkName, tableName, columns, refTable, refColumns);
@@ -282,7 +282,7 @@ public class BaseTableDdl implements TableDdl {
if (i > 0) {
buffer.append(",");
}
buffer.append(lowerName(columns[i].trim()));
buffer.append(lowerColumnName(columns[i].trim()));
}
buffer.append(")");
}
@@ -367,7 +367,7 @@ public class BaseTableDdl implements TableDdl {
buffer.append(",").newLine();
buffer.append(" constraint ").append(uqName).append(" unique ");
buffer.append("(");
buffer.append(lowerName(column.getName()));
buffer.append(lowerColumnName(column.getName()));
buffer.append(")");
}
@@ -401,14 +401,17 @@ public class BaseTableDdl implements TableDdl {
}
/**
* Convert the table or column name to lower case.
* <p>
* This is passed up to the platformDdl to override as desired.
* Generally lower case with underscore is a good cross database
* choice for column/table names.
* Convert the table lower case.
*/
protected String lowerName(String name) {
return naming.lowerName(name);
protected String lowerTableName(String name) {
return naming.lowerTableName(name);
}
/**
* Convert the column name to lower case.
*/
protected String lowerColumnName(String name) {
return naming.lowerColumnName(name);
}
/**
@@ -176,7 +176,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
String platformType = platformDdl.convert(type, false);
buffer.append(" ");
buffer.append(platformDdl.lowerName(columnName), 29);
buffer.append(platformDdl.lowerColumnName(columnName), 29);
buffer.append(platformType);
}
@@ -10,6 +10,7 @@ public class H2Ddl extends PlatformDdl {
public H2Ddl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
super(platformTypes, dbIdentity);
this.historyDdl = new H2HistoryDdl();
}
}
@@ -0,0 +1,73 @@
package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.H2HistoryTrigger;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.List;
/**
* H2 history support using DB triggers to maintain a history table.
*/
public class H2HistoryDdl extends DbTriggerBasedHistoryDdl {
private static final String TRIGGER_CLASS = H2HistoryTrigger.class.getName();
public H2HistoryDdl() {
}
@Override
protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement();
}
@Override
protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
apply.append("-- Regenerated ").newLine();
apply.append("-- changes: ").append(update.description()).newLine();
dropTriggers(apply, baseTableName);
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
dropTriggers(rollback, baseTableName);
addCreateTrigger(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
// Note that this does not take into account the historyTable name (excepts _history suffix) and
// does not take into account excluded columns (all columns included in history)
apply
.append("create trigger ").append(triggerName).append(" before update,delete on ").append(baseTable)
.append(" for each row call \"" + TRIGGER_CLASS + "\";").newLine();
}
}
@@ -136,7 +136,7 @@ public class PlatformDdl {
String platformType = convert(column.getType(), identityColumn);
buffer.append(" ");
buffer.append(lowerName(column.getName()), 29);
buffer.append(lowerColumnName(column.getName()), 29);
buffer.append(platformType);
if (isTrue(column.isNotnull()) || isTrue(column.isPrimaryKey())) {
buffer.append(" not null");
@@ -254,7 +254,7 @@ public class PlatformDdl {
appendColumns(columns, buffer);
buffer
.append(" references ")
.append(lowerName(refTable));
.append(lowerTableName(refTable));
appendColumns(refColumns, buffer);
appendWithSpace(foreignKeyRestrict, buffer);
@@ -346,7 +346,7 @@ public class PlatformDdl {
if (i > 0) {
buffer.append(",");
}
buffer.append(lowerName(columns[i].trim()));
buffer.append(lowerColumnName(columns[i].trim()));
}
buffer.append(")");
}
@@ -358,16 +358,26 @@ public class PlatformDdl {
}
/**
* Convert the table or column name to lower case.
* Convert the table to lower case.
* <p>
* This is passed up to the platformDdl to override as desired.
* Generally lower case with underscore is a good cross database
* Override as desired. Generally lower case with underscore is a good cross database
* choice for column/table names.
*/
protected String lowerName(String name) {
return naming.lowerName(name);
protected String lowerTableName(String name) {
return naming.lowerTableName(name);
}
/**
* Convert the column name to lower case.
* <p>
* Override as desired. Generally lower case with underscore is a good cross database
* choice for column/table names.
*/
protected String lowerColumnName(String name) {
return naming.lowerColumnName(name);
}
/**
* Null safe Boolean true test.
*/
@@ -40,7 +40,7 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
.endOfStatement();
if (whenCreatedColumn != null) {
apply.append("update table ").append(baseTableName).append(" set ")
apply.append("update ").append(baseTableName).append(" set ")
.append(sysPeriod).append(" = tstzrange(").append(whenCreatedColumn).append(", null)").endOfStatement();
}
}
@@ -81,12 +81,12 @@ public class PostgresHistoryDdl extends DbTriggerBasedHistoryDdl {
appendInsertIntoHistory(apply, historyTable, includedColumns);
apply
.append(" NEW.").append(sysPeriod).append(" = tstzrange(CURRENT_TIMESTAMP,null);").newLine()
.append(" return new;").newLine().newLine();
.append(" return new;").newLine();
apply
.append(" elsif (TG_OP = 'DELETE') then").newLine();
appendInsertIntoHistory(apply, historyTable, includedColumns);
apply
.append(" return old;").newLine().newLine();
.append(" return old;").newLine();
apply
.append(" end if;").newLine()
.append("end;").newLine()
@@ -77,6 +77,9 @@ public class CurrentModel {
ModelBuildBeanVisitor visitor = new ModelBuildBeanVisitor(context);
VisitAllUsing visit = new VisitAllUsing(visitor, server);
visit.visitAllBeans();
// adjust the foreign keys on the 'draft' tables
context.adjustDraftReferences();
}
return model;
}
@@ -35,6 +35,8 @@ public class MColumn {
*/
private AlterColumn alterColumn;
private boolean draftOnly;
public MColumn(Column column) {
this.name = column.getName();
this.type = column.getType();
@@ -63,6 +65,28 @@ public class MColumn {
this.notnull = notnull;
}
/**
* Return a copy of this column used for creating the associated draft table.
*/
public MColumn copyForDraft() {
MColumn copy = new MColumn(name, type);
copy.draftOnly = draftOnly;
copy.checkConstraint = checkConstraint;
copy.checkConstraintName = checkConstraintName;
copy.defaultValue = defaultValue;
copy.references = references;
copy.foreignKeyName = foreignKeyName;
copy.foreignKeyIndex = foreignKeyIndex;
copy.historyExclude = historyExclude;
copy.notnull = notnull;
copy.primaryKey = primaryKey;
copy.identity = identity;
copy.unique = unique;
copy.uniqueOneToOne = uniqueOneToOne;
return copy;
}
public String getName() {
return name;
}
@@ -174,6 +198,21 @@ public class MColumn {
return uniqueOneToOne;
}
/**
* Set the draftOnly status for this column.
*/
public void setDraftOnly(boolean draftOnly) {
this.draftOnly = draftOnly;
}
/**
* Return the draftOnly status for this column.
*/
public boolean isDraftOnly() {
return draftOnly;
}
public Column createColumn() {
Column c = new Column();
@@ -14,7 +14,7 @@ import java.util.List;
*/
public class MCompoundForeignKey {
private final String name;
private String name;
private final String referenceTable;
private final List<String> columns = new ArrayList<String>();
private final List<String> referenceColumns = new ArrayList<String>();
@@ -47,6 +47,28 @@ public class MCompoundForeignKey {
return fk;
}
/**
* Add a counter to the foreign key and index names to avoid duplication.
*/
public void addNameSuffix(int counter) {
this.name = name + "_" + counter;
this.indexName = indexName + "_" + counter;
}
/**
* Return the foreign key name.
*/
public String getName() {
return name;
}
/**
* Return the index name.
*/
public String getIndexName() {
return indexName;
}
/**
* Return the columns making up the foreign key in order.
*/
@@ -14,6 +14,7 @@ import com.avaje.ebean.dbmigration.migration.UniqueConstraint;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -42,6 +43,17 @@ public class MTable {
*/
private final String name;
/**
* The associated draft table.
*/
private MTable draftTable;
/**
* Marked true for draft tables. These need to have their FK references adjusted
* after all the draft tables have been identified.
*/
private boolean draft;
/**
* Primary key name.
*/
@@ -106,6 +118,27 @@ public class MTable {
*/
private AddColumn addColumn;
/**
* Create a copy of this table structure as a 'draft' table.
*
* Note that both tables contain @DraftOnly MColumns and these are filtered out
* later when creating the CreateTable object.
*/
public MTable createDraftTable() {
draftTable = new MTable(name+"_draft");
draftTable.draft = true;
draftTable.whenCreatedColumn = whenCreatedColumn;
// compoundKeys
// compoundUniqueConstraints
draftTable.identityType = identityType;
for (MColumn col: columns.values()) {
draftTable.addColumn(col.copyForDraft());
}
return draftTable;
}
/**
* Construct for migration.
@@ -163,7 +196,10 @@ public class MTable {
}
for (MColumn column : this.columns.values()) {
createTable.getColumn().add(column.createColumn());
// filter out draftOnly columns from the base table
if (draft || !column.isDraftOnly()) {
createTable.getColumn().add(column.createColumn());
}
}
for (MCompoundForeignKey compoundKey : compoundKeys) {
@@ -270,6 +306,13 @@ public class MTable {
return name;
}
/**
* Return true if this table is a 'Draft' table.
*/
public boolean isDraft() {
return draft;
}
public String getPkName() {
return pkName;
}
@@ -482,4 +525,70 @@ public class MTable {
return (value == 0) ? null : BigInteger.valueOf(value);
}
/**
* Check if there are duplicate foreign keys.
* <p>
* This can occur when an ManyToMany relates back to itself.
* </p>
*/
public void checkDuplicateForeignKeys() {
if (hasDuplicateForeignKeys()) {
int counter = 1;
for (MCompoundForeignKey fk : compoundKeys) {
fk.addNameSuffix(counter++);
}
}
}
/**
* Return true if the foreign key names are not unique.
*/
private boolean hasDuplicateForeignKeys() {
Set<String> fkNames = new HashSet<String>();
for (MCompoundForeignKey fk : compoundKeys) {
if (!fkNames.add(fk.getName())) {
return true;
}
}
return false;
}
/**
* Adjust the references (FK) if it should relate to a draft table.
*/
public void adjustReferences(ModelContainer modelContainer) {
Collection<MColumn> cols = columns.values();
for (MColumn col : cols) {
String references = col.getReferences();
if (references != null) {
String baseTable = extractBaseTable(references);
MTable refBaseTable = modelContainer.getTable(baseTable);
if (refBaseTable.draftTable != null) {
// change references to another associated 'draft' table
String newReferences = deriveReferences(references, refBaseTable.draftTable.getName());
col.setReferences(newReferences);
}
}
}
}
/**
* Return the base table name from references (table.column).
*/
private String extractBaseTable(String references) {
int lastDot = references.lastIndexOf('.');
return references.substring(0,lastDot);
}
/**
* Return the new references using the given draftTableName.
* (The referenced column is the same as before).
*/
private String deriveReferences(String references, String draftTableName) {
int lastDot = references.lastIndexOf('.');
return draftTableName+"."+references.substring(lastDot+1);
}
}
@@ -12,6 +12,7 @@ import com.avaje.ebean.dbmigration.migration.DropIndex;
import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.Migration;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -38,6 +39,18 @@ public class ModelContainer {
}
/**
* Adjust the FK references on all the draft tables.
*/
public void adjustDraftReferences() {
Collection<MTable> tables = this.tables.values();
for (MTable table : tables) {
if (table.isDraft()) {
table.adjustReferences(this);
}
}
}
/**
* Return the map of all the tables.
*/
@@ -213,8 +226,8 @@ public class ModelContainer {
/**
* Add a table (typically from reading EbeanServer meta data).
*/
public void addTable(MTable table) {
tables.put(table.getName(), table);
public MTable addTable(MTable table) {
return tables.put(table.getName(), table);
}
/**
@@ -5,7 +5,6 @@ import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.dbmigration.migration.IdentityType;
import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.visitor.BeanPropertyVisitor;
import com.avaje.ebean.dbmigration.model.visitor.BeanVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
@@ -29,7 +28,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
* This creates an MTable and adds it to the model.
* </p>
*/
public BeanPropertyVisitor visitBean(BeanDescriptor<?> descriptor) {
public ModelBuildPropertyVisitor visitBean(BeanDescriptor<?> descriptor) {
if (!descriptor.isInheritanceRoot()) {
return null;
@@ -58,7 +57,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
table.addColumn(new MColumn(discColumn, discDbType, true));
}
return new ModelBuildPropertyVisitor(ctx, table, descriptor.getCompoundUniqueConstraints());
return new ModelBuildPropertyVisitor(ctx, table, descriptor);
}
/**
@@ -3,11 +3,14 @@ package com.avaje.ebean.dbmigration.model.build;
import com.avaje.ebean.config.DbConstraintNaming;
import com.avaje.ebean.config.dbplatform.DbType;
import com.avaje.ebean.config.dbplatform.DbTypeMap;
import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.ModelContainer;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.type.ScalarType;
import java.util.Collection;
/**
* The context used during DDL generation.
*/
@@ -31,6 +34,14 @@ public class ModelBuildContext {
this.maxLength = maxLength;
}
/**
* Adjust the foreign key references on any draft tables (that reference other draft tables).
* This is called as a 'second pass' after all the draft tables have been identified.
*/
public void adjustDraftReferences() {
model.adjustDraftReferences();
}
public String primaryKeyName(String tableName) {
return maxLength(constraintNaming.primaryKeyName(tableName), 0);
}
@@ -67,8 +78,8 @@ public class ModelBuildContext {
return maxLength(constraintNaming.checkConstraintName(tableName, columnName), checkCount);
}
public void addTable(MTable table) {
model.addTable(table);
public MTable addTable(MTable table) {
return model.addTable(table);
}
public void addIndex(String indexName, String tableName, String columnName) {
@@ -120,4 +131,30 @@ public class ModelBuildContext {
return dbTypeMap.get(dbType);
}
/**
* Create the draft table for a given table.
*/
public void createDraft(MTable table) {
MTable draftTable = table.createDraftTable();
draftTable.setPkName(primaryKeyName(draftTable.getName()));
int fkCount = 0;
int ixCount = 0;
Collection<MColumn> cols = draftTable.getColumns().values();
for (MColumn col: cols) {
if (col.getForeignKeyName() != null) {
// Note that we adjust the 'references' table later in a second pass
// after we know all the tables that are 'draftable'
//col.setReferences(refTable + "." + refColumn);
col.setForeignKeyName(foreignKeyConstraintName(draftTable.getName(), col.getName(), ++fkCount));
String[] indexCols = {col.getName()};
col.setForeignKeyIndex(foreignKeyIndexName(draftTable.getName(), indexCols, ++ixCount));
}
}
addTable(draftTable);
}
}
@@ -35,9 +35,18 @@ public class ModelBuildIntersectionTable {
public void build() {
intersectionTable = createTable();
ctx.addTable(intersectionTable);
MTable existingTable = ctx.addTable(intersectionTable);
if (existingTable != null) {
throw new IllegalStateException("Property " + manyProp.getFullBeanName() + " has duplicate ManyToMany intersection table " + intersectionTable.getName()
+ ". Please use @JoinTable to define unique table to use");
}
buildFkConstraints();
if (manyProp.getTargetDescriptor().isDraftable()) {
ctx.createDraft(intersectionTable);
}
}
private void buildFkConstraints() {
@@ -47,6 +56,8 @@ public class ModelBuildIntersectionTable {
BeanDescriptor<?> targetDesc = manyProp.getTargetDescriptor();
buildFkConstraints(targetDesc, tableJoin.columns(), false);
intersectionTable.checkDuplicateForeignKeys();
}
@@ -5,6 +5,7 @@ import com.avaje.ebean.dbmigration.model.MColumn;
import com.avaje.ebean.dbmigration.model.MCompoundForeignKey;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.dbmigration.model.visitor.BaseTablePropertyVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
@@ -14,6 +15,7 @@ import com.avaje.ebeaninternal.server.deploy.TableJoinColumn;
import com.avaje.ebeaninternal.server.deploy.id.ImportedId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -26,6 +28,8 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
private final MTable table;
private final BeanDescriptor<?> beanDescriptor;
private final IndexSet indexSet = new IndexSet();
private MColumn lastColumn;
@@ -36,11 +40,11 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
private int countCheck;
public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, CompoundUniqueConstraint[] constraints) {
public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, BeanDescriptor<?> beanDescriptor) {
this.ctx = ctx;
this.table = table;
addCompoundUniqueConstraint(constraints);
this.beanDescriptor = beanDescriptor;
addCompoundUniqueConstraint(beanDescriptor.getCompoundUniqueConstraints());
}
/**
@@ -97,6 +101,19 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
compoundKey.setIndexName(null);
}
}
addDraftTable();
}
/**
* Create a 'draft' table that is mostly the same as the base table.
* It has @DraftOnly columns and adjusted primary and foreign keys.
*/
private void addDraftTable() {
if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) {
// create a 'Draft' table which looks very similar (change PK, FK etc)
ctx.createDraft(table);
}
}
@@ -213,6 +230,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
}
MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p));
col.setDraftOnly(p.isDraftOnly());
if (p.isId()) {
col.setPrimaryKey(true);
@@ -1,5 +1,6 @@
package com.avaje.ebean.dbmigration.model.visitor;
import com.avaje.ebean.dbmigration.model.build.ModelBuildPropertyVisitor;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
/**
@@ -12,6 +13,6 @@ public interface BeanVisitor {
* Visit a BeanDescriptor and return a PropertyVisitor to use to visit each
* property on the entity bean (return null to skip visiting this bean).
*/
BeanPropertyVisitor visitBean(BeanDescriptor<?> descriptor);
ModelBuildPropertyVisitor visitBean(BeanDescriptor<?> descriptor);
}
@@ -0,0 +1,8 @@
/**
* Generation DDL migration scripts based on changes to the model.
* <p>
* You can use <code>DbMigration</code> to compare the entity bean model to its prior state
* and generate DDL for the differences - adding tables, columns etc.
* </p>
*/
package com.avaje.ebean.dbmigration;
@@ -0,0 +1,13 @@
/**
* Provides a built in change log mechanism and can audit changes.
* <p>
* Although you can build something similar using existing event adapters such as <code>BeanPersistController</code>
* this is built for purpose to reduce the effort and provide optimal performance for auditing or logging changes.
* </p>
* <p>
* By default you can annotate beans with <code>@ChangeLog</code> and associated change events are logged by default
* in a JSON form with appropriate auditing attributes such as who made the changes and ip address of the user etc
* via implementation of <code>ChangeLogPrepare</code>
* </p>
*/
package com.avaje.ebean.event.changelog;
@@ -0,0 +1,8 @@
/**
* Provides Auditing of read events including queries and L2 cache.
* <p>
* Provides a built support for supplied an audit of all the 'read events' for beans annotated
* with <code>@ReadAudit</code>
* </p>
*/
package com.avaje.ebean.event.readaudit;
@@ -0,0 +1,8 @@
/**
* Provides a API for plugins.
* <p>
* Plugins can get and read meta data about the entity beans and enhance or utilise the
* functionality of the EbeanServer.
* </p>
*/
package com.avaje.ebean.plugin;
@@ -8,140 +8,139 @@ import org.slf4j.LoggerFactory;
* <p>
* Helper for ClassUtil.
* </p>
*
*
* @author rbygrave
*
*/
class ClassLoadContext {
private static final Logger logger = LoggerFactory.getLogger(ClassLoadContext.class);
private final ClassLoader callerLoader;
private static final Logger logger = LoggerFactory.getLogger(ClassLoadContext.class);
private final ClassLoader contextLoader;
private final ClassLoader callerLoader;
private final boolean preferContext;
private boolean ambiguous;
public static ClassLoadContext of(Class<?> caller, boolean preferContext) {
return new ClassLoadContext(caller, preferContext);
private final ClassLoader contextLoader;
private final boolean preferContext;
private boolean ambiguous;
public static ClassLoadContext of(Class<?> caller, boolean preferContext) {
return new ClassLoadContext(caller, preferContext);
}
/**
* This constructor is package-private to restrict instantiation to
*/
ClassLoadContext(final Class<?> caller, boolean preferContext) {
if (caller == null) {
throw new IllegalArgumentException("caller is null");
}
this.callerLoader = caller.getClassLoader();
this.contextLoader = Thread.currentThread().getContextClassLoader();
this.preferContext = preferContext;
}
public Class<?> forName(String name) throws ClassNotFoundException {
ClassLoader defaultLoader = getDefault(preferContext);
try {
return Class.forName(name, true, defaultLoader);
} catch (ClassNotFoundException e) {
if (callerLoader == defaultLoader) {
throw e;
} else {
return Class.forName(name, true, callerLoader);
}
}
}
/**
* Return the expected class loader to use.
* <p>
* Works on the assumption that the child of the caller or context class
* loader is preferred.
* </p>
*/
public ClassLoader getDefault(boolean preferContext) {
if (contextLoader == null) {
if (logger.isDebugEnabled()) {
logger.debug("No Context ClassLoader, using " + callerLoader.getClass().getName());
}
return callerLoader;
}
if (contextLoader == callerLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Context and Caller ClassLoader's same instance of " + contextLoader.getClass().getName());
}
return callerLoader;
}
/**
* This constructor is package-private to restrict instantiation to
*/
ClassLoadContext(final Class<?> caller, boolean preferContext) {
if (caller == null){
throw new IllegalArgumentException("caller is null");
}
this.callerLoader = caller.getClassLoader();
this.contextLoader = Thread.currentThread().getContextClassLoader();
this.preferContext = preferContext;
if (isChild(contextLoader, callerLoader)) {
if (logger.isDebugEnabled()) {
logger.debug("Caller ClassLoader " + callerLoader.getClass().getName()
+ " child of ContextLoader " + contextLoader.getClass().getName());
}
return callerLoader;
} else if (isChild(callerLoader, contextLoader)) {
if (logger.isDebugEnabled()) {
logger.debug("Context ClassLoader " + contextLoader.getClass().getName()
+ " child of Caller ClassLoader " + callerLoader.getClass().getName());
}
return contextLoader;
} else {
// ambiguous case, perhaps both null
logger.debug("Ambiguous ClassLoader choice preferContext:" + preferContext
+ " Context:" + contextLoader.getClass().getName() + " Caller:" + callerLoader.getClass().getName());
ambiguous = true;
return preferContext ? contextLoader : callerLoader;
}
}
/**
* Return true if the 'default' class loader is ambiguous.
*/
public boolean isAmbiguous() {
return ambiguous;
}
/**
* Return the ClassLoader of the caller.
*/
public ClassLoader getCallerLoader() {
return callerLoader;
}
/**
* Return the Thread Context ClassLoader.
*/
public ClassLoader getContextLoader() {
return contextLoader;
}
/**
* Return the ClassLoader for this class.
*/
public ClassLoader getThisLoader() {
return this.getClass().getClassLoader();
}
/**
* Returns 'true' if 'loader2' is a delegation child of 'loader1' [or if
* 'loader1'=='loader2'].
*/
private boolean isChild(final ClassLoader loader1, ClassLoader loader2) {
for (; loader2 != null; loader2 = loader2.getParent()) {
if (loader2 == loader1) {
return true;
}
}
public Class<?> forName(String name) throws ClassNotFoundException {
ClassLoader defaultLoader = getDefault(preferContext);
try {
return Class.forName(name, true, defaultLoader);
} catch (ClassNotFoundException e) {
if (callerLoader == defaultLoader) {
throw e;
} else {
return Class.forName(name, true, callerLoader);
}
}
}
/**
* Return the expected class loader to use.
* <p>
* Works on the assumption that the child of the caller or context class
* loader is preferred.
* </p>
*/
public ClassLoader getDefault(boolean preferContext) {
if (contextLoader == null){
if (logger.isDebugEnabled()){
logger.debug("No Context ClassLoader, using "+callerLoader.getClass().getName());
}
return callerLoader;
}
if (contextLoader == callerLoader){
if (logger.isDebugEnabled()){
logger.debug("Context and Caller ClassLoader's same instance of "+contextLoader.getClass().getName());
}
return callerLoader;
}
if (isChild(contextLoader, callerLoader)) {
if (logger.isDebugEnabled()){
logger.debug("Caller ClassLoader "+callerLoader.getClass().getName()
+" child of ContextLoader "+contextLoader.getClass().getName());
}
return callerLoader;
} else if (isChild(callerLoader, contextLoader)) {
if (logger.isDebugEnabled()){
logger.debug("Context ClassLoader "+contextLoader.getClass().getName()
+" child of Caller ClassLoader "+callerLoader.getClass().getName());
}
return contextLoader;
} else {
// ambiguous case, perhaps both null
logger.debug("Ambiguous ClassLoader choice preferContext:"+preferContext
+" Context:"+contextLoader.getClass().getName()+" Caller:"+callerLoader.getClass().getName());
ambiguous = true;
return preferContext ? contextLoader : callerLoader;
}
}
/**
* Return true if the 'default' class loader is ambiguous.
*/
public boolean isAmbiguous() {
return ambiguous;
}
/**
* Return the ClassLoader of the caller.
*/
public ClassLoader getCallerLoader() {
return callerLoader;
}
/**
* Return the Thread Context ClassLoader.
*/
public ClassLoader getContextLoader() {
return contextLoader;
}
/**
* Return the ClassLoader for this class.
*/
public ClassLoader getThisLoader() {
return this.getClass().getClassLoader();
}
/**
* Returns 'true' if 'loader2' is a delegation child of 'loader1' [or if
* 'loader1'=='loader2'].
*/
private boolean isChild(final ClassLoader loader1, ClassLoader loader2) {
for (; loader2 != null; loader2 = loader2.getParent()) {
if (loader2 == loader1) {
return true;
}
}
return false;
}
return false;
}
}
@@ -1,23 +1,11 @@
package com.avaje.ebeaninternal.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper to find classes taking into account the context class loader.
*/
public class ClassUtil {
private static final Logger logger = LoggerFactory.getLogger(ClassUtil.class);
/**
* Load a class taking into account a context class loader (if present).
*/
public static Class<?> forName(String name) throws ClassNotFoundException {
return forName(name, null);
}
/**
* Load a class taking into account a context class loader (if present).
*/
@@ -27,24 +15,9 @@ public class ClassUtil {
caller = ClassUtil.class;
}
ClassLoadContext ctx = ClassLoadContext.of(caller, true);
return ctx.forName(name);
}
public static ClassLoader getClassLoader(Class<?> caller, boolean preferContext) {
if (caller == null) {
caller = ClassUtil.class;
}
ClassLoadContext ctx = ClassLoadContext.of(caller, preferContext);
ClassLoader classLoader = ctx.getDefault(preferContext);
if (ctx.isAmbiguous()) {
logger.info("Ambigous ClassLoader (Context vs Caller) chosen " + classLoader);
}
return classLoader;
}
/**
* Return true if javax validation annotations like Size and NotNull are present.
*/
@@ -59,11 +59,6 @@ public class ScopedTransaction implements SpiTransaction {
}
}
@Override
public void endQueryOnly() {
transaction.endQueryOnly();
}
@Override
public String getLogPrefix() {
return transaction.getLogPrefix();
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.api;
import java.util.ArrayList;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
@@ -10,6 +11,11 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
*/
public interface SpiExpressionRequest {
/**
* Return the DB specific JSON expression handler.
*/
JsonExpressionHandler getJsonHander();
/**
* Parse the logical property name to the deployment name.
*/
@@ -94,6 +94,11 @@ public interface SpiQuery<T> extends Query<T> {
}
enum TemporalMode {
/**
* Query runs against draft tables.
*/
DRAFT,
/**
* Query runs against current data (normal).
*/
@@ -171,6 +176,11 @@ public interface SpiQuery<T> extends Query<T> {
*/
boolean isAsOfQuery();
/**
* Return true if this is a 'As Draft' query.
*/
boolean isAsDraft();
/**
* Return the asOf Timestamp which the query should run as.
*/
@@ -19,11 +19,6 @@ import com.avaje.ebeaninternal.server.persist.BatchControl;
*/
public interface SpiTransaction extends Transaction {
/**
* End the transaction when had query only use.
*/
void endQueryOnly();
/**
* Return the string prefix with the transactin id and label used in logging.
*/
@@ -1,14 +1,14 @@
package com.avaje.ebeaninternal.server.core;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.Set;
import com.avaje.ebean.BeanState;
import com.avaje.ebean.ValuePair;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.EntityBeanIntercept;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.Set;
/**
* Default implementation of BeanState.
*/
@@ -23,9 +23,13 @@ public class DefaultBeanState implements BeanState {
this.intercept = entityBean._ebean_getIntercept();
}
public boolean isReference() {
return intercept.isReference();
}
public void setPropertyLoaded(String propertyName, boolean loaded) {
intercept.setPropertyLoaded(propertyName, loaded);
}
public boolean isReference() {
return intercept.isReference();
}
public boolean isNew() {
return intercept.isNew();
@@ -1301,6 +1301,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query, t);
try {
request.initTransIfRequired();
request.markNotQueryOnly();
return request.delete();
} finally {
request.endTransIfRequired();
@@ -1624,6 +1625,40 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
}
public <T> List<T> publish(Query<T> query, Transaction transaction) {
TransWrapper wrap = initTransIfRequired(transaction);
try {
SpiTransaction trans = wrap.transaction;
List<T> liveBeans = persister.publish(query, trans);
wrap.commitIfCreated();
return liveBeans;
} catch (RuntimeException e) {
wrap.rollbackIfCreated();
throw e;
}
}
@Override
public <T> T publish(Class<T> beanType, Object id) {
return publish(beanType, id, null);
}
@Override
public <T> List<T> publish(Query<T> query) {
return publish(query, null);
}
@Override
public <T> T publish(Class<T> beanType, Object id, Transaction transaction) {
Query<T> query = find(beanType).setId(id);
List<T> liveBeans = publish(query, transaction);
return (liveBeans.size() == 1) ? liveBeans.get(0) : null;
}
private EntityBean checkEntityBean(Object bean) {
if (bean == null) {
throw new IllegalArgumentException(Message.msg("bean.isnull"));
@@ -39,6 +39,7 @@ import com.avaje.ebeaninternal.server.readaudit.DefaultReadAuditPrepare;
import com.avaje.ebeaninternal.server.text.json.DJsonContext;
import com.avaje.ebeaninternal.server.transaction.AutoCommitTransactionManager;
import com.avaje.ebeaninternal.server.transaction.DefaultTransactionScopeManager;
import com.avaje.ebeaninternal.server.transaction.ExplicitTransactionManager;
import com.avaje.ebeaninternal.server.transaction.ExternalTransactionScopeManager;
import com.avaje.ebeaninternal.server.transaction.JtaTransactionManager;
import com.avaje.ebeaninternal.server.transaction.TransactionManager;
@@ -126,13 +127,14 @@ public class InternalConfiguration {
this.beanDescriptorManager = new BeanDescriptorManager(this);
Map<String, String> asOfTableMapping = beanDescriptorManager.deploy();
Map<String, String> draftTableMap = beanDescriptorManager.getDraftTableMap();
this.transactionManager = createTransactionManager();
DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform();
this.binder = getBinder(typeManager, databasePlatform);
this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod());
this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod(), draftTableMap);
ExternalTransactionManager externalTransactionManager = serverConfig.getExternalTransactionManager();
if (externalTransactionManager == null && serverConfig.isUseJtaTransactionManager()) {
@@ -208,11 +210,29 @@ public class InternalConfiguration {
* For 'As Of' queries return the number of bind variables per predicate.
*/
private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform) {
JsonExpressionHandler jsonHandler = getJsonExpressionHandler(databasePlatform);
DbHistorySupport historySupport = databasePlatform.getHistorySupport();
if (historySupport == null) {
return new Binder(typeManager, 0, false);
return new Binder(typeManager, 0, false, jsonHandler);
}
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause());
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause(), jsonHandler);
}
/**
* Return the JSON expression handler for the given database platform.
*/
private JsonExpressionHandler getJsonExpressionHandler(DatabasePlatform databasePlatform) {
String name = databasePlatform.getName();
if ("postgres".equalsIgnoreCase(name)) {
return new PostgresJsonExpression();
}
if ("oracle".equalsIgnoreCase(name)) {
return new OracleJsonExpression();
}
return new NotSupportedJsonExpression();
}
/**
@@ -220,6 +240,10 @@ public class InternalConfiguration {
*/
private TransactionManager createTransactionManager() {
if (serverConfig.isExplicitTransactionBeginMode()) {
return new ExplicitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses());
}
if (isAutoCommitMode()) {
return new AutoCommitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses());
}
@@ -0,0 +1,17 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Adds the db platform specific json expression.
*/
public interface JsonExpressionHandler {
/**
* Write the db platform specific json expression.
*/
void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value);
}
@@ -0,0 +1,15 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Not supported JSON expression handler.
*/
public class NotSupportedJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
throw new RuntimeException("JSON expressions only supported on Postgres and Oracle");
}
}
@@ -0,0 +1,24 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class OracleJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
if (operator == Op.EXISTS) {
request.append("json_exists(").append(propName).append(", '$.").append(path).append("')");
} else if (operator == Op.NOT_EXISTS) {
request.append("not json_exists(").append(propName).append(", '$.").append(path).append("')");
} else {
request.append("json_value(").append(propName).append(", '$.").append(path).append("')");
request.append(operator.bind());
}
}
}
@@ -10,6 +10,7 @@ import javax.persistence.PersistenceException;
import com.avaje.ebean.*;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanQueryRequest;
@@ -71,6 +72,13 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
this.readOnly = query.isReadOnly();
}
/**
* Mark the transaction as not being query only.
*/
@Override
public void markNotQueryOnly() {
transaction.markNotQueryOnly();
}
/**
* Return the database platform like clause.
@@ -151,6 +159,14 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
return persistenceContext;
}
/**
* Add the bean to the persistence context.
*/
public void persistenceContextAdd(EntityBean bean) {
Object id = beanDescriptor.getId(bean);
persistenceContext.put(id, bean);
}
/**
* This will create a local (readOnly) transaction if no current transaction
* exists.
@@ -210,7 +226,7 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
*/
public void endTransIfRequired() {
if (createdTransaction) {
transaction.endQueryOnly();
transaction.commit();
}
}
@@ -61,6 +61,8 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private final boolean dirty;
private final boolean publish;
private ConcurrencyMode concurrencyMode;
/**
@@ -120,7 +122,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private boolean requestUpdateAllLoadedProps;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse) {
PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse, boolean publish) {
super(server, t, persistExecute);
this.entityBean = (EntityBean) bean;
@@ -132,7 +134,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
this.parentBean = parentBean;
this.controller = beanDescriptor.getPersistController();
this.type = type;
if (saveRecurse) {
this.persistCascade = t.isPersistCascade();
}
@@ -147,6 +149,10 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
beanDescriptor.checkMutableProperties(intercept);
}
this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept);
this.publish = publish;
if (!publish && beanDescriptor.isDraftable()) {
beanDescriptor.setDraftDirty(entityBean, true);
}
this.dirty = intercept.isDirty();
}
@@ -229,7 +235,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
public boolean isNotify() {
this.notifyCache = beanDescriptor.isCacheNotify();
this.notifyCache = beanDescriptor.isCacheNotify(publish);
return notifyCache || isNotifyPersistListener();
}
@@ -243,17 +249,17 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
public void notifyCache() {
if (notifyCache) {
switch (type) {
case INSERT:
beanDescriptor.cacheHandleInsert(this);
break;
case UPDATE:
beanDescriptor.cacheHandleUpdate(idValue, this);
break;
case DELETE:
// Bean deleted from cache early via postDelete()
break;
default:
throw new IllegalStateException("Invalid type " + type);
case INSERT:
beanDescriptor.cacheHandleInsert(this);
break;
case UPDATE:
beanDescriptor.cacheHandleUpdate(idValue, this);
break;
case DELETE:
// Bean deleted from cache early via postDelete()
break;
default:
throw new IllegalStateException("Invalid type " + type);
}
}
}
@@ -447,24 +453,24 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
@Override
public int executeNow() {
switch (type) {
case INSERT:
persistExecute.executeInsertBean(this);
return -1;
case INSERT:
persistExecute.executeInsertBean(this);
return -1;
case UPDATE:
if (beanPersistListener != null) {
// store the updated properties for sending later
updatedProperties = getUpdatedProperties();
}
persistExecute.executeUpdateBean(this);
return -1;
case UPDATE:
if (beanPersistListener != null) {
// store the updated properties for sending later
updatedProperties = getUpdatedProperties();
}
persistExecute.executeUpdateBean(this);
return -1;
case DELETE:
persistExecute.executeDeleteBean(this);
return -1;
case DELETE:
persistExecute.executeDeleteBean(this);
return -1;
default:
throw new RuntimeException("Invalid type " + type);
default:
throw new RuntimeException("Invalid type " + type);
}
}
@@ -513,8 +519,12 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
throw new OptimisticLockException(m, null, bean);
}
switch (type) {
case DELETE: postDelete(); break;
case UPDATE: postUpdate(); break;
case DELETE:
postDelete();
break;
case UPDATE:
postUpdate();
break;
default: // do nothing
}
}
@@ -571,35 +581,36 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private void controllerPost() {
switch (type) {
case INSERT:
controller.postInsert(this);
break;
case UPDATE:
controller.postUpdate(this);
break;
case DELETE:
controller.postDelete(this);
break;
default:
break;
case INSERT:
controller.postInsert(this);
break;
case UPDATE:
controller.postUpdate(this);
break;
case DELETE:
controller.postDelete(this);
break;
default:
break;
}
}
private void logSummary() {
String draft = (beanDescriptor.isDraftable() && !publish) ? " draft[true]" : "";
String name = beanDescriptor.getName();
switch (type) {
case INSERT:
transaction.logSummary("Inserted [" + name + "] [" + idValue + "]");
break;
case UPDATE:
transaction.logSummary("Updated [" + name + "] [" + idValue + "]");
break;
case DELETE:
transaction.logSummary("Deleted [" + name + "] [" + idValue + "]");
break;
default:
break;
case INSERT:
transaction.logSummary("Inserted [" + name + "] [" + idValue + "]" + draft);
break;
case UPDATE:
transaction.logSummary("Updated [" + name + "] [" + idValue + "]" + draft);
break;
case DELETE:
transaction.logSummary("Deleted [" + name + "] [" + idValue + "]" + draft);
break;
default:
break;
}
}
@@ -720,7 +731,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
/**
* Determine if all loaded properties should be used for an update.
* <p>
* Takes into account transaction setting and JDBC batch.
* Takes into account transaction setting and JDBC batch.
* </p>
*/
public boolean determineUpdateAllLoadedProperties() {
@@ -736,4 +747,44 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
return requestUpdateAllLoadedProps;
}
/**
* Return true if this request is a 'publish' action.
*/
public boolean isPublish() {
return publish;
}
/**
* Return the key for an update persist request.
*/
public int getUpdatePlanHash() {
int hash;
if (determineUpdateAllLoadedProperties()) {
hash = intercept.getLoadedPropertyHash();
} else {
hash = intercept.getDirtyPropertyHash();
}
BeanProperty versionProperty = beanDescriptor.getVersionProperty();
if (versionProperty != null) {
if (intercept.isLoadedProperty(versionProperty.getPropertyIndex())) {
hash = hash * 31 + 7;
}
}
if (publish) {
hash = hash * 31;
}
return hash;
}
/**
* Return the table to update depending if the request is a 'publish' one or normal.
*/
public String getUpdateTable() {
return publish ? beanDescriptor.getBaseTable() : beanDescriptor.getDraftTable();
}
}
@@ -1,14 +1,15 @@
package com.avaje.ebeaninternal.server.core;
import java.util.Collection;
import java.util.List;
import com.avaje.ebean.CallableSql;
import com.avaje.ebean.Query;
import com.avaje.ebean.SqlUpdate;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.Update;
import com.avaje.ebean.bean.EntityBean;
/**
* API for persisting a bean.
*/
@@ -17,77 +18,79 @@ public interface Persister {
/**
* Update the bean.
*/
void update(EntityBean entityBean, Transaction t);
void update(EntityBean entityBean, Transaction t);
/**
* Update the bean specifying deleteMissingChildren.
*/
void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren);
/**
* Update the bean specifying deleteMissingChildren.
*/
void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren);
/**
* Force an Insert using the given bean.
*/
void insert(EntityBean entityBean, Transaction t);
/**
* Force an Insert using the given bean.
*/
void insert(EntityBean entityBean, Transaction t);
/**
* Insert or update the bean depending on its state.
*/
void save(EntityBean entityBean, Transaction t);
/**
* Insert or update the bean depending on its state.
*/
void save(EntityBean entityBean, Transaction t);
/**
* Save the associations of a ManyToMany given the owner bean and the
* propertyName of the ManyToMany collection.
*/
void saveManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Save the associations of a ManyToMany given the owner bean and the
* propertyName of the ManyToMany collection.
*/
void saveManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Save an association (OneToMany, ManyToOne, OneToOne or ManyToMany).
*
* @param parentBean
* the bean that owns the association.
* @param propertyName
* the name of the property to save.
* @param t
* the transaction to use.
*/
void saveAssociation(EntityBean parentBean, String propertyName, Transaction t);
/**
* Save an association (OneToMany, ManyToOne, OneToOne or ManyToMany).
*
* @param parentBean the bean that owns the association.
* @param propertyName the name of the property to save.
* @param t the transaction to use.
*/
void saveAssociation(EntityBean parentBean, String propertyName, Transaction t);
/**
* Delete the associations of a ManyToMany given the owner bean and the property name of the ManyToMany.
*/
int deleteManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Delete the associations of a ManyToMany given the owner bean and the property name of the ManyToMany.
*/
int deleteManyToManyAssociations(EntityBean ownerBean, String propertyName, Transaction t);
/**
* Delete a bean given it's type and id value.
* <p>
* This will also cascade delete one level of children.
* </p>
*/
int delete(Class<?> beanType, Object id, Transaction transaction);
/**
* Delete a bean given it's type and id value.
* <p>
* This will also cascade delete one level of children.
* </p>
*/
int delete(Class<?> beanType, Object id, Transaction transaction);
/**
* Delete the bean.
*/
void delete(EntityBean entityBean, Transaction t);
/**
* Delete the bean.
*/
void delete(EntityBean entityBean, Transaction t);
/**
* Delete multiple beans given a collection of Id values.
*/
void deleteMany(Class<?> beanType, Collection<?> ids, Transaction transaction);
/**
* Delete multiple beans given a collection of Id values.
*/
void deleteMany(Class<?> beanType, Collection<?> ids, Transaction transaction);
/**
* Execute the Update.
*/
int executeOrmUpdate(Update<?> update, Transaction t);
/**
* Execute the UpdateSql.
*/
int executeSqlUpdate(SqlUpdate update, Transaction t);
/**
* Execute the CallableSql.
*/
int executeCallable(CallableSql callable, Transaction t);
/**
* Publish the draft beans matching the given query.
*/
<T> List<T> publish(Query<T> query, Transaction transaction);
/**
* Execute the Update.
*/
int executeOrmUpdate(Update<?> update, Transaction t);
/**
* Execute the UpdateSql.
*/
int executeSqlUpdate(SqlUpdate update, Transaction t);
/**
* Execute the CallableSql.
*/
int executeCallable(CallableSql callable, Transaction t);
}
@@ -0,0 +1,71 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class PostgresJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
StringBuilder sb = new StringBuilder(50);
String[] paths = path.split("\\.");
if (paths.length == 1) {
// (t0.content ->> 'title') = 'Some value'
sb.append("(").append(propName).append(" ->> '").append(path).append("')");
} else {
// (t0.content #>> '{path,inner}') = 'Some value'
sb.append("(").append(propName).append(" #>> '{");
for (int i = 0; i < paths.length; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(paths[i]);
}
sb.append("}')");
}
request.append(castType(sb.toString(), value));
request.append(operator.bind());
}
/**
* Postgres CAST the type if necessary as text values always returned from the json operators used.
*/
private String castType(String expression, Object value) {
if (value == null) {
// for exists and not-exists expressions
return expression;
}
// Postgres cast of returned text value
if (isIntegerType(value)) {
return expression+"::INTEGER";
}
if (isNumberType(value)) {
return expression+"::DECIMAL";
}
if (isBooleanType(value)) {
return expression+"::BOOLEAN";
}
return expression;
}
private boolean isBooleanType(Object value) {
return (value instanceof Boolean);
}
private boolean isIntegerType(Object value) {
return (value instanceof Integer) || (value instanceof Long);
}
private boolean isNumberType(Object value) {
return (value instanceof Number);
}
}
@@ -59,7 +59,7 @@ public final class RelationalQueryRequest {
*/
public void endTransIfRequired() {
if (createdTransaction) {
trans.endQueryOnly();
trans.commit();
}
}
@@ -112,4 +112,8 @@ public interface SpiOrmQueryRequest<T> {
*/
String getDBLikeClause();
/**
* Mark the underlying transaction as not being query only.
*/
void markNotQueryOnly();
}
@@ -51,8 +51,8 @@ public class BeanCascadeInfo {
/**
* Set to true if delete should cascade.
*/
public void setDelete(boolean isDelete) {
this.delete = isDelete;
public void setDelete(boolean delete) {
this.delete = delete;
}
/**
* Return true if save should cascade.
@@ -60,5 +60,13 @@ public class BeanCascadeInfo {
public boolean isSave() {
return save;
}
/**
* Set cascade save and delete settings.
*/
public void setSaveDelete(boolean save, boolean delete) {
this.save = save;
this.delete = delete;
}
}
@@ -1,6 +1,7 @@
package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.OrderBy;
import com.avaje.ebean.Query;
import com.avaje.ebean.SqlUpdate;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.ValuePair;
@@ -147,11 +148,19 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final String baseTableVersionsBetween;
private final boolean historySupport;
private final String draftTable;
/**
* Set to true if read auditing is on for this bean type.
*/
private final boolean readAuditing;
private final boolean draftable;
private final boolean draftableElement;
private final BeanProperty draftDirty;
/**
* Map of BeanProperty Linked so as to preserve order.
*/
@@ -319,7 +328,8 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final boolean updateChangesOnly;
private final boolean cacheSharableBeans;
private final BeanDescriptorDraftHelp<T> draftHelp;
private final BeanDescriptorCacheHelp<T> cacheHelp;
private final BeanDescriptorJsonHelp<T> jsonHelp;
@@ -372,7 +382,10 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.compoundUniqueConstraints = deploy.getCompoundUniqueConstraints();
this.readAuditing = deploy.isReadAuditing();
this.draftable = deploy.isDraftable();
this.draftableElement = deploy.isDraftableElement();
this.historySupport = deploy.isHistorySupport();
this.draftTable = deploy.getDraftTable();
this.baseTable = InternString.intern(deploy.getBaseTable());
this.baseTableAsOf = deploy.getBaseTableAsOf();
this.baseTableVersionsBetween = deploy.getBaseTableVersionsBetween();
@@ -383,6 +396,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.idProperty = listHelper.getId();
this.versionProperty = listHelper.getVersionProperty();
this.draftDirty = listHelper.getDraftDirty();
this.propMap = listHelper.getPropertyMap();
this.propertiesTransient = listHelper.getTransients();
this.propertiesNonTransient = listHelper.getNonTransients();
@@ -413,6 +427,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly();
this.cacheHelp = new BeanDescriptorCacheHelp<T>(this, owner.getCacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
this.jsonHelp = new BeanDescriptorJsonHelp<T>(this);
this.draftHelp = new BeanDescriptorDraftHelp<T>(this);
// Check if there are no cascade save associated beans ( subject to change
// in initialiseOther()). Note that if we are in an inheritance hierarchy
@@ -525,12 +540,15 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
* </p>
* @param withHistoryTables map populated if @History is supported on this entity bean
*/
public void initialiseId(Map<String, String> withHistoryTables) {
public void initialiseId(Map<String, String> withHistoryTables, Map<String,String> draftTables) {
if (logger.isTraceEnabled()) {
logger.trace("BeanDescriptor initialise " + fullName);
}
if (draftable) {
draftTables.put(baseTable, draftTable);
}
if (historySupport) {
// add mapping (used to swap out baseTable for asOf queries)
withHistoryTables.put(baseTable, baseTableAsOf);
@@ -558,16 +576,21 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
*
* @param asOfTableMap the map of base tables to associated 'with history' tables
* @param asOfViewSuffix the suffix added to the table name to derive the 'with history' view name
* @param draftTableMap the map of base tables to associated 'draft' tables.
*/
public void initialiseOther(Map<String, String> asOfTableMap, String asOfViewSuffix) {
public void initialiseOther(Map<String, String> asOfTableMap, String asOfViewSuffix, Map<String, String> draftTableMap) {
for (int i = 0; i < propertiesManyToMany.length; i++) {
// register associated draft table for M2M intersection
propertiesManyToMany[i].registerDraftIntersectionTable(draftTableMap);
}
if (historySupport) {
// history support on this bean so check all associated intersection tables
// and if they are not excluded register the associated 'with history' table
for (int i = 0; i < propertiesManyToMany.length; i++) {
// register associated history table for M2M intersection
if (!propertiesManyToMany[i].isExcludedFromHistory()) {
// this intersection table has history support so also register
// it into the asOfTableMap
TableJoin intersectionTableJoin = propertiesManyToMany[i].getIntersectionTableJoin();
String intersectionTableName = intersectionTableJoin.getTable();
asOfTableMap.put(intersectionTableName, intersectionTableName + asOfViewSuffix);
@@ -810,6 +833,17 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return inheritInfo == null || inheritInfo.isRoot();
}
public T publish(T draftBean, T liveBean) {
return draftHelp.publish(draftBean, liveBean);
}
/**
* Return the draft dirty boolean property or null if there is not one assigned to this bean type.
*/
public BeanProperty getDraftDirty() {
return draftDirty;
}
/**
* Set the bean caching on or off.
*/
@@ -831,7 +865,11 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
/**
* Return true if the persist request needs to notify the cache.
*/
public boolean isCacheNotify() {
public boolean isCacheNotify(boolean publish) {
if (draftable && !publish) {
// no caching when editing draft beans
return false;
}
return cacheHelp.isCacheNotify();
}
@@ -1856,12 +1894,20 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
*/
public String getBaseTable(SpiQuery.TemporalMode mode) {
switch (mode) {
case DRAFT: return draftTable;
case VERSIONS: return baseTableVersionsBetween;
case AS_OF: return baseTableAsOf;
default: return baseTable;
}
}
/**
* Return the associated draft table.
*/
public String getDraftTable() {
return draftTable;
}
/**
* Return true if read auditing is on this entity bean.
*/
@@ -1869,6 +1915,40 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return readAuditing;
}
/**
* Return true if this entity type is draftable.
*/
public boolean isDraftable() {
return draftable;
}
/**
* Return true if this entity type is a draftable element (child).
*/
public boolean isDraftableElement() {
return draftableElement;
}
/**
* If there is a @DraftDirty property set it's value on the bean.
*/
public void setDraftDirty(EntityBean entityBean, boolean value) {
if (draftDirty != null) {
// check to see if the dirty property has already
// been set and if so do not set the value
if (!entityBean._ebean_getIntercept().isChangedProperty(draftDirty.getPropertyIndex())) {
draftDirty.setValueIntercept(entityBean, value);
}
}
}
/**
* Optimise the draft query fetching any draftable element relationships.
*/
public void draftQueryOptimise(Query<T> query) {
draftHelp.draftQueryOptimise(query);
}
/**
* Return true if this entity bean has history support.
*/
@@ -0,0 +1,74 @@
package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.Query;
import com.avaje.ebean.bean.EntityBean;
/**
* Helper for BeanDescriptor that manages draft entity beans.
*
* @param <T> The entity bean type
*/
public final class BeanDescriptorDraftHelp<T> {
private final BeanDescriptor<T> desc;
public BeanDescriptorDraftHelp(BeanDescriptor<T> desc) {
this.desc = desc;
}
/**
*
*/
public void initialise() {
}
public T publish(T draftBean, T liveBean) {
if (liveBean == null) {
liveBean = (T)desc.createEntityBean();
}
EntityBean draft = (EntityBean)draftBean;
EntityBean live = (EntityBean)liveBean;
BeanProperty idProperty = desc.getIdProperty();
if (idProperty != null) {
idProperty.publish(draft, live);
}
BeanProperty[] props = desc.propertiesNonMany();
for (int i = 0; i < props.length; i++) {
props[i].publish(draft, live);
}
BeanPropertyAssocMany<?>[] many = desc.propertiesMany();
for (int i = 0; i < many.length; i++) {
if (many[i].getTargetDescriptor().isDraftable()) {
many[i].publishMany(draft, live);
}
}
return liveBean;
}
/**
* Fetch draftable element relationships.
*/
public void draftQueryOptimise(Query<T> query) {
BeanPropertyAssocOne<?>[] one = desc.propertiesOne();
for (int i = 0; i < one.length; i++) {
if (one[i].getTargetDescriptor().isDraftableElement()) {
query.fetch(one[i].getName());
}
}
BeanPropertyAssocMany<?>[] many = desc.propertiesMany();
for (int i = 0; i < many.length; i++) {
if (many[i].getTargetDescriptor().isDraftableElement()) {
query.fetch(many[i].getName());
}
}
}
}
@@ -5,6 +5,7 @@ import com.avaje.ebean.Model;
import com.avaje.ebean.RawSql;
import com.avaje.ebean.RawSqlBuilder;
import com.avaje.ebean.annotation.ConcurrencyMode;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebean.config.EncryptKey;
@@ -167,6 +168,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
*/
private final Map<String,String> asOfTableMap = new HashMap<String, String>();
/**
* Map of base tables to 'draft' tables.
*/
private final Map<String,String> draftTableMap = new HashMap<String, String>();
/**
* Create for a given database dbConfig.
*/
@@ -269,6 +275,13 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
return idBinderFactory.createIdBinder(idProperty);
}
/**
* Return the map of base tables to draft tables.
*/
public Map<String,String> getDraftTableMap() {
return draftTableMap;
}
/**
* Deploy returning the asOfTableMap (which is required by the SQL builders).
*/
@@ -396,7 +409,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
// first (as they are needed to initialise the
// associated properties in the second pass).
for (BeanDescriptor<?> d : descMap.values()) {
d.initialiseId(asOfTableMap);
d.initialiseId(asOfTableMap, draftTableMap);
}
// PASS 2:
@@ -411,7 +424,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
// also look for intersection tables with
// associated history support and register them
// into the asOfTableMap
d.initialiseOther(asOfTableMap, asOfViewSuffix);
d.initialiseOther(asOfTableMap, asOfViewSuffix, draftTableMap);
}
// create BeanManager for each non-embedded entity bean
@@ -898,7 +911,13 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
*/
private void checkMappedByOneToMany(DeployBeanInfo<?> info, DeployBeanPropertyAssocMany<?> prop) {
// get the bean descriptor that holds the mappedBy property
DeployBeanDescriptor<?> targetDesc = getTargetDescriptor(prop);
if (targetDesc.isDraftableElement()) {
// automatically turning on orphan removal and CascadeType.ALL
prop.setModifyListenMode(BeanCollection.ModifyListenMode.REMOVALS);
prop.getCascadeInfo().setSaveDelete(true, true);
}
if (prop.getMappedBy() == null) {
if (!findMappedBy(prop)) {
@@ -912,7 +931,6 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
String mappedBy = prop.getMappedBy();
// get the mappedBy property
DeployBeanDescriptor<?> targetDesc = getTargetDescriptor(prop);
DeployBeanProperty mappedProp = targetDesc.getBeanProperty(mappedBy);
if (mappedProp == null) {
@@ -948,6 +966,9 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
// get the bean descriptor that holds the mappedBy property
String mappedBy = prop.getMappedBy();
if (mappedBy == null) {
if (getTargetDescriptor(prop).isDraftable()) {
prop.setIntersectionDraftTable();
}
return;
}
@@ -995,6 +1016,10 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
DeployTableJoin inverseJoin = new DeployTableJoin();
mappedIntJoin.copyTo(inverseJoin, false, intTableName);
prop.setInverseJoin(inverseJoin);
if (targetDesc.isDraftable()) {
prop.setIntersectionDraftTable();
}
}
private <T> void setBeanControllerFinderListener(DeployBeanDescriptor<T> descriptor) {
@@ -84,7 +84,7 @@ public final class BeanMapHelp<T> implements BeanCollectionHelp<T> {
this.map = map;
}
public void addBean(EntityBean bean) {
public void addEntityBean(EntityBean bean) {
Object keyValue = beanProperty.getValue(bean);
map.put(keyValue, bean);
}
@@ -223,6 +223,10 @@ public class BeanProperty implements ElPropertyValue {
final boolean jsonDeserialize;
final boolean draftOnly;
final boolean draftDirty;
final boolean indexed;
final String indexName;
@@ -249,6 +253,8 @@ public class BeanProperty implements ElPropertyValue {
this.dbInsertable = deploy.isDbInsertable();
this.dbUpdatable = deploy.isDbUpdateable();
this.excludedFromHistory = deploy.isExcludedFromHistory();
this.draftDirty = deploy.isDraftDirty();
this.draftOnly = deploy.isDraftOnly();
this.secondaryTable = deploy.isSecondaryTable();
if (secondaryTable) {
@@ -333,6 +339,8 @@ public class BeanProperty implements ElPropertyValue {
this.formula = false;
this.excludedFromHistory = source.excludedFromHistory;
this.draftDirty = source.draftDirty;
this.draftOnly = source.draftOnly;
this.fetchEager = source.fetchEager;
this.unidirectionalShadow = source.unidirectionalShadow;
this.discriminator = source.discriminator;
@@ -493,7 +501,7 @@ public class BeanProperty implements ElPropertyValue {
if (formula) {
ctx.appendFormulaSelect(sqlFormulaSelect);
} else if (!isTransient) {
} else if (!isTransient && !ignoreDraftOnlyProperty(ctx.isDraftQuery())) {
if (secondaryTableJoin != null) {
String relativePrefix = ctx.getRelativePrefix(secondaryTableJoinPrefix);
@@ -592,6 +600,18 @@ public class BeanProperty implements ElPropertyValue {
return local;
}
/**
* Copy/set the property value from the draft bean to the live bean.
*/
public void publish(EntityBean draftBean, EntityBean liveBean) {
if (!version && !draftOnly) {
// set property value from draft to live
Object value = getValueIntercept(draftBean);
setValueIntercept(liveBean, value);
}
}
/**
* Set the value of the property without interception or
* PropertyChangeSupport.
@@ -600,10 +620,7 @@ public class BeanProperty implements ElPropertyValue {
try {
setter.set(bean, value);
} catch (Exception ex) {
String beanType = bean == null ? "null" : bean.getClass().getName();
String msg = "set " + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType
+ "] threw error";
throw new RuntimeException(msg, ex);
throw new RuntimeException(setterErrorMsg(bean, value, "set "), ex);
}
}
@@ -614,13 +631,18 @@ public class BeanProperty implements ElPropertyValue {
try {
setter.setIntercept(bean, value);
} catch (Exception ex) {
String beanType = bean == null ? "null" : bean.getClass().getName();
String msg = "setIntercept " + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType
+ "] threw error";
throw new RuntimeException(msg, ex);
throw new RuntimeException(setterErrorMsg(bean, value, "setIntercept "), ex);
}
}
/**
* Return an error message when calling a setter.
*/
private String setterErrorMsg(EntityBean bean, Object value, String prefix) {
String beanType = bean == null ? "null" : bean.getClass().getName();
return prefix + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType + "] threw error";
}
public Object getCacheDataValue(EntityBean bean) {
return getValue(bean);
}
@@ -900,8 +922,16 @@ public class BeanProperty implements ElPropertyValue {
/**
* Return true if this property is loadable from a resultSet.
*/
public boolean isLoadProperty() {
return !isTransient || formula;
public boolean isLoadProperty(boolean draftQuery) {
return !ignoreDraftOnlyProperty(draftQuery) && (!isTransient || formula);
}
/**
* Return true if this is a draftOnly property on a non-asDraft query and as such this
* property should not be included in a sql query.
*/
protected boolean ignoreDraftOnlyProperty(boolean draftQuery) {
return draftOnly && !draftQuery;
}
/**
@@ -951,7 +981,7 @@ public class BeanProperty implements ElPropertyValue {
return lob;
}
private boolean isLobType(int type) {
public static boolean isLobType(int type) {
switch (type) {
case Types.CLOB:
return true;
@@ -1000,6 +1030,21 @@ public class BeanProperty implements ElPropertyValue {
return excludedFromHistory;
}
/**
* Return true if this property only exists on the draft table.
*/
public boolean isDraftOnly() {
return draftOnly;
}
/**
* Return true if this property is a boolean flag only on the draft table
* indicating that when the draft is different from the published row.
*/
public boolean isDraftDirty() {
return draftDirty;
}
/**
* Return true if this property should be included in an Insert.
*/
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +40,8 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
* Join for manyToMany intersection table.
*/
private final TableJoin intersectionJoin;
private final String intersectionPublishTable;
private final String intersectionDraftTable;
/**
* For ManyToMany this is the Inverse join used to build reference queries.
@@ -109,6 +112,13 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
this.mapKey = deploy.getMapKey();
this.fetchOrderBy = deploy.getFetchOrderBy();
this.intersectionJoin = deploy.createIntersectionTableJoin();
if (intersectionJoin != null) {
this.intersectionPublishTable = intersectionJoin.getTable();
this.intersectionDraftTable = deploy.getIntersectionDraftTable();
} else {
this.intersectionPublishTable = null;
this.intersectionDraftTable = null;
}
this.inverseJoin = deploy.createInverseTableJoin();
this.modifyListenMode = deploy.getModifyListenMode();
this.jsonHelp = new BeanPropertyAssocManyJsonHelp(this);
@@ -798,22 +808,39 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
return row;
}
public IntersectionRow buildManyToManyDeleteChildren(EntityBean parentBean) {
public IntersectionRow buildManyToManyDeleteChildren(EntityBean parentBean, boolean publish) {
IntersectionRow row = new IntersectionRow(intersectionJoin.getTable());
String tableName = publish ? intersectionPublishTable : intersectionDraftTable;
IntersectionRow row = new IntersectionRow(tableName);
buildExport(row, parentBean);
return row;
}
public IntersectionRow buildManyToManyMapBean(EntityBean parent, EntityBean other) {
IntersectionRow row = new IntersectionRow(intersectionJoin.getTable());
public IntersectionRow buildManyToManyMapBean(EntityBean parent, EntityBean other, boolean publish) {
String tableName = publish ? intersectionPublishTable : intersectionDraftTable;
IntersectionRow row = new IntersectionRow(tableName);
buildExport(row, parent);
buildImport(row, other);
return row;
}
/**
* Register the mapping of intersection table to associated draft table.
*/
public void registerDraftIntersectionTable(Map<String, String> draftTableMap) {
if (hasDraftIntersection()) {
draftTableMap.put(intersectionPublishTable, intersectionDraftTable);
}
}
/**
* Return true if the relationship is a ManyToMany with the intersection having an associated draft table.
*/
private boolean hasDraftIntersection() {
return intersectionDraftTable != null && !intersectionDraftTable.equals(intersectionPublishTable);
}
private void buildExport(IntersectionRow row, EntityBean parentBean) {
if (embeddedExportedProperties) {
@@ -876,4 +903,57 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
public void jsonRead(ReadJson readJson, EntityBean parentBean) throws IOException {
jsonHelp.jsonRead(readJson, parentBean);
}
public void publishMany(EntityBean draft, EntityBean live) {
// collections will not be null due to enhancement
BeanCollection<T> draftVal = (BeanCollection<T>)getValueIntercept(draft);
BeanCollection<T> liveVal = (BeanCollection<T>)getValueIntercept(live);
// Organise the existing live beans into map keyed by id
Map<Object, T> liveBeansAsMap = liveBeansAsMap(liveVal);
// publish from each draft to live bean creating new live beans as required
draftVal.size();
Collection<T> actualDetails = draftVal.getActualDetails();
for (T bean : actualDetails) {
Object id = targetDescriptor.getId((EntityBean) bean);
T liveBean = liveBeansAsMap.remove(id);
if (isManyToMany()) {
if (liveBean == null) {
// add new relationship (Map not allowed here)
liveVal.addBean(targetDescriptor.createReference(Boolean.FALSE, id));
}
} else {
// recursively publish the OneToMany child bean
T newLive = targetDescriptor.publish(bean, liveBean);
if (liveBean == null) {
// Map not allowed here
liveVal.addBean(newLive);
}
}
}
// anything remaining should be deleted (so remove from modify aware collection)
Collection<T> values = liveBeansAsMap.values();
for (T value : values) {
liveVal.removeBean(value);
}
}
private Map<Object,T> liveBeansAsMap(BeanCollection<?> liveVal) {
liveVal.size();
Collection<?> liveBeans = liveVal.getActualDetails();
Map<Object,T> liveMap = new LinkedHashMap<Object, T>();
for (Object liveBean : liveBeans) {
Object id = targetDescriptor.getId((EntityBean) liveBean);
liveMap.put(id, (T)liveBean);
}
return liveMap;
}
}
@@ -65,7 +65,7 @@ public class BeanPropertyAssocManyJsonHelp {
// read the entire array
break;
}
add.addBean(detailBean);
add.addEntityBean(detailBean);
if (parentBean != null && many.childMasterProperty != null) {
// bind detail bean back to master via mappedBy property
@@ -79,4 +79,9 @@ public interface DbReadContext {
* Return the query mode.
*/
SpiQuery.Mode getQueryMode();
/**
* Return true if the underlying query is a 'asDraft' query.
*/
boolean isDraftQuery();
}
@@ -120,4 +120,9 @@ public interface DbSqlContext {
*/
void appendHistorySysPeriod();
/**
* Return true if the query is a 'asDraft' query.
*/
boolean isDraftQuery();
}
@@ -118,10 +118,16 @@ public class DeployBeanDescriptor<T> {
private String baseTableVersionsBetween;
private String draftTable;
private boolean historySupport;
private boolean readAuditing;
private boolean draftable;
private boolean draftableElement;
private TableName baseTableFull;
private String[] properties;
@@ -201,6 +207,23 @@ public class DeployBeanDescriptor<T> {
return readAuditing;
}
public void setDraftable() {
draftable = true;
}
public boolean isDraftable() {
return draftable;
}
public void setDraftableElement() {
draftable = true;
draftableElement = true;
}
public boolean isDraftableElement() {
return draftableElement;
}
public boolean isScalaObject() {
Class<?>[] interfaces = beanType.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
@@ -485,6 +508,10 @@ public class DeployBeanDescriptor<T> {
postLoaders.add(postLoad);
}
public String getDraftTable() {
return draftTable;
}
/**
* Return the base table. Only properties mapped to the base table are by
* default persisted.
@@ -522,6 +549,7 @@ public class DeployBeanDescriptor<T> {
this.baseTable = baseTableFull == null ? null : baseTableFull.getQualifiedName();
this.baseTableAsOf = baseTable + asOfSuffix;
this.baseTableVersionsBetween = baseTable + versionsBetweenSuffix;
this.draftTable = (draftable) ? baseTable+"_draft" : baseTable;
}
public void sortProperties() {
@@ -8,6 +8,7 @@ import com.avaje.ebean.config.ScalarTypeConverter;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.config.dbplatform.DbEncryptFunction;
import com.avaje.ebeaninternal.server.core.InternString;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import com.avaje.ebeaninternal.server.properties.BeanPropertyGetter;
@@ -182,6 +183,9 @@ public class DeployBeanProperty {
private boolean excludedFromHistory;
private boolean draftOnly;
private boolean draftDirty;
public DeployBeanProperty(DeployBeanDescriptor<?> desc, Class<?> propertyType, ScalarType<?> scalarType, ScalarTypeConverter<?, ?> typeConverter) {
this.desc = desc;
this.propertyType = propertyType;
@@ -598,7 +602,7 @@ public class DeployBeanProperty {
*/
public void setDbType(int dbType) {
this.dbType = dbType;
this.lob = isLobType(dbType);
this.lob = BeanProperty.isLobType(dbType);
}
/**
@@ -609,22 +613,6 @@ public class DeployBeanProperty {
return lob;
}
private boolean isLobType(int type) {
switch (type) {
case Types.CLOB:
return true;
case Types.BLOB:
return true;
case Types.LONGVARBINARY:
return true;
case Types.LONGVARCHAR:
return true;
default:
return false;
}
}
public boolean isDbNumberType() {
return isNumericType(dbType);
}
@@ -849,4 +837,21 @@ public class DeployBeanProperty {
public void setExcludedFromHistory() {
this.excludedFromHistory = true;
}
public void setDraftOnly() {
this.draftOnly = true;
}
public boolean isDraftOnly() {
return draftOnly;
}
public void setDraftDirty() {
this.draftOnly = true;
this.draftDirty = true;
}
public boolean isDraftDirty() {
return draftDirty;
}
}
@@ -9,6 +9,11 @@ import com.avaje.ebeaninternal.server.deploy.TableJoin;
*/
public class DeployBeanPropertyAssocMany<T> extends DeployBeanPropertyAssoc<T> {
/**
* The type of the many, set, list or map.
*/
final ManyType manyType;
ModifyListenMode modifyListenMode = ModifyListenMode.NONE;
/**
@@ -35,12 +40,9 @@ public class DeployBeanPropertyAssocMany<T> extends DeployBeanPropertyAssoc<T> {
String mapKey;
/**
* The type of the many, set, list or map.
*/
final ManyType manyType;
String intersectionDraftTable;
/**
/**
* Create this property.
*/
public DeployBeanPropertyAssocMany(DeployBeanDescriptor<?> desc, Class<T> targetType, ManyType manyType) {
@@ -191,4 +193,17 @@ public class DeployBeanPropertyAssocMany<T> extends DeployBeanPropertyAssoc<T> {
}
}
/**
* Return a draft table for intersection between 2 @Draftable entities.
*/
public String getIntersectionDraftTable() {
return (intersectionDraftTable != null) ? intersectionDraftTable : intersectionJoin.getTable();
}
/**
* ManyToMany between 2 @Draftable entities to also need draft intersection table.
*/
public void setIntersectionDraftTable() {
this.intersectionDraftTable = intersectionJoin.getTable()+"_draft";
}
}
@@ -28,6 +28,8 @@ public class DeployBeanPropertyLists {
private BeanProperty versionProperty;
private BeanProperty draftDirty;
private final BeanDescriptor<?> desc;
private final LinkedHashMap<String, BeanProperty> propertyMap;
@@ -73,7 +75,31 @@ public class DeployBeanPropertyLists {
this.propertyMap = new LinkedHashMap<String, BeanProperty>();
// see if there is a discriminator property we should add
String discriminatorColumn = null;
BeanProperty discProperty = null;
InheritInfo inheritInfo = deploy.getInheritInfo();
if (inheritInfo != null) {
// Create a BeanProperty for the discriminator column to support
// using RawSql queries with inheritance
discriminatorColumn = inheritInfo.getDiscriminatorColumn();
DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, new ScalarTypeString(), null);
discDeployProp.setDiscriminator();
discDeployProp.setName(discriminatorColumn);
discDeployProp.setDbColumn(discriminatorColumn);
// only register it in the propertyMap. This might not be used if
// an explicit property is mapped to the discriminator on the bean
discProperty = new BeanProperty(desc, discDeployProp);
}
for (DeployBeanProperty prop : deploy.propertiesAll()) {
if (discriminatorColumn != null && discriminatorColumn.equals(prop.getDbColumn())) {
// we have an explicit property mapped to the discriminator column
prop.setDiscriminator();
discProperty = null;
}
BeanProperty beanProp = createBeanProperty(owner, prop);
propertyMap.put(beanProp.getName(), beanProp);
}
@@ -84,21 +110,12 @@ public class DeployBeanPropertyLists {
allocateToList(prop);
}
InheritInfo inheritInfo = deploy.getInheritInfo();
if (inheritInfo != null) {
// Create a BeanProperty for the discriminator column to support
// using RawSql queries with inheritance
String discriminatorColumn = inheritInfo.getDiscriminatorColumn();
DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, new ScalarTypeString(), null);
discDeployProp.setDiscriminator();
discDeployProp.setName(discriminatorColumn);
discDeployProp.setDbColumn(discriminatorColumn);
// create the discriminator BeanProperty and only register it in the propertyMap
BeanProperty dprop = new BeanProperty(desc, discDeployProp);
propertyMap.put(dprop.getName(), dprop);
if (discProperty != null) {
// put the discriminator property into the property map only
// (after the real properties have been organised into their lists)
propertyMap.put(discProperty.getName(), discProperty);
}
List<DeployTableJoin> deployTableJoins = deploy.getTableJoins();
tableJoins = new TableJoin[deployTableJoins.size()];
for (int i = 0; i < deployTableJoins.size(); i++) {
@@ -160,6 +177,8 @@ public class DeployBeanPropertyLists {
logger.warn("Multiple @Version properties - property " + prop.getFullBeanName()
+ " not treated as a version property");
}
} else if (prop.isDraftDirty()) {
draftDirty = prop;
}
if (prop instanceof BeanPropertyCompound) {
baseCompound.add((BeanPropertyCompound) prop);
@@ -270,6 +289,10 @@ public class DeployBeanPropertyLists {
return getMany2Many();
}
public BeanProperty getDraftDirty() {
return draftDirty;
}
/**
* Mode used to determine which BeanPropertyAssoc to include.
*/
@@ -9,6 +9,8 @@ import javax.persistence.UniqueConstraint;
import com.avaje.ebean.annotation.CacheStrategy;
import com.avaje.ebean.annotation.CacheTuning;
import com.avaje.ebean.annotation.Draftable;
import com.avaje.ebean.annotation.DraftableElement;
import com.avaje.ebean.annotation.EntityConcurrencyMode;
import com.avaje.ebean.annotation.History;
import com.avaje.ebean.annotation.Index;
@@ -98,6 +100,16 @@ public class AnnotationClass extends AnnotationParser {
}
}
Draftable draftable = cls.getAnnotation(Draftable.class);
if (draftable != null) {
descriptor.setDraftable();
}
DraftableElement draftableElement = cls.getAnnotation(DraftableElement.class);
if (draftableElement != null) {
descriptor.setDraftableElement();
}
ReadAudit readAudit = cls.getAnnotation(ReadAudit.class);
if (readAudit != null) {
descriptor.setReadAuditing();
@@ -76,6 +76,8 @@ public class AnnotationFields extends AnnotationParser {
*/
private void readAssocOne(DeployBeanProperty prop) {
readJsonAnnotations(prop);
Id id = get(prop, Id.class);
if (id != null) {
prop.setId();
@@ -117,25 +119,7 @@ public class AnnotationFields extends AnnotationParser {
readColumn(column, prop);
}
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(!jsonIgnore.value());
prop.setJsonDeserialize(!jsonIgnore.value());
}
}
Expose expose = get(prop, Expose.class);
if (expose != null) {
prop.setJsonSerialize(expose.serialize());
prop.setJsonDeserialize(expose.deserialize());
}
JsonIgnore jsonIgnore = get(prop, JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(jsonIgnore.serialize());
prop.setJsonDeserialize(jsonIgnore.deserialize());
}
readJsonAnnotations(prop);
if (prop.getDbColumn() == null) {
// No @Column annotation or @Column.name() not set
@@ -165,6 +149,14 @@ public class AnnotationFields extends AnnotationParser {
util.setLobType(prop);
}
if (get(prop, DraftOnly.class) != null) {
prop.setDraftOnly();
}
if (get(prop, DraftDirty.class) != null) {
prop.setDraftDirty();
}
DbJson dbJson = get(prop, DbJson.class);
if (dbJson != null) {
util.setDbJsonType(prop, dbJson);
@@ -291,6 +283,28 @@ public class AnnotationFields extends AnnotationParser {
}
}
private void readJsonAnnotations(DeployBeanProperty prop) {
if (jacksonAnnotationsPresent) {
com.fasterxml.jackson.annotation.JsonIgnore jsonIgnore = get(prop, com.fasterxml.jackson.annotation.JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(!jsonIgnore.value());
prop.setJsonDeserialize(!jsonIgnore.value());
}
}
Expose expose = get(prop, Expose.class);
if (expose != null) {
prop.setJsonSerialize(expose.serialize());
prop.setJsonDeserialize(expose.deserialize());
}
JsonIgnore jsonIgnore = get(prop, JsonIgnore.class);
if (jsonIgnore != null) {
prop.setJsonSerialize(jsonIgnore.serialize());
prop.setJsonDeserialize(jsonIgnore.deserialize());
}
}
private boolean hasRelationshipItem(DeployBeanProperty prop) {
return get(prop, OneToMany.class) != null ||
get(prop, ManyToOne.class) != null ||
@@ -111,8 +111,8 @@ public class DeployUtil {
}
ScalarType<?> scalarType = typeManager.getScalarType(enumType);
if (scalarType == null) {
// see if it has a Mapping in avaje.properties
scalarType = typeManager.createEnumScalarType(enumType);
// look for @DbEnumValue or @EnumValue annotations etc
scalarType = typeManager.createEnumScalarType((Class<? extends Enum<?>>)enumType);
if (scalarType == null) {
// use JPA normal Enum type (without mapping)
EnumType type = enumerated != null ? enumerated.value() : null;
@@ -211,7 +211,7 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio
// exclude the zero values typically to weed out
// primitive int and long that initialise to 0
if (includeZeros || !isZero(value)) {
list.add(new SimpleExpression(propName, SimpleExpression.Op.EQ, value));
list.add(new SimpleExpression(propName, Op.EQ, value));
}
}
}
@@ -1,9 +1,5 @@
package com.avaje.ebeaninternal.server.expression;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.avaje.ebean.ExampleExpression;
import com.avaje.ebean.Expression;
import com.avaje.ebean.ExpressionFactory;
@@ -15,6 +11,10 @@ import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.api.SpiExpressionFactory;
import com.avaje.ebeaninternal.api.SpiQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Default Expression factory for creating standard expressions.
*/
@@ -36,6 +36,42 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return "sql";
}
public Expression jsonExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.EXISTS, null);
}
public Expression jsonNotExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.NOT_EXISTS, null);
}
public Expression jsonEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.EQ, value);
}
public Expression jsonNotEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.NOT_EQ, value);
}
public Expression jsonGreaterThan(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.GT, value);
}
public Expression jsonGreaterOrEqual(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.GT_EQ, value);
}
public Expression jsonLessThan(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.LT, value);
}
public Expression jsonLessOrEqualTo(String propertyName, String path, Object value) {
return new JsonPathExpression(propertyName, path, Op.LT_EQ, value);
}
public Expression jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue) {
return new JsonPathExpression(propertyName, path, lowerValue, upperValue);
}
/**
* Equal To - property equal to the given value.
*/
@@ -43,7 +79,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.EQ, value);
return new SimpleExpression(propertyName, Op.EQ, value);
}
/**
@@ -53,7 +89,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNotNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.NOT_EQ, value);
return new SimpleExpression(propertyName, Op.NOT_EQ, value);
}
/**
@@ -88,7 +124,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression gt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT, value);
return new SimpleExpression(propertyName, Op.GT, value);
}
/**
@@ -97,7 +133,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression ge(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT_EQ, value);
return new SimpleExpression(propertyName, Op.GT_EQ, value);
}
/**
@@ -105,7 +141,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression lt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT, value);
return new SimpleExpression(propertyName, Op.LT, value);
}
/**
@@ -113,7 +149,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression le(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT_EQ, value);
return new SimpleExpression(propertyName, Op.LT_EQ, value);
}
/**
@@ -0,0 +1,96 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
/**
* Generally speaking tests the value at a given path in the JSON document.
* <p>
* Supports the usual operators (equal to, greater than etc).
* </p>
* <p>
* The value passed in is expected to be a valid JSON type so string, number, boolean.
* </p>
*/
class JsonPathExpression extends AbstractExpression {
/**
* The path in the JSON document in dot notation form.
*/
protected final String path;
/**
* The expression operator.
*/
protected final Op operator;
/**
* The bind value used to compare against the document path value.
*/
protected final Object value;
/**
* For Between this is the upper bind value.
*/
protected final Object upperValue;
/**
* Construct for Operator (not BETWEEN though).
*/
JsonPathExpression(String propertyName, String path, Op operator, Object value) {
super(propertyName);
this.path = path;
this.operator = operator;
this.value = value;
this.upperValue = null;
}
/**
* Construct for BETWEEN expression.
*/
JsonPathExpression(String propertyName, String path, Object value, Object upperValue) {
super(propertyName);
this.path = path;
this.operator = Op.BETWEEN;
this.value = value;
this.upperValue = upperValue;
}
@Override
public void queryAutoTuneHash(HashQueryPlanBuilder builder) {
builder.add(JsonPathExpression.class).add(propName).add(path).add(operator);
}
@Override
public void queryPlanHash(BeanQueryRequest<?> request, HashQueryPlanBuilder builder) {
queryAutoTuneHash(builder);
}
@Override
public int queryBindHash() {
int hc = (value == null) ? 0 : value.hashCode();
hc = (upperValue == null) ? hc : hc * 31 + upperValue.hashCode();
return hc;
}
@Override
public void addSql(SpiExpressionRequest request) {
// Use DB specific expression handling (Postgres and Oracle supported)
request.getJsonHander().addSql(request, propName, path, operator, value);
}
@Override
public void addBindValues(SpiExpressionRequest request) {
if (value != null) {
// value is null for EXISTS/NOT EXISTS
request.addBindValue(value);
}
if (upperValue != null) {
// upperValue only for BETWEEN operator
request.addBindValue(upperValue);
}
}
}
@@ -313,6 +313,78 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
return exprList.findUnique();
}
/**
* Path exists - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonExists(String propertyName, String path) {
return exprList.jsonExists(propertyName, path);
}
/**
* Path does not exist - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotExists(String propertyName, String path){
return exprList.jsonNotExists(propertyName, path);
}
/**
* Equal to - for the value at the given path in the JSON document.
*/
@Override
public ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value){
return exprList.jsonEqualTo(propertyName, path, value);
}
/**
* Not Equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object val){
return exprList.jsonNotEqualTo(propertyName, path, val);
}
/**
* Greater than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val){
return exprList.jsonGreaterThan(propertyName, path, val);
}
/**
* Greater than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val){
return exprList.jsonGreaterOrEqual(propertyName, path, val);
}
/**
* Less than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessThan(String propertyName, String path, Object val){
return exprList.jsonLessThan(propertyName, path, val);
}
/**
* Less than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val){
return exprList.jsonLessOrEqualTo(propertyName, path, val);
}
/**
* Between - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonBetween(String propertyName, String path, Object lowerValue, Object upperValue){
return exprList.jsonBetween(propertyName, path, lowerValue, upperValue);
}
@Override
public ExpressionList<T> ge(String propertyName, Object value) {
return exprList.ge(propertyName, value);
@@ -0,0 +1,67 @@
package com.avaje.ebeaninternal.server.expression;
/**
* Simple operators - equals, greater than, less than etc.
*/
public enum Op {
/**
* Exists (JSON).
*/
EXISTS(" is not null "),
/**
* Not Exists (JSON).
*/
NOT_EXISTS(" is null "),
/**
* Between (JSON).
*/
BETWEEN(" between ? and ? "),
/**
* Equal to
*/
EQ(" = ? "),
/**
* Not equal to.
*/
NOT_EQ(" <> ? "),
/**
*
* Less than.
*/
LT(" < ? "),
/**
* Less than or equal to.
*/
LT_EQ(" <= ? "),
/**
* Greater than.
*/
GT(" > ? "),
/**
* Greater than or equal to.
*/
GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
/**
* Return the bind expression include JDBC ? bind placeholder.
*/
public String bind() {
return exp;
}
}
@@ -10,20 +10,6 @@ public class SimpleExpression extends AbstractExpression {
private static final long serialVersionUID = -382881395755603790L;
enum Op {
EQ(" = ? "), NOT_EQ(" <> ? "), LT(" < ? "), LT_EQ(" <= ? "), GT(" > ? "), GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
public String bind() {
return exp;
}
}
private final Op type;
private final Object value;
@@ -64,12 +64,16 @@ public final class ShutdownManager {
/**
* Deregister the Shutdown hook.
* <p>
* In calling this method it is expected that application code will invoke
* the shutdown() method.
* </p>
* <p>
* For running in a Servlet Container a redeploy will cause a shutdown, and
* for that case we need to make sure the shutdown hook is deregistered.
* </p>
*/
protected static void deregisterShutdownHook() {
public static void deregisterShutdownHook() {
synchronized (servers) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
@@ -87,7 +91,10 @@ public final class ShutdownManager {
protected static void registerShutdownHook() {
synchronized (servers) {
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
String value = System.getProperty("ebean.registerShutdownHook");
if (value == null || !value.trim().equalsIgnoreCase("false")) {
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
} catch (IllegalStateException ex) {
if (!ex.getMessage().equals("Shutdown in progress")) {
throw ex;
@@ -54,10 +54,12 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex
query.asOf(parent.getAsOf());
query.setParentNode(objectGraphNode);
query.setLazyLoadProperty(lazyLoadProperty);
if (parent.isAsDraft()) {
query.asDraft();
}
if (parent.isDisableReadAudit()) {
query.setDisableReadAuditing();
}
if (queryProps != null) {
queryProps.configureBeanQuery(query);
}

Some files were not shown because too many files have changed in this diff Show More