Compare commits

...
Author SHA1 Message Date
Robin Bygrave 9cb43bd5dd [maven-release-plugin] prepare release avaje-ebeanorm-6.15.2 2016-01-13 16:13:30 +13:00
Robin Bygrave 9665de2dbe #502 - jsonContext.toJson(o, generator, path) can not apply path on @Transient field that is an entity bean 2016-01-13 13:29:06 +13:00
Robin Bygrave e2666f0c4c No effective change - turn off autotune profiling/tuning flags for test running 2016-01-13 12:38:04 +13:00
Robin Bygrave 89c83c90d1 No effective change - add test to show findRowCount with joins 2016-01-13 12:35:52 +13:00
Robin Bygrave cdd25351a6 No effective change - test code static imports 2016-01-13 11:40:23 +13:00
Robin Bygrave 17e0413837 No effective change - code format 2016-01-13 11:38:53 +13:00
Robin Bygrave b04419d9c7 #527 - @OrderBy on child-of-child property 2016-01-13 11:37:38 +13:00
Robin Bygrave 0453892ebd #526 - findPagedList with @EmbeddedId broken - automatically adds orderby t0.null 2016-01-13 10:43:56 +13:00
Robin Bygrave 55493c5b7d #524 - ExplicitJdbcTransaction leaves dangling “begin” - comment update 2016-01-13 00:17:22 +13:00
Robin Bygrave eb2f7cd852 #525 - Automatic autotune data 2016-01-13 00:08:00 +13:00
Robin Bygrave f44eb234c8 #525 - Automatic autotune data - initial wip 2016-01-12 11:45:24 +13:00
Robin Bygrave 404b57ab21 #524 - ExplicitJdbcTransaction leaves dangling “begin” - expected fix 2016-01-12 08:37:12 +13:00
Robin Bygrave a54f09ca4b #524 - ExplicitJdbcTransaction leaves dangling “begin” - expected fix 2016-01-11 14:04:14 +13:00
Robin Bygrave a3bb909308 [maven-release-plugin] prepare for next development iteration 2016-01-08 00:22:06 +13:00
Robin Bygrave 7eb443859e [maven-release-plugin] prepare release avaje-ebeanorm-6.15.1 2016-01-08 00:21:44 +13:00
Robin Bygrave 8b39788ae3 Bump pom to 6.15.1-SNAPSHOT 2016-01-08 00:20:27 +13:00
Robin Bygrave 1ec132b7dd #508 - Upgrading to 6.13.5 causes "No suitable driver found for jdbc:mysql" error 2016-01-08 00:18:20 +13:00
Robin Bygrave 343a975073 #520 - SPI - Deprecate SpiEbeanPlugin in favor of SpiServerPlugin. - Deprecate SpiEbeanPlugin 2016-01-07 23:24:57 +13:00
Robin Bygrave 944f7bd086 #520 - SPI - Deprecate SpiEbeanPlugin in favor of SpiServerPlugin. - Add ServiceLoading for SpiServerPlugin 2016-01-07 16:45:24 +13:00
Robin Bygrave 3639c2a540 #518 - Refactor: Remove getDdlGenerator() from SpiEbeanServer. 2016-01-07 14:27:27 +13:00
Robin Bygrave a9cc8ed702 #517 - Refactor DdlGenerator such that it is not an SpiEbeanPlugin 2016-01-07 14:22:37 +13:00
Robin Bygrave 0351a15067 #516 - DdlParser for seed and init sql is not ignoring sql comment lines (starting with --) 2016-01-07 14:03:17 +13:00
Robin Bygrave c7300a1707 #246 - ENH: Add ebean.ddl.initsql=.... to support executing a sql script prior to running the create all DDL - was - When the scripts generates the evolution, it should generate the create schema too. 2016-01-07 13:12:43 +13:00
Robin Bygrave c48008c4b2 #515 - ENH: Add ebean.ddl.seedSql=xxx.sql ... such that a sql script will execute to insert seed data typically for testing 2016-01-07 12:48:55 +13:00
Robin Bygrave e5d8804591 #514 - ENH: Add ebean.ddl.createOnly=true ... such that all the drop table statements are skipped (for running tests against H2 usually) 2016-01-07 10:06:35 +13:00
Robin Bygrave f6dc294bc2 No effective change - update public project url in pom 2016-01-05 21:38:25 +13:00
Robin Bygrave 692baf8bb6 [maven-release-plugin] prepare for next development iteration 2016-01-05 21:28:03 +13:00
Robin Bygrave e1c00f3856 [maven-release-plugin] prepare release avaje-ebeanorm-6.14.1 2016-01-05 21:27:41 +13:00
Robin Bygrave cf173d0695 Bump pom to 6.14.1-SNAPSHOT 2016-01-05 21:26:20 +13:00
Robin Bygrave a3798c6ca3 #506 - Changes to DB Migration, support run on startup and external version numbering - suffix/subdirectory refactor 2016-01-05 21:25:07 +13:00
Robin Bygrave 7bcf1da823 #506 - Changes to DB Migration, support run on startup and external version numbering - suffix/subdirectory refactor 2016-01-05 21:21:16 +13:00
Rob Bygrave 2ccd8dabeb Merge pull request #510 from apuckey/master
Added support for custom database functions that have multiple arguments
2016-01-05 10:25:19 +13:00
Robin Bygrave f9800dd59e #509 - ExpressList.notIn method does not work 2016-01-05 09:11:14 +13:00
Robin Bygrave fb3e8b9fc4 #511 - PagedList getTotalRowCount() includes soft deleted rows in its count 2016-01-05 07:37:17 +13:00
Antony Puckey 18be5989c7 Added support for custom database functions that have multiple arguments
seperated by commas inside brackets.
2015-12-29 11:06:22 +10:00
Robin Bygrave be91e3f3f7 #506 - Changes to DB Migration, support run on startup and external version numbering 2015-12-24 16:41:06 +13:00
Robin Bygrave 03a9aa3dec #507 - PropertiesWrapper support null enum values with getEnum() 2015-12-24 15:23:25 +13:00
Robin Bygrave a2a301beda No effective change - remove commented out lines 2015-12-23 09:13:31 +13:00
Robin Bygrave 695e4f653b No effective change - typo in comment 2015-12-23 07:38:58 +13:00
Robin Bygrave e84c75929c #505 - Remove unused method - EntityBean._ebean_createCopy(); 2015-12-22 17:04:51 +13:00
Robin Bygrave 7592178b6b No effective change - update tests to ensure query does not hit L2 cache 2015-12-22 17:02:35 +13:00
Robin Bygrave 70d65b734f #504 - @Draft treated as transient such that it doesn't invoke lazy loading in 4.8.1 enhancement agent 2015-12-22 16:48:42 +13:00
Robin Bygrave f9a3dfeda0 #503 - @DraftDirty should be marked as not null automatically 2015-12-22 07:52:05 +13:00
Robin Bygrave eb85931fe2 #500 - Change raw(String, Object[]) expression to varargs raw(String, Object...) 2015-12-16 08:11:32 +13:00
Robin Bygrave 1745334737 No effective change - add extra test 2015-12-16 08:06:12 +13:00
Robin Bygrave ea92ddfd89 [maven-release-plugin] prepare for next development iteration 2015-12-15 15:46:21 +13:00
Robin Bygrave 3400928798 [maven-release-plugin] prepare release avaje-ebeanorm-6.13.5 2015-12-15 15:45:55 +13:00
Robin Bygrave 4e665fd66f #499 - DDL issue for entity bean that is both @Draftable with @History - draft only columns included in history table 2015-12-15 15:44:22 +13:00
Robin Bygrave 09cd2c12e2 Add tests for @Draftable from example application 2015-12-15 15:25:19 +13:00
Robin Bygrave e3e45929f0 #498 - @Draftable - lazy loading after stateless update results in - EntityNotFoundException: Bean not found during lazy load or refresh 2015-12-15 15:15:20 +13:00
Robin Bygrave 2caeafaaa2 #498 - @Draftable - lazy loading after stateless update results in - EntityNotFoundException: Bean not found during lazy load or refresh 2015-12-15 14:38:39 +13:00
Robin Bygrave 5cd21de174 #496 - @EnumValue is being trimmed, may result in unexpected behaviour with CHAR columns for some database systems 2015-12-14 17:21:01 +13:00
Robin Bygrave 78df595861 [maven-release-plugin] prepare for next development iteration 2015-12-11 10:28:21 +13:00
Robin Bygrave 3c89a0537f [maven-release-plugin] prepare release avaje-ebeanorm-6.13.4 2015-12-11 10:28:01 +13:00
Robin Bygrave 282463c2e8 #247 - @AttributeOverride at class level not read - was: @AttributeOverride annotation does not seem to work (Play 2.2.0 - ebean 3.2.2) 2015-12-11 10:23:34 +13:00
Robin Bygrave 03bb7a4aaf #494 - ENH: Support findPagingList() ... that uses firstRow & maxRows (rather than pageIndex, pageSize) 2015-12-11 10:20:53 +13:00
Robin Bygrave e4fa031cd3 #494 - ENH: Support findPagingList() ... that uses firstRow & maxRows (rather than pageIndex, pageSize) 2015-12-10 14:52:27 +13:00
Robin Bygrave 00d227c598 #491 - RawSqlParser doesn't seem to understand new lines directly after the SELECT keyword 2015-12-09 17:24:58 +13:00
Robin Bygrave fb86cb91bc #492 - MS Sql Server DDL - Fix for drop constraint syntax (remove extra IF EXISTS) and ignore table and column comments 2015-12-09 16:59:51 +13:00
Robin Bygrave f11ee20b73 #365 - ENH: Add @DbComment with DDL generation of table and column comments 2015-12-09 16:56:29 +13:00
Robin Bygrave 3665c5bfaf No effective change - adjust test connection properties for sql server 2015-12-09 16:53:26 +13:00
Robin Bygrave 638446680c #365 - ENH: Add @DbComment with DDL generation of table and column comments 2015-12-09 16:52:58 +13:00
Robin Bygrave bd9bfa25be No effective change - rename test property from 'top' as that is ms sql server keyword 2015-12-09 16:49:35 +13:00
Robin Bygrave 753d17a8d2 [maven-release-plugin] prepare for next development iteration 2015-12-09 12:46:40 +13:00
Robin Bygrave f5cfce3f44 [maven-release-plugin] prepare release avaje-ebeanorm-6.13.3 2015-12-09 12:46:18 +13:00
Robin Bygrave 1c6363e85c #490 - Invalid SQL for generating ID from sequence on DB2 2015-12-09 12:44:27 +13:00
Robin Bygrave b8bcbd8af9 #489 - @Draftable - insert does not set 'draft' to true ... so subsequent update fails 2015-12-09 11:06:48 +13:00
Robin Bygrave d70768277b #488 - BeanPropertyAssocManyJsonHelp error when Jackson not in classPath 2015-12-09 11:00:21 +13:00
Robin Bygrave b39b4a2e8c #487 - DDL - @SoftDelete deleted column should have NOT NULL constraint in create table DDL 2015-12-09 10:27:04 +13:00
Robin Bygrave 93b2f4035c #485 - @SoftDelete with join to nullable relation (optional @ManyToOne for example) ... will incorrectly filter out row from result when FK value is null. 2015-12-09 10:12:25 +13:00
Robin Bygrave 31ee0c4582 #486 - Postgres DDL - Add "if exists" to alter table [if exists] drop constraint if exists .... 2015-12-09 10:06:03 +13:00
Robin Bygrave 6c2dad4851 [maven-release-plugin] prepare for next development iteration 2015-12-07 22:20:54 +13:00
Robin Bygrave ed190ede03 [maven-release-plugin] prepare release avaje-ebeanorm-6.13.2 2015-12-07 22:20:25 +13:00
Robin Bygrave f252b80c2d #476 - ENH: Enable ServiceConfig to use alternate ClassLoader - was ServiceConfig add setClassloader api --- part 3: Refactor with ClassLoadConfig 2015-12-07 22:13:27 +13:00
Robin Bygrave 2859ea657e #484 - JDBC4+ drivers do not need Class.forName() registration - removing that call 2015-12-07 21:02:00 +13:00
Robin Bygrave 664b1cbd75 #476 - ENH: Enable ServiceConfig to use alternate ClassLoader - was ServiceConfig add setClassloader api --- part 2 : Use current context classLoader 2015-12-07 17:41:56 +13:00
Robin Bygrave 8bc283fef4 #476 - ENH: Enable ServiceConfig to use alternate ClassLoader - was ServiceConfig add setClassloader api --- part 1 : protected methods 2015-12-07 17:37:59 +13:00
Robin Bygrave 7d85398139 #483 - ENH: Draftable - Add FK from live table back to draft table (for top level @Draftable tables) 2015-12-07 15:21:22 +13:00
Robin Bygrave f42be3d65a #482 - ENH: @Draftable - Save or update is not allowed on a 'live' bean - only draft beans ... add this check when @Draft property is used 2015-12-07 12:09:10 +13:00
Robin Bygrave ff8e5af4e5 #481 - ENH: @Draftable ... add support for transient @Draft boolean draft; property ... used to distinguish a 'live' instance from a 'draft' instance. 2015-12-07 11:11:22 +13:00
Robin Bygrave 526d67eeb4 No effective change - whitespace 2015-12-07 11:02:32 +13:00
Robin Bygrave 374fc8d702 #480 - @History trigger on @Draftable does not exclude the @DraftOnly and @DraftDirty properties - ERROR: column "dirty" of relation "link_history" does not exist Where: PL/pgSQL function link_history_version() 2015-12-07 09:44:34 +13:00
Robin Bygrave 9c6c59afab #479 - Insert of @Draftable with getGeneratedKeys ... PersistenceException executing insert 2015-12-07 09:21:26 +13:00
Robin Bygrave c3eae0d3a5 #477 - findUnique() should throw NonUniqueResultException and not PersistenceException 2015-12-04 11:12:45 +13:00
Robin Bygrave 25ec73364e [maven-release-plugin] prepare for next development iteration 2015-12-03 21:41:41 +13:00
Robin Bygrave c7818e7f23 [maven-release-plugin] prepare release avaje-ebeanorm-6.13.1 2015-12-03 21:41:20 +13:00
Robin Bygrave 8318bbc4ca #118 - Additional: Add query includeSoftDeletes() asDraft() to ExpressionList (for fluid style when added after predicates) 2015-12-03 17:42:26 +13:00
Robin Bygrave 337646a6ce No effective change - javadoc update 2015-12-03 16:47:19 +13:00
Robin Bygrave fa6ee93e5b Bump pom to 6.13.1-SNAPSHOT 2015-12-03 16:35:43 +13:00
Robin Bygrave 0fe691112f Merge branch 'softDelete1' into softDelete2 2015-12-03 00:33:47 +13:00
Robin Bygrave 6946ff07e0 [maven-release-plugin] prepare for next development iteration 2015-12-02 23:48:15 +13:00
Robin Bygrave 7c3d052da0 [maven-release-plugin] prepare release avaje-ebeanorm-6.12.3 2015-12-02 23:47:49 +13:00
Robin Bygrave aea7a7fd65 #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... 2015-12-02 23:46:26 +13:00
Robin Bygrave f39a684c8e #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... 2015-12-02 21:38:34 +13:00
Robin Bygrave 3cb5854786 #475 - Unique constraint name not unique when using @Draftable 2015-12-02 07:57:27 +13:00
Robin Bygrave e8192723cc [maven-release-plugin] prepare for next development iteration 2015-12-01 23:25:26 +13:00
Robin Bygrave 0ad52cf6a7 #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... - initial 2015-12-01 23:21:11 +13:00
173 changed files with 5940 additions and 1461 deletions
+4 -4
View File
@@ -9,11 +9,11 @@
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm</artifactId>
<version>6.12.2</version>
<version>6.15.2</version>
<packaging>jar</packaging>
<name>avaje-ebeanorm</name>
<url>http://www.avaje.org</url>
<url>http://ebean-orm.github.io/</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -152,7 +152,7 @@
<dependency>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm-agent</artifactId>
<version>4.7.1</version>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
@@ -219,7 +219,7 @@
<plugin>
<groupId>org.avaje.ebeanorm</groupId>
<artifactId>avaje-ebeanorm-mavenenhancer</artifactId>
<version>4.7.1</version>
<version>4.8.1</version>
<executions>
<!-- Not going to enhance Model bean -->
<execution>
@@ -45,7 +45,7 @@ final class DRawSqlColumnsParser {
String colInfo = sqlSelect.substring(start, pos++);
colInfo = colInfo.trim();
String[] split = colInfo.split(" ");
String[] split = colInfo.split("\\s(?=[^\\)]*(?:\\(|$))");
if (split.length > 1) {
ArrayList<String> tmp = new ArrayList<String>(split.length);
for (int i = 0; i < split.length; i++) {
@@ -82,11 +82,13 @@ final class DRawSqlColumnsParser {
private void nextComma() {
boolean inQuote = false;
int inbrackets = 0;
while (pos < end) {
char c = sqlSelect.charAt(pos);
if (c == '\'') {
inQuote = !inQuote;
} else if (!inQuote && c == ',') {
if (c == '\'') inQuote = !inQuote;
else if (c == '(') inbrackets++;
else if (c == ')') inbrackets--;
else if (!inQuote && inbrackets == 0 && c == ',') {
return;
}
pos++;
@@ -46,6 +46,7 @@ class DRawSqlParser {
private DRawSqlParser(String sqlString) {
sqlString = sqlString.trim();
sqlString = sqlString.replace('\n',' ');
this.sql = sqlString;
this.hasPlaceHolders = findAndRemovePlaceHolders();
this.textParser = new SimpleTextParser(sqlString);
@@ -120,8 +121,7 @@ class DRawSqlParser {
// trim of distinct keyword
String distinct = preWhereExprSql.substring(0, 9);
if (!distinct.equalsIgnoreCase("distinct ")) {
throw new RuntimeException("Expecting [" + preWhereExprSql
+ "] to start with \"select distinct\"");
throw new RuntimeException("Expecting [" + preWhereExprSql + "] to start with \"select distinct\"");
}
preWhereExprSql = preWhereExprSql.substring(9);
}
+18
View File
@@ -704,6 +704,10 @@ public final class Ebean {
* you automatically.
* </p>
* <p>
* If the bean is configured with <code>@SoftDelete</code> then this will perform a soft
* delete rather than a hard/permanent delete.
* </p>
* <p>
* If the Bean does not have a version property (or loaded version property) and
* the bean does not exist then this returns false indicating that nothing was
* deleted. Note that, if JDBC batch mode is used then this always returns true.
@@ -713,6 +717,13 @@ public final class Ebean {
return serverMgr.getDefaultServer().delete(bean);
}
/**
* Delete the bean in permanent fashion (will not use soft delete).
*/
public static boolean deletePermanent(Object bean) throws OptimisticLockException {
return serverMgr.getDefaultServer().deletePermanent(bean);
}
/**
* Delete the bean given its type and id.
*/
@@ -734,6 +745,13 @@ public final class Ebean {
return serverMgr.getDefaultServer().deleteAll(beans);
}
/**
* Delete permanent all the beans in the Collection (will not use soft delete).
*/
public static int deleteAllPermanent(Collection<?> beans) throws OptimisticLockException {
return serverMgr.getDefaultServer().deleteAllPermanent(beans);
}
/**
* Refresh the values of a bean.
* <p>
+61 -4
View File
@@ -9,6 +9,7 @@ import com.avaje.ebean.text.csv.CsvReader;
import com.avaje.ebean.text.json.JsonContext;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import java.util.Collection;
@@ -1019,7 +1020,7 @@ public interface EbeanServer {
SqlFutureList findFutureList(SqlQuery query, Transaction transaction);
/**
* Return a PagedList for this query.
* Return a PagedList for this query using pageIndex and pageSize.
* <p>
* The benefit of using this over just using the normal {@link Query#setFirstRow(int)} and
* {@link Query#setMaxRows(int)} is that it additionally wraps an optional call to
@@ -1041,6 +1042,38 @@ public interface EbeanServer {
*/
<T> PagedList<T> findPagedList(Query<T> query, Transaction transaction, int pageIndex, int pageSize);
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* </p>
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
* </p>
*
* <pre>{@code
*
* PagedList<Order> pagedList = Ebean.find(Order.class)
* .setFirstRow(50)
* .setMaxRows(20)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadRowCount();
*
* List<Order> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalRowCount();
*
* }</pre>
*
* @return The PagedList
*
* @see Query#findPagedList()
*/
<T> PagedList<T> findPagedList(Query<T> query, Transaction transaction);
/**
* Execute the query returning a set of entity beans.
* <p>
@@ -1091,14 +1124,17 @@ public interface EbeanServer {
<T> Map<?, T> findMap(Query<T> query, Transaction transaction);
/**
* Execute the query returning at most one entity bean. This will throw a
* PersistenceException if the query finds more than one result.
* Execute the query returning at most one entity bean or null (if no matching
* bean is found).
* <p>
* This will throw a NonUniqueResultException if the query finds more than one result.
* </p>
* <p>
* Generally you are able to use {@link Query#findUnique()} rather than
* explicitly calling this method. You could use this method if you wish to
* explicitly control the transaction used for the query.
* </p>
*
*
* @param <T>
* the type of entity bean to fetch.
* @param query
@@ -1106,6 +1142,7 @@ public interface EbeanServer {
* @param transaction
* the transaction to use (can be null).
* @return the list of fetched beans.
* @throws NonUniqueResultException if more than one result was found
*
* @see Query#findUnique()
*/
@@ -1273,6 +1310,26 @@ public interface EbeanServer {
*/
boolean delete(Object bean, Transaction transaction) throws OptimisticLockException;
/**
* Delete a bean permanently without soft delete.
*/
boolean deletePermanent(Object bean) throws OptimisticLockException;
/**
* Delete a bean permanently without soft delete using an explicit transaction.
*/
boolean deletePermanent(Object bean, Transaction transaction) throws OptimisticLockException;
/**
* Delete all the beans in the collection permanently without soft delete.
*/
int deleteAllPermanent(Collection<?> beans) throws OptimisticLockException;
/**
* Delete all the beans in the collection permanently without soft delete using an explicit transaction.
*/
int deleteAllPermanent(Collection<?> beans, Transaction transaction) throws OptimisticLockException;
/**
* Delete the bean given its type and id.
*/
@@ -3,6 +3,7 @@ package com.avaje.ebean;
import com.avaje.ebean.text.PathProperties;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Collection;
@@ -107,6 +108,16 @@ public interface ExpressionList<T> extends Serializable {
*/
Query<T> asOf(Timestamp asOf);
/**
* Execute the query against the draft set of tables.
*/
Query<T> asDraft();
/**
* Execute the query including soft deleted rows.
*/
Query<T> includeSoftDeletes();
/**
* Execute as a delete query deleting the 'root level' beans that match the predicates
* in the query.
@@ -183,8 +194,15 @@ public interface ExpressionList<T> extends Serializable {
<K> Map<K, T> findMap(String keyProperty, Class<K> keyType);
/**
* Execute the query returning a single bean.
*
* Execute the query returning a single bean or null (if no matching
* bean is found).
* <p>
* If more than 1 row is found for this query then a NonUniqueResultException is
* thrown.
* </p>
*
* @throws NonUniqueResultException if more than one result was found
*
* @see Query#findUnique()
*/
@Nullable
@@ -227,7 +245,7 @@ public interface ExpressionList<T> extends Serializable {
FutureList<T> findFutureList();
/**
* Return a PagedList for this query.
* Return a PagedList for this query using pageIndex and pageSize.
* <p>
* The benefit of using this over just using the normal {@link Query#setFirstRow(int)} and
* {@link Query#setMaxRows(int)} is that it additionally wraps an optional call to
@@ -247,6 +265,38 @@ public interface ExpressionList<T> extends Serializable {
*/
PagedList<T> findPagedList(int pageIndex, int pageSize);
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* </p>
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
* </p>
*
* <pre>{@code
*
* PagedList<Order> pagedList = Ebean.find(Order.class)
* .setFirstRow(50)
* .setMaxRows(20)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadRowCount();
*
* List<Order> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalRowCount();
*
* }</pre>
*
* @return The PagedList
*
* @see Query#findPagedList()
*/
PagedList<T> findPagedList();
/**
* Return versions of a @History entity bean.
* <p>
@@ -735,7 +785,7 @@ public interface ExpressionList<T> extends Serializable {
* qualified) will still be translated to their physical name.
* </p>
*/
ExpressionList<T> raw(String raw, Object[] values);
ExpressionList<T> raw(String raw, Object... values);
/**
* Add raw expression with no parameters.
+32 -1
View File
@@ -17,6 +17,33 @@ import java.util.concurrent.Future;
* limit the result set.
* </p>
*
*
* <h4>Example: typical use including total row count</h4>
* <pre>{@code
*
* // We want to find the first 50 new orders
* // ... so we don't really need setFirstRow(0)
*
* PagedList<Order> pagedList
* = ebeanServer.find(Order.class)
* .where().eq("status", Order.Status.NEW)
* .order().asc("id")
* .setFirstRow(0)
* .setMaxRows(50)
* .findPagedList();
*
* // Optional: initiate the loading of the total
* // row count in a background thread
* pagedList.loadRowCount();
*
* // fetch and return the list in the foreground thread
* List<Order> orders = pagedList.getList();
*
* // get the total row count (from the future)
* int totalRowCount = pagedList.getTotalRowCount();
*
* }</pre>
*
* <h4>Example: typical use including total row count</h4>
* <pre>{@code
*
@@ -156,11 +183,15 @@ public interface PagedList<T> {
/**
* Return the index position of this page. Zero based.
* <p>
* Note that if firstRows/maxRows is used rather than pageIndex/pageSize then
* this always returns 0.
* </p>
*/
int getPageIndex();
/**
* Return the page size used for this query.
* Return the page size used for this query. This is the same value as maxRows used by the query.
*/
int getPageSize();
+40 -2
View File
@@ -3,6 +3,7 @@ package com.avaje.ebean;
import com.avaje.ebean.text.PathProperties;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;
@@ -302,6 +303,11 @@ public interface Query<T> extends Serializable {
*/
Query<T> asDraft();
/**
* Execute the query including soft deleted rows.
*/
Query<T> includeSoftDeletes();
/**
* Cancel the query execution if supported by the underlying database and
* driver.
@@ -695,7 +701,7 @@ public interface Query<T> extends Serializable {
* Execute the query returning either a single bean or null (if no matching
* bean is found).
* <p>
* If more than 1 row is found for this query then a PersistenceException is
* If more than 1 row is found for this query then a NonUniqueResultException is
* thrown.
* </p>
* <p>
@@ -731,6 +737,8 @@ public interface Query<T> extends Serializable {
* List<OrderDetail> details = order.getDetails();
* ...
* }</pre>
*
* @throws NonUniqueResultException if more than one result was found
*/
@Nullable
T findUnique();
@@ -814,7 +822,7 @@ public interface Query<T> extends Serializable {
FutureList<T> findFutureList();
/**
* Return a PagedList for this query.
* Return a PagedList for this query using pageIndex and pageSize.
* <p>
* The benefit of using this over just using the normal {@link Query#setFirstRow(int)} and
* {@link Query#setMaxRows(int)} is that it additionally wraps an optional call to
@@ -859,6 +867,36 @@ public interface Query<T> extends Serializable {
*/
PagedList<T> findPagedList(int pageIndex, int pageSize);
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* </p>
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
* </p>
*
* <pre>{@code
*
* PagedList<Order> pagedList = Ebean.find(Order.class)
* .setFirstRow(50)
* .setMaxRows(20)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadRowCount();
*
* List<Order> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalRowCount();
*
* }</pre>
*
* @return The PagedList
*/
PagedList<T> findPagedList();
/**
* Set a named bind parameter. Named parameters have a colon to prefix the name.
*
@@ -9,6 +9,10 @@ import java.lang.annotation.Target;
* For a timestamp property that is set to the datetime when the entity is
* created/inserted.
* <p>
* This is effectively an alias for @WhenCreated which was added as it hints
* towards a better naming convention (WhenCreated, WhenModified).
* </p>
* <p>
* An alternative to using this annotation would be to use insertable=false,
* updateable=false with @Column and have the DB insert the current time
* (default value on the DB column is SYSTIME etc).
@@ -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;
/**
* A database table or column comment.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
public @interface DbComment {
/**
* The database table or column comment.
*/
String value();
}
@@ -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 boolean property on a @Draftable bean that indicates if the bean instance is a 'draft' or 'live' bean.
* The property is transient and has no underlying DB column.
* <p>
* For beans returned from an <code>asDraft()</code> query this property will be set to true.
* </p>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Draft {
}
@@ -0,0 +1,25 @@
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 a property on an entity bean used to control 'soft delete'
* (also known as 'logical delete').
* <p>
* The property should be of type boolean.
* </p>
* <pre>{@code
*
* @SoftDelete
* boolean deleted;
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SoftDelete {
}
@@ -8,6 +8,10 @@ import java.lang.annotation.Target;
/**
* For a timestamp property that is set to the datetime when the entity was last
* updated.
* <p>
* This is effectively an alias for @WhenModified which was added as it hints
* towards a better naming convention (WhenCreated, WhenModified).
* </p>
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@@ -11,7 +11,6 @@ import java.lang.annotation.Target;
* This is effectively an alias for @UpdatedTimestamp and added to hint
* towards a better naming convention (WhenCreated, WhenModified).
* </p>
* </p>
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@@ -73,16 +73,6 @@ public interface EntityBean extends Serializable {
*/
EntityBeanIntercept _ebean_intercept();
/**
* Create a copy of this entity bean.
* <p>
* This occurs when a bean is changed. The copy represents the bean as it was
* initially (oldValues) before any changes where made. This is used for
* optimistic concurrency control.
* </p>
*/
Object _ebean_createCopy();
/**
* Set the value of a field of an entity bean of this type.
* <p>
@@ -507,7 +507,7 @@ public final class EntityBeanIntercept implements Serializable {
setDirty(true);
}
private void setChangedProperty(int propertyIndex) {
public void setChangedProperty(int propertyIndex) {
if (changedProps == null) {
changedProps = new boolean[owner._ebean_getPropertyNames().length];
}
@@ -21,6 +21,8 @@ public class AutoTuneConfig {
private double profilingRate = 0.01;
private int profilingUpdateFrequency;
private int garbageCollectionWait = 100;
private boolean skipCollectionOnShutdown;
@@ -56,6 +58,20 @@ public class AutoTuneConfig {
this.profilingFile = profilingFile;
}
/**
* Return the frequency in seconds the profiling should be collected and automatically applied to the tuning.
*/
public int getProfilingUpdateFrequency() {
return profilingUpdateFrequency;
}
/**
* Set the frequency in seconds the profiling should be collected and automatically applied to the tuning.
*/
public void setProfilingUpdateFrequency(int profilingUpdateFrequency) {
this.profilingUpdateFrequency = profilingUpdateFrequency;
}
/**
* Return the mode used when autoTune has not been explicit defined on a
* query.
@@ -214,5 +230,6 @@ public class AutoTuneConfig {
profilingBase = p.getInt("autoTune.profilingBase", profilingBase);
profilingRate = p.getDouble("autoTune.profilingRate", profilingRate);
profilingFile = p.get("autoTune.profilingFile", profilingFile);
profilingUpdateFrequency = p.getInt("autoTune.profilingUpdateFrequency", profilingUpdateFrequency);
}
}
@@ -0,0 +1,143 @@
package com.avaje.ebean.config;
/**
* Helper to find classes taking into account the context class loader.
*/
public class ClassLoadConfig {
protected final ClassLoaderContext context;
/**
* Construct with the default classLoader search with context classLoader first.
*/
public ClassLoadConfig() {
this(null);
}
/**
* Specify the classLoader to use for class detection and new instance creation.
*/
public ClassLoadConfig(ClassLoader classLoader) {
this.context = new ClassLoaderContext(classLoader);
}
/**
* Return true if the Java.time types are available and should be supported.
*/
public boolean isJavaTimePresent() {
return isPresent("java.time.LocalDate");
}
/**
* Return true if the Joda types are available and should be supported.
*/
public boolean isJodaTimePresent() {
return isPresent("org.joda.time.LocalDateTime");
}
/**
* Return true if javax validation annotations like Size and NotNull are present.
*/
public boolean isJavaxValidationAnnotationsPresent() {
return isPresent("javax.validation.constraints.NotNull");
}
/**
* Return true if Jackson annotations like JsonIgnore are present.
*/
public boolean isJacksonAnnotationsPresent() {
return isPresent("com.fasterxml.jackson.annotation.JsonIgnore");
}
/**
* Return true if Jackson ObjectMapper is present.
*/
public boolean isJacksonObjectMapperPresent() {
return isPresent("com.fasterxml.jackson.databind.ObjectMapper");
}
/**
* Return a new instance of the class using the default constructor.
*/
public Object newInstance(String className) {
try {
Class<?> cls = forName(className);
return cls.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("Error constructing " + className, e);
}
}
/**
* Return true if the given class is present.
*/
protected boolean isPresent(String className) {
try {
forName(className);
return true;
} catch (Throwable ex) {
// Class or one of its dependencies is not present...
return false;
}
}
/**
* Load a class taking into account a context class loader (if present).
*/
protected Class<?> forName(String name) throws ClassNotFoundException {
return context.forName(name);
}
/**
* Wraps the preferred, caller and context class loaders.
*/
protected class ClassLoaderContext {
/**
* Optional - if set only use this classLoader (no fallback).
*/
protected final ClassLoader preferredLoader;
protected final ClassLoader contextLoader;
protected final ClassLoader callerLoader;
ClassLoaderContext(ClassLoader preferredLoader) {
this.preferredLoader = preferredLoader;
this.callerLoader = ServerConfig.class.getClassLoader();
this.contextLoader = contextLoader();
}
ClassLoader contextLoader() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return (loader != null) ? loader: callerLoader;
}
Class<?> forName(String name) throws ClassNotFoundException {
if (preferredLoader != null) {
// only use the explicitly set classLoader
return classForName(name, preferredLoader);
}
try {
// try the context loader first
return classForName(name, contextLoader);
} catch (ClassNotFoundException e) {
if (callerLoader == contextLoader) {
throw e;
} else {
// fallback to the caller classLoader
return classForName(name, callerLoader);
}
}
}
Class<?> classForName(String name, ClassLoader classLoader) throws ClassNotFoundException {
return Class.forName(name, true, classLoader);
}
}
}
@@ -1,39 +1,393 @@
package com.avaje.ebean.config;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.config.dbplatform.DbPlatformName;
import com.avaje.ebean.dbmigration.DbMigration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configuration for the DB migration processing.
*/
public class DbMigrationConfig {
protected static final Logger logger = LoggerFactory.getLogger(DbMigrationConfig.class);
/**
* The database platform to generate migration DDL for.
*/
protected DbPlatformName platform;
/**
* Set to true if the DB migration should be generated on server start.
*/
protected boolean generate;
/**
* Set to true to suppress the output of the rollback script.
*/
protected boolean suppressRollback;
/**
* The migration version name (typically FlywayDb compatible).
* <p>
* Example: 1.1.1_2
* <p>
* The version is expected to be the combination of the current pom version plus
* a 'feature' id. The combined version must be unique and ordered to work with
* FlywayDb so each developer sets a unique version so that the migration script
* generated is unique (typically just prior to being submitted as a merge request).
*/
protected String version;
/**
* Description text that can be appended to the version to become the ddl script file name.
* <p>
* So if the name is "a foo table" then the ddl script file could be:
* "1.1.1_2__a-foo-table.sql"
* <p>
* When the DB migration relates to a git feature (merge request) then this description text
* is a short description of the feature.
*/
protected String name;
/**
* Resource path for the migration xml and sql.
* Typically you would change 'app' to be a better/more unique.
*/
private String resourcePath = "dbmigration/app";
protected String migrationPath = "dbmigration";
/**
* Subdirectory the model xml files go into.
*/
protected String modelPath = "model";
/**
* Subdirectory the drop ddl scripts go into.
*/
protected String dropPath = "drop";
/**
* Subdirectory the rollback ddl scripts go into.
*/
protected String rollbackPath = "rollback";
/**
* Apply script suffix.
*/
protected String applySuffix = ".sql";
/**
* Default drop script suffix to ddl so that it isn't picked up by FlywayDb.
*/
protected String dropSuffix = ".drop.ddl";
/**
* Default rollback script suffix to ddl so that it isn't picked up by FlywayDb.
*/
protected String rollbackSuffix = ".rollback.ddl";
protected String modelSuffix = ".model.xml";
/**
* Return the DB platform to generate migration DDL for.
*
* We typically need to explicitly specify this as migration can often be generated
* when running against H2.
*/
public DbPlatformName getPlatform() {
return platform;
}
/**
* Set the DB platform to generate migration DDL for.
*/
public void setPlatform(DbPlatformName platform) {
this.platform = platform;
}
/**
* Return the resource path for db migrations.
*/
public String getResourcePath() {
return resourcePath;
public String getMigrationPath() {
return migrationPath;
}
/**
* Set the resource path for db migrations.
* <p>
* Typically this would be something like "dbmigration/myapp" where myapp gives it a
* unique resource path in the case there are multiple EbeanServer applications in the
* single classpath.
* The default of "dbmigration" is reasonable in most cases. You may look to set this
* to be something like "dbmigration/myapp" where myapp gives it a unique resource path
* in the case there are multiple EbeanServer applications in the single classpath.
* </p>
*/
public void setResourcePath(String resourcePath) {
this.resourcePath = resourcePath;
public void setMigrationPath(String migrationPath) {
this.migrationPath = migrationPath;
}
/**
* Return the relative path for the model files (defaults to model).
*/
public String getModelPath() {
return modelPath;
}
/**
* Set the relative path for the model files.
*/
public void setModelPath(String modelPath) {
this.modelPath = modelPath;
}
/**
* Return the relative path for the drop ddl scripts (defaults to drop).
*/
public String getDropPath() {
return dropPath;
}
/**
* Set the relative path for the drop ddl scripts (defaults to drop).
*/
public void setDropPath(String dropPath) {
this.dropPath = dropPath;
}
/**
* Return the relative path for the rollback ddl scripts (defaults to rollback).
*/
public String getRollbackPath() {
return rollbackPath;
}
/**
* Set the relative path for the rollback ddl scripts (defaults to rollback).
*/
public void setRollbackPath(String rollbackPath) {
this.rollbackPath = rollbackPath;
}
/**
* Return the model suffix (defaults to model.xml)
*/
public String getModelSuffix() {
return modelSuffix;
}
/**
* Set the model suffix.
*/
public void setModelSuffix(String modelSuffix) {
this.modelSuffix = modelSuffix;
}
/**
* Return true if the rollback script should not be output.
*/
public boolean isSuppressRollback() {
return suppressRollback;
}
/**
* Set to true to suppress the output of the rollback script.
*/
public void setSuppressRollback(boolean suppressRollback) {
this.suppressRollback = suppressRollback;
}
/**
* Return the apply script suffix (defaults to sql).
*/
public String getApplySuffix() {
return applySuffix;
}
/**
* Set the apply script suffix (defaults to sql).
*/
public void setApplySuffix(String applySuffix) {
this.applySuffix = applySuffix;
}
/**
* Return the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public String getDropSuffix() {
return dropSuffix;
}
/**
* Set the drop script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public void setDropSuffix(String dropSuffix) {
this.dropSuffix = dropSuffix;
}
/**
* Return the rollback script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public String getRollbackSuffix() {
return rollbackSuffix;
}
/**
* Set the rollback script suffix (defaults to ddl so that it isn't picked up by FlywayDb).
*/
public void setRollbackSuffix(String rollbackSuffix) {
this.rollbackSuffix = rollbackSuffix;
}
/**
* Set the migration version.
* <p>
* Note that version set via System property or environment variable <code>ddl.migration.version</code> takes precedence.
*/
public void setVersion(String version) {
this.version = version;
}
/**
* Set the migration name.
* <p>
* Note that name set via System property or environment variable <code>ddl.migration.name</code> takes precedence.
*/
public void setName(String name) {
this.name = name;
}
/**
* Set the model, rollback and drop paths to be empty such that all the migration files are generated
* into a single directory.
*/
public void singleDirectory() {
this.dropPath = "";
this.rollbackPath = "";
this.modelPath = "";
}
/**
* Load the settings from the PropertiesWrapper.
*/
public void loadSettings(PropertiesWrapper properties) {
resourcePath = properties.get("migration.resourcePath", resourcePath);
migrationPath = properties.get("migration.migrationPath", migrationPath);
if (properties.getBoolean("migration.singleDirectory", false)) {
singleDirectory();
} else {
modelPath = properties.get("migration.modelPath", modelPath);
rollbackPath = properties.get("migration.rollbackPath", rollbackPath);
dropPath = properties.get("migration.dropPath", dropPath);
}
applySuffix = properties.get("migration.applySuffix", applySuffix);
dropSuffix = properties.get("migration.dropSuffix", dropSuffix);
rollbackSuffix = properties.get("migration.rollbackSuffix", rollbackSuffix);
modelSuffix = properties.get("migration.modelSuffix", modelSuffix);
platform = properties.getEnum(DbPlatformName.class, "migration.platform", platform);
suppressRollback = properties.getBoolean("migration.suppressRollback", suppressRollback);
generate = properties.getBoolean("migration.generate", generate);
version = properties.get("migration.version", version);
name = properties.get("migration.name", name);
}
/**
* Return true if the migration should be generated.
* <p>
* It is expected that when an environment variable <code>ddl.migration.enabled</code>
* is set to <code>true</code> then the DB migration will generate the migration DDL.
* </p>
*/
public boolean isGenerateOnStart() {
// environment properties take precedence
String envGenerate = readEnvironment("ddl.migration.generate");
if (envGenerate != null) {
return "true".equalsIgnoreCase(envGenerate.trim());
}
return generate;
}
/**
* Called by EbeanServer on start.
*
* <p>
* If enabled this generates the migration xml and DDL scripts.
* </p>
*/
public void generateOnStart(EbeanServer server) {
if (isGenerateOnStart()) {
if (platform == null) {
logger.warn("No platform set for migration DDL generation");
} else {
// generate the migration xml and platform specific DDL
DbMigration migration = new DbMigration(server);
migration.setPlatform(platform);
try {
migration.generateMigration();
} catch (Exception e) {
throw new RuntimeException("Error generating DB migration", e);
}
}
}
}
/**
* Return the migration version (typically FlywayDb compatible).
* <p>
* Example: 1.1.1_2
* <p>
* The version is expected to be the combination of the current pom version plus
* a 'feature' id. The combined version must be unique and ordered to work with
* FlywayDb so each developer sets a unique version so that the migration script
* generated is unique (typically just prior to being submitted as a merge request).
*/
public String getVersion() {
String envVersion = readEnvironment("ddl.migration.version");
if (!isEmpty(envVersion)) {
return envVersion.trim();
}
return version;
}
/**
* Return the migration name which is short description text that can be appended to
* the migration version to become the ddl script file name.
* <p>
* So if the name is "a foo table" then the ddl script file could be:
* "1.1.1_2__a-foo-table.sql"
* </p>
* <p>
* When the DB migration relates to a git feature (merge request) then this description text
* is a short description of the feature.
* </p>
*/
public String getName() {
String envName = readEnvironment("ddl.migration.name");
if (!isEmpty(envName)) {
return envName.trim();
}
return name;
}
/**
* Return the system or environment property.
*/
protected String readEnvironment(String key) {
String val = System.getProperty(key);
if (val == null) {
val = System.getenv(key);
}
return val;
}
/**
* Return true if the string is null or empty.
*/
protected boolean isEmpty(String val) {
return val == null || val.trim().isEmpty();
}
}
@@ -141,8 +141,8 @@ public class PropertiesWrapper {
* Return a Enum property value.
*/
public <T extends Enum<T>> T getEnum(Class<T> enumType, String key, T defaultValue) {
String level = get(key, defaultValue.name());
return Enum.valueOf(enumType, level.toUpperCase());
String level = get(key, null);
return (level == null) ? defaultValue : Enum.valueOf(enumType, level.toUpperCase());
}
}
@@ -7,14 +7,20 @@ import com.avaje.ebean.cache.ServerCacheFactory;
import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.event.*;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanPostLoad;
import com.avaje.ebean.event.BeanQueryAdapter;
import com.avaje.ebean.event.BulkTableEventListener;
import com.avaje.ebean.event.ServerConfigStartup;
import com.avaje.ebean.event.TransactionEventListener;
import com.avaje.ebean.event.changelog.ChangeLogListener;
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
import com.avaje.ebean.event.changelog.ChangeLogRegister;
import com.avaje.ebean.event.readaudit.ReadAuditLogger;
import com.avaje.ebean.event.readaudit.ReadAuditPrepare;
import com.avaje.ebean.meta.MetaInfoManager;
import com.avaje.ebean.util.ClassUtil;
import com.fasterxml.jackson.core.JsonFactory;
import javax.sql.DataSource;
@@ -206,6 +212,12 @@ public class ServerConfig {
private boolean ddlRun;
private boolean ddlCreateOnly;
private String ddlInitSql;
private String ddlSeedSql;
private boolean useJtaTransactionManager;
/**
@@ -228,6 +240,11 @@ public class ServerConfig {
*/
private DbMigrationConfig migrationConfig = new DbMigrationConfig();
/**
* The ClassLoadConfig used to detect Joda, Java8, Jackson etc and create plugin instances given a className.
*/
private ClassLoadConfig classLoadConfig = new ClassLoadConfig();
/**
* Set to true if the DataSource uses autoCommit.
* <p>
@@ -1518,19 +1535,81 @@ public class ServerConfig {
}
/**
* Set to true to run the DDL generation on startup.
* Set to true to generate the "create all" DDL on startup.
*
* Typically we want this on when we are running tests locally (and often using H2)
* and we want to create the full DB schema from scratch to run tests.
*/
public void setDdlGenerate(boolean ddlGenerate) {
this.ddlGenerate = ddlGenerate;
}
/**
* Set to true to run the generated DDL on startup.
* Set to true to run the generated "create all DDL" on startup.
*
* Typically we want this on when we are running tests locally (and often using H2)
* and we want to create the full DB schema from scratch to run tests.
*/
public void setDdlRun(boolean ddlRun) {
this.ddlRun = ddlRun;
}
/**
* Return true if the "drop all ddl" should be skipped.
*
* Typically we want to do this when using H2 (in memory) as our test database and the drop statements
* are not required so skipping the drop table statements etc makes it faster with less noise in the logs.
*/
public boolean isDdlCreateOnly() {
return ddlCreateOnly;
}
/**
* Set to true if the "drop all ddl" should be skipped.
*
* Typically we want to do this when using H2 (in memory) as our test database and the drop statements
* are not required so skipping the drop table statements etc makes it faster with less noise in the logs.
*/
public void setDdlCreateOnly(boolean ddlCreateOnly) {
this.ddlCreateOnly = ddlCreateOnly;
}
/**
* Return SQL script to execute after the "create all" DDL has been run.
* <p>
* Typically this is a sql script that inserts test seed data when running tests.
* Place a sql script in src/test/resources that inserts test seed data.
* </p>
*/
public String getDdlSeedSql() {
return ddlSeedSql;
}
/**
* Set a SQL script to execute after the "create all" DDL has been run.
* <p>
* Typically this is a sql script that inserts test seed data when running tests.
* Place a sql script in src/test/resources that inserts test seed data.
* </p>
*/
public void setDdlSeedSql(String ddlSeedSql) {
this.ddlSeedSql = ddlSeedSql;
}
/**
* Return a SQL script to execute before the "create all" DDL has been run.
*/
public String getDdlInitSql() {
return ddlInitSql;
}
/**
* Set a SQL script to execute before the "create all" DDL has been run.
*/
public void setDdlInitSql(String ddlInitSql) {
this.ddlInitSql = ddlInitSql;
}
/**
* Return true if the DDL should be generated.
*/
@@ -2023,6 +2102,22 @@ public class ServerConfig {
this.persistenceContextScope = persistenceContextScope;
}
/**
* Return the ClassLoadConfig which is used to detect Joda, Java8 types etc and also
* create new instances of plugins given a className.
*/
public ClassLoadConfig getClassLoadConfig() {
return classLoadConfig;
}
/**
* Set the ClassLoadConfig which is used to detect Joda, Java8 types etc and also
* create new instances of plugins given a className.
*/
public void setClassLoadConfig(ClassLoadConfig classLoadConfig) {
this.classLoadConfig = classLoadConfig;
}
/**
* Load settings from ebean.properties.
*/
@@ -2062,14 +2157,31 @@ public class ServerConfig {
return properties;
}
/**
* Return the instance to use (can be null) for the given plugin.
*
* @param properties the properties
* @param pluginType the type of plugin
* @param key properties key
* @param instance existing instance
*/
@SuppressWarnings("unchecked")
private <T> T createInstance(PropertiesWrapper p, Class<T> pluginType, String key, T instance) {
protected <T> T createInstance(PropertiesWrapper properties, 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);
String classname = properties.get(key, null);
return createInstance(pluginType, classname);
}
/**
* Return the instance to use (can be null) for the given plugin.
* @param pluginType the type of plugin
* @param classname the implementation class as per properties
*/
protected <T> T createInstance(Class<T> pluginType, String classname) {
return classname == null ? null : (T) classLoadConfig.newInstance(classname);
}
/**
@@ -2188,6 +2300,9 @@ public class ServerConfig {
ddlGenerate = p.getBoolean("ddl.generate", ddlGenerate);
ddlRun = p.getBoolean("ddl.run", ddlRun);
ddlCreateOnly = p.getBoolean("ddl.createOnly", ddlCreateOnly);
ddlInitSql = p.get("ddl.initSql", ddlInitSql);
ddlSeedSql = p.get("ddl.seedSql", ddlSeedSql);
classes = getClasses(p);
}
@@ -17,7 +17,7 @@ public class DB2SequenceIdGenerator extends SequenceIdGenerator {
*/
public DB2SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) {
super(be, ds, seqName, batchSize);
this.baseSql = "select nextval for " + seqName;
this.baseSql = "values nextval for " + seqName;
this.unionBaseSql = " union " + baseSql;
}
@@ -21,8 +21,8 @@ import com.avaje.ebean.dbmigration.model.CurrentModel;
import com.avaje.ebean.dbmigration.model.MConfiguration;
import com.avaje.ebean.dbmigration.model.MigrationModel;
import com.avaje.ebean.dbmigration.model.ModelContainer;
import com.avaje.ebean.dbmigration.model.PlatformDdlWriter;
import com.avaje.ebean.dbmigration.model.ModelDiff;
import com.avaje.ebean.dbmigration.model.PlatformDdlWriter;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,6 +57,13 @@ public class DbMigration {
protected static final Logger logger = LoggerFactory.getLogger(DbMigration.class);
private static final String initialVersion = "1.0";
/**
* Set to true if DbMigration run with online EbeanServer instance.
*/
protected final boolean online;
protected SpiEbeanServer server;
protected DbMigrationConfig migrationConfig;
@@ -71,7 +78,19 @@ public class DbMigration {
protected DbConstraintNaming constraintNaming;
/**
* Create for offline migration generation.
*/
public DbMigration() {
this.online = false;
}
/**
* Create using online EbeanServer.
*/
public DbMigration(EbeanServer server) {
this.online = true;
setServer(server);
}
/**
@@ -125,7 +144,9 @@ public class DbMigration {
*/
public void setPlatform(DatabasePlatform databasePlatform) {
this.databasePlatform = databasePlatform;
DbOffline.setPlatform(databasePlatform.getName());
if (!online) {
DbOffline.setPlatform(databasePlatform.getName());
}
}
/**
@@ -175,16 +196,19 @@ public class DbMigration {
public void generateMigration() throws IOException {
// use this flag to stop other plugins like full DDL generation
DbOffline.setRunningMigration();
if (!online) {
DbOffline.setRunningMigration();
}
setDefaults();
try {
MigrationModel migrationModel = new MigrationModel(migrationConfig.getResourcePath());
ModelContainer migrated = migrationModel.read();
int nextMajorVersion = migrationModel.getNextMajorVersion();
logger.info("next migration version {}", nextMajorVersion);
File migrationDir = getMigrationDirectory();
File modelDir = getModelDirectory(migrationDir);
MigrationModel migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix());
ModelContainer migrated = migrationModel.read();
CurrentModel currentModel = new CurrentModel(server, constraintNaming);
ModelContainer current = currentModel.read();
@@ -200,46 +224,89 @@ public class DbMigration {
// there were actually changes to write
Migration dbMigration = diff.getMigration();
File writePath = getWritePath();
logger.info("migration writing version {} to {}", nextMajorVersion, writePath.getAbsolutePath());
writeMigrationXml(dbMigration, writePath, nextMajorVersion);
String fullVersion = getFullVersion(migrationModel);
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read());
PlatformDdlWriter writer = new PlatformDdlWriter(databasePlatform, serverConfig);
writer.processMigration(dbMigration, write, writePath, nextMajorVersion);
logger.info("generating migration:{}", fullVersion);
if (!writeMigrationXml(dbMigration, modelDir, fullVersion)) {
logger.warn("migration already exists, not generating DDL");
} else {
if (databasePlatform != null) {
// writer needs the current model to provide table/column details for
// history ddl generation (triggers, history tables etc)
DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read());
PlatformDdlWriter writer = createDdlWriter(databasePlatform, "");
writer.processMigration(dbMigration, write, migrationDir , fullVersion);
}
writeExtraPlatformDdl(fullVersion, currentModel, dbMigration, migrationDir);
}
writeExtraPlatformDdl(nextMajorVersion, currentModel, dbMigration, writePath);
} finally {
DbOffline.reset();
if (!online) {
DbOffline.reset();
}
}
}
/**
* Return the full version for the migration being generated.
*/
private String getFullVersion(MigrationModel migrationModel) {
String version = migrationConfig.getVersion();
if (version == null) {
version = migrationModel.getNextVersion(initialVersion);
}
String fullVersion = version;
String name = migrationConfig.getName();
if (name != null) {
fullVersion += "__" + toUnderScore(name);
}
return fullVersion;
}
/**
* Replace spaces with underscores.
*/
private String toUnderScore(String name) {
return name.replace(' ','_');
}
/**
* Write any extra platform ddl.
*/
protected void writeExtraPlatformDdl(int nextMajorVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException {
protected void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException {
for (Pair pair : platforms) {
DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read());
PlatformDdlWriter platformWriter = new PlatformDdlWriter(pair.platform, serverConfig, pair.prefix);
platformWriter.processMigration(dbMigration, platformBuffer, writePath, nextMajorVersion);
PlatformDdlWriter platformWriter = createDdlWriter(pair);
platformWriter.processMigration(dbMigration, platformBuffer, writePath, fullVersion);
}
}
private PlatformDdlWriter createDdlWriter(Pair pair) {
return createDdlWriter(pair.platform, pair.prefix);
}
private PlatformDdlWriter createDdlWriter(DatabasePlatform platform, String prefix) {
return new PlatformDdlWriter(platform, serverConfig, prefix, migrationConfig);
}
/**
* Write the migration xml.
*/
protected void writeMigrationXml(Migration dbMigration, File resourcePath, int migrationVersion) {
protected boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) {
File file = new File(resourcePath, "v"+migrationVersion+".0.xml");
String modelFile = fullVersion + migrationConfig.getModelSuffix();
File file = new File(resourcePath, modelFile);
if (file.exists()) {
return false;
}
MigrationXmlWriter xmlWriter = new MigrationXmlWriter();
xmlWriter.write(dbMigration, file);
return true;
}
/**
@@ -260,14 +327,13 @@ public class DbMigration {
/**
* Return the file path to write the xml and sql to.
*/
protected File getWritePath() {
protected File getMigrationDirectory() {
// path to src/main/resources in typical maven project
File resourceRootDir = new File(pathToResources);
String resourcePath = migrationConfig.getMigrationPath();
String resourcePath = migrationConfig.getResourcePath();
// expect to be a path to something like - src/main/resources/dbmigration/myapp
// expect to be a path to something like - src/main/resources/dbmigration/model
File path = new File(resourceRootDir, resourcePath);
if (!path.exists()) {
if (!path.mkdirs()) {
@@ -277,6 +343,21 @@ public class DbMigration {
return path;
}
/**
* Return the model directory (relative to the migration directory).
*/
protected File getModelDirectory(File migrationDirectory) {
String modelPath = migrationConfig.getModelPath();
if (modelPath == null || modelPath.isEmpty()) {
return migrationDirectory;
}
File modelDir = new File(migrationDirectory, migrationConfig.getModelPath());
if (!modelDir.exists() && !modelDir.mkdirs()) {
logger.debug("Unable to ensure migration model directory exists at {}", modelDir.getAbsolutePath());
}
return modelDir;
}
/**
* Return the DatabasePlatform given the platform key.
*/
@@ -1,47 +1,43 @@
package com.avaje.ebean.dbmigration;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.dbmigration.model.CurrentModel;
import com.avaje.ebeaninternal.api.SpiEbeanPlugin;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.io.Reader;
/**
* Controls the generation of DDL and potentially runs the resulting scripts.
* Controls the generation and execution of "Create All" and "Drop All" DDL scripts.
*
* Typically the "Create All" DDL is executed for running tests etc and has nothing to do
* with DB Migration (diff based) DDL.
*/
public class DdlGenerator implements SpiEbeanPlugin {
public class DdlGenerator {
private static final Logger logger = LoggerFactory.getLogger(DdlGenerator.class);
private final SpiEbeanServer server;
private SpiEbeanServer server;
private boolean generateDdl;
private boolean runDdl;
private final boolean generateDdl;
private final boolean runDdl;
private final boolean createOnly;
private CurrentModel currentModel;
private String dropContent;
private String createContent;
public void setup(SpiEbeanServer server, ServerConfig serverConfig) {
public DdlGenerator(SpiEbeanServer server, ServerConfig serverConfig) {
this.server = server;
this.generateDdl = serverConfig.isDdlGenerate();
this.runDdl = serverConfig.isDdlRun();
this.createOnly = serverConfig.isDdlCreateOnly();
}
/**
@@ -58,9 +54,11 @@ public class DdlGenerator implements SpiEbeanPlugin {
/**
* Generate the DDL drop and create scripts if the properties have been set.
*/
public void generateDdl() {
protected void generateDdl() {
if (generateDdl) {
writeDrop(getDropFileName());
if (!createOnly) {
writeDrop(getDropFileName());
}
writeCreate(getCreateFileName());
}
}
@@ -68,18 +66,14 @@ public class DdlGenerator implements SpiEbeanPlugin {
/**
* Run the DDL drop and DDL create scripts if properties have been set.
*/
public void runDdl() {
protected void runDdl() {
if (runDdl) {
try {
if (dropContent == null) {
dropContent = readFile(getDropFileName());
}
if (createContent == null) {
createContent = readFile(getCreateFileName());
}
runScript(true, dropContent);
runScript(false, createContent);
runInitSql();
runDropSql();
runCreateSql();
runSeedSql();
} catch (IOException e) {
String msg = "Error reading drop/create script from file system";
@@ -88,6 +82,53 @@ public class DdlGenerator implements SpiEbeanPlugin {
}
}
protected void runDropSql() throws IOException {
if (!createOnly) {
if (dropContent == null) {
dropContent = readFile(getDropFileName());
}
runScript(true, dropContent, getDropFileName());
}
}
protected void runCreateSql() throws IOException {
if (createContent == null) {
createContent = readFile(getCreateFileName());
}
runScript(false, createContent, getCreateFileName());
}
protected void runInitSql() throws IOException {
runResourceScript(server.getServerConfig().getDdlInitSql());
}
protected void runSeedSql() throws IOException {
runResourceScript(server.getServerConfig().getDdlSeedSql());
}
protected void runResourceScript(String sqlScript) throws IOException {
if (sqlScript != null) {
InputStream is = getClassLoader().getResourceAsStream(sqlScript);
if (is != null) {
DdlRunner runner = new DdlRunner(false, sqlScript);
String content = readContent(new InputStreamReader(is));
runner.runAll(content, server);
}
}
}
/**
* Return the classLoader to use to read sql scripts as resources.
*/
protected ClassLoader getClassLoader() {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = this.getClassLoader();
}
return cl;
}
protected void writeDrop(String dropFile) {
try {
@@ -108,7 +149,7 @@ public class DdlGenerator implements SpiEbeanPlugin {
}
}
public String generateDropDdl() {
protected String generateDropDdl() {
try {
dropContent = currentModel().getDropDdl();
@@ -118,7 +159,7 @@ public class DdlGenerator implements SpiEbeanPlugin {
}
}
public String generateCreateDdl() {
protected String generateCreateDdl() {
try {
createContent = currentModel().getCreateDdl();
@@ -163,201 +204,33 @@ public class DdlGenerator implements SpiEbeanPlugin {
return null;
}
return readContent(new FileReader(f));
}
protected String readContent(Reader reader) throws IOException {
StringBuilder buf = new StringBuilder();
FileReader fr = new FileReader(f);
LineNumberReader lr = new LineNumberReader(fr);
LineNumberReader lineReader = new LineNumberReader(reader);
try {
String s;
while ((s = lr.readLine()) != null) {
while ((s = lineReader.readLine()) != null) {
buf.append(s).append("\n");
}
} finally {
lr.close();
}
return buf.toString();
return buf.toString();
} finally {
lineReader.close();
}
}
/**
* Execute all the DDL statements in the script.
*/
public void runScript(boolean expectErrors, String content) {
public int runScript(boolean expectErrors, String content, String scriptName) {
StringReader sr = new StringReader(content);
List<String> statements = parseStatements(sr);
Transaction t = server.createTransaction();
try {
Connection connection = t.getConnection();
logger.info("Running DDL");
runStatements(expectErrors, statements, connection);
logger.info("Running DDL Complete");
t.commit();
} catch (Exception e) {
throw new PersistenceException("Error: " + e.getMessage(), e);
} finally {
t.end();
}
DdlRunner runner = new DdlRunner(expectErrors, scriptName);
return runner.runAll(content, server);
}
/**
* Execute the list of statements.
*/
private void runStatements(boolean expectErrors, List<String> statements, Connection c) {
List<String> noDuplicates = new ArrayList<String>();
for (String statement : statements) {
if (!noDuplicates.contains(statement)) {
noDuplicates.add(statement);
}
}
for (int i = 0; i < noDuplicates.size(); i++) {
String xOfy = (i + 1) + " of " + noDuplicates.size();
runStatement(expectErrors, xOfy, noDuplicates.get(i), c);
}
}
/**
* Execute the statement.
*/
private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) {
PreparedStatement pstmt = null;
try {
// trim and remove trailing ; or /
stmt = stmt.trim();
if (stmt.endsWith(";")) {
stmt = stmt.substring(0, stmt.length() - 1);
} else if (stmt.endsWith("/")) {
stmt = stmt.substring(0, stmt.length() - 1);
}
logger.info("executing " + oneOf + " " + getSummary(stmt));
pstmt = c.prepareStatement(stmt);
pstmt.execute();
} catch (Exception e) {
if (expectErrors) {
logger.info(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage());
} else {
String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]";
throw new RuntimeException(msg, e);
}
} finally {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
logger.error("Error closing pstmt", e);
}
}
}
}
/**
* 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;
*/
protected List<String> parseStatements(StringReader reader) {
try {
BufferedReader br = new BufferedReader(reader);
StatementsSeparator statements = new StatementsSeparator();
String s;
while ((s = br.readLine()) != null) {
s = s.trim();
statements.nextLine(s);
}
return statements.statements;
} catch (IOException e) {
throw new PersistenceException(e);
}
}
private String getSummary(String s) {
if (s.length() > 80) {
return s.substring(0, 80).trim() + "...";
}
return s;
}
}
@@ -0,0 +1,110 @@
package com.avaje.ebean.dbmigration;
import javax.persistence.PersistenceException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
/**
* Parses string content into separate SQL/DDL statements.
*/
public class DdlParser {
/**
* Break up the sql in reader into a list of statements using the semi-colon and $$ delimiters;
*/
public List<String> parse(StringReader reader) {
try {
BufferedReader br = new BufferedReader(reader);
StatementsSeparator statements = new StatementsSeparator();
String s;
while ((s = br.readLine()) != null) {
s = s.trim();
statements.nextLine(s);
}
return statements.statements;
} catch (IOException e) {
throw new PersistenceException(e);
}
}
/**
* 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 (sb.length() == 0 && (line.isEmpty() || line.startsWith("--"))) {
// ignore leading empty lines and sql comments
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));
}
}
}
}
@@ -0,0 +1,138 @@
package com.avaje.ebean.dbmigration;
import com.avaje.ebean.Transaction;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Runs DDL scripts.
*/
public class DdlRunner {
protected static final Logger logger = LoggerFactory.getLogger(DdlRunner.class);
protected DdlParser ddlParser = new DdlParser();
protected final String scriptName;
protected final boolean expectErrors;
/**
* Construct with a script name (for logging) and flag indicating if errors are expected.
*/
public DdlRunner(boolean expectErrors, String scriptName) {
this.expectErrors = expectErrors;
this.scriptName = scriptName;
}
/**
* Parse the content into sql statements and execute them in a transaction.
*/
public int runAll(String content, SpiEbeanServer server) {
List<String> statements = ddlParser.parse(new StringReader(content));
return runStatements(statements, server);
}
/**
* Execute all the statements in a single transaction.
*/
public int runStatements(List<String> statements, SpiEbeanServer server) {
Transaction t = server.createTransaction();
try {
int statementCount = runStatements(expectErrors, statements, t.getConnection());
t.commit();
return statementCount;
} catch (Exception e) {
throw new PersistenceException("Error: " + e.getMessage(), e);
} finally {
t.end();
}
}
/**
* Execute the list of statements.
*/
private int runStatements(boolean expectErrors, List<String> statements, Connection c) {
List<String> noDuplicates = new ArrayList<String>();
for (String statement : statements) {
if (!noDuplicates.contains(statement)) {
noDuplicates.add(statement);
}
}
logger.info("Executing {} - {} statements", scriptName, noDuplicates.size());
for (int i = 0; i < noDuplicates.size(); i++) {
String xOfy = (i + 1) + " of " + noDuplicates.size();
runStatement(expectErrors, xOfy, noDuplicates.get(i), c);
}
return noDuplicates.size();
}
/**
* Execute the statement.
*/
private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) {
PreparedStatement pstmt = null;
try {
// trim and remove trailing ; or /
stmt = stmt.trim();
if (stmt.endsWith(";")) {
stmt = stmt.substring(0, stmt.length() - 1);
} else if (stmt.endsWith("/")) {
stmt = stmt.substring(0, stmt.length() - 1);
}
if (logger.isDebugEnabled()) {
logger.debug("executing " + oneOf + " " + getSummary(stmt));
}
pstmt = c.prepareStatement(stmt);
pstmt.execute();
} catch (Exception e) {
if (expectErrors) {
logger.debug(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage());
} else {
String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]";
throw new RuntimeException(msg, e);
}
} finally {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
logger.error("Error closing pstmt", e);
}
}
}
}
private String getSummary(String s) {
if (s.length() > 80) {
return s.substring(0, 80).trim() + "...";
}
return s;
}
}
@@ -21,6 +21,7 @@ import com.avaje.ebean.dbmigration.migration.DropTable;
import com.avaje.ebean.dbmigration.migration.ForeignKey;
import com.avaje.ebean.dbmigration.migration.UniqueConstraint;
import com.avaje.ebean.dbmigration.model.MTable;
import com.avaje.ebean.util.StringHelper;
import java.io.IOException;
import java.math.BigInteger;
@@ -123,7 +124,11 @@ public class BaseTableDdl implements TableDdl {
writePrimaryKeyConstraint(apply, createTable.getPkName(), toColumnNames(pk));
}
apply.newLine().append(")").endOfStatement();
apply.newLine().append(")");
addTableCommentInline(apply, createTable);
apply.endOfStatement();
addComments(apply, createTable);
writeUniqueOneToOneConstraints(writer, createTable);
@@ -150,6 +155,37 @@ public class BaseTableDdl implements TableDdl {
}
/**
* Add table and column comments (separate from the create table statement).
*/
private void addComments(DdlBuffer apply, CreateTable createTable) throws IOException {
if (!platformDdl.isInlineComments()) {
String tableComment = createTable.getComment();
if (!StringHelper.isNull(tableComment)) {
platformDdl.addTableComment(apply, createTable.getName(), tableComment);
}
List<Column> columns = createTable.getColumn();
for (Column column : columns) {
if (!StringHelper.isNull(column.getComment())) {
platformDdl.addColumnComment(apply, createTable.getName(), column.getName(), column.getComment());
}
}
}
}
/**
* Add the table comment inline with the create table statement.
*/
private void addTableCommentInline(DdlBuffer apply, CreateTable createTable) throws IOException {
if (platformDdl.isInlineComments()) {
String tableComment = createTable.getComment();
if (!StringHelper.isNull(tableComment)) {
platformDdl.inlineTableComment(apply, tableComment);
}
}
}
private void writeTableColumns(DdlBuffer apply, List<Column> columns, boolean useIdentity) throws IOException {
platformDdl.writeTableColumns(apply, columns, useIdentity);
}
@@ -160,8 +160,10 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
Collection<MColumn> cols = table.getColumns().values();
for (MColumn column : cols) {
writeColumnDefinition(apply, column.getName(), column.getType());
apply.append(",").newLine();
if (!column.isDraftOnly()) {
writeColumnDefinition(apply, column.getName(), column.getType());
apply.append(",").newLine();
}
}
writeColumnDefinition(apply, sysPeriodStart, sysPeriodType);
apply.append(",").newLine();
@@ -232,7 +234,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl {
List<String> includedColumns = new ArrayList<String>(columns.size());
for (MColumn column : columns) {
if (!column.isHistoryExclude()) {
if (column.isIncludeInHistory()) {
includedColumns.add(column.getName());
}
}
@@ -2,8 +2,11 @@ package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.DbIdentity;
import com.avaje.ebean.config.dbplatform.DbTypeMap;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.migration.AlterColumn;
import java.io.IOException;
/**
* MS SQL Server platform specific DDL.
*/
@@ -15,6 +18,7 @@ public class MsSqlServerDdl extends PlatformDdl {
this.foreignKeyRestrict = "";
this.inlineUniqueOneToOne = false;
this.columnSetDefault = "add default";
this.dropConstraintIfExists = "drop constraint";
}
@Override
@@ -83,4 +87,20 @@ public class MsSqlServerDdl extends PlatformDdl {
// can't alter itself - done in alterColumnBaseAttributes()
return null;
}
/**
* Add table comment as a separate statement (from the create table statement).
*/
public void addTableComment(DdlBuffer apply, String tableName, String tableComment) throws IOException {
// do nothing for MS SQL Server (cause it requires stored procedures etc)
}
/**
* Add column comment as a separate statement.
*/
public void addColumnComment(DdlBuffer apply, String table, String column, String comment) throws IOException {
// do nothing for MS SQL Server (cause it requires stored procedures etc)
}
}
@@ -2,7 +2,12 @@ package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.DbIdentity;
import com.avaje.ebean.config.dbplatform.DbTypeMap;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.migration.AlterColumn;
import com.avaje.ebean.dbmigration.migration.Column;
import com.avaje.ebeaninternal.server.lib.util.StringHelper;
import java.io.IOException;
/**
* MySql specific DDL.
@@ -14,6 +19,7 @@ public class MySqlDdl extends PlatformDdl {
this.alterColumn = "modify";
this.dropUniqueConstraint = "drop index";
this.historyDdl = new MySqlHistoryDdl();
this.inlineComments = true;
}
/**
@@ -64,4 +70,26 @@ public class MySqlDdl extends PlatformDdl {
// use modify
return "alter table " + tableName + " modify " + columnName + " " + type + notnullClause;
}
@Override
protected void writeColumnDefinition(DdlBuffer buffer, Column column, boolean useIdentity) throws IOException {
super.writeColumnDefinition(buffer, column, useIdentity);
String comment = column.getComment();
if (!StringHelper.isNull(comment)) {
// in mysql 5.5 column comment save in information_schema.COLUMNS.COLUMN_COMMENT(VARCHAR 1024)
if (comment.length() > 500) {
comment = comment.substring(0, 500);
}
buffer.append(String.format(" comment '%s'", comment));
}
}
public void inlineTableComment(DdlBuffer apply, String tableComment) throws IOException {
if (tableComment.length() > 1000) {
tableComment = tableComment.substring(0, 1000);
}
apply.append(" comment='").append(tableComment).append("'");
}
}
@@ -37,6 +37,11 @@ public class PlatformDdl {
*/
private final DbIdentity dbIdentity;
/**
* Set to true if table and column comments are included inline with the create statements.
*/
protected boolean inlineComments;
/**
* Default assumes if exists is supported.
*/
@@ -53,6 +58,8 @@ public class PlatformDdl {
protected String identitySuffix = " auto_increment";
protected String alterTableIfExists = "";
protected String dropConstraintIfExists = "drop constraint if exists";
protected String dropIndexIfExists = "drop index if exists ";
@@ -114,6 +121,13 @@ public class PlatformDdl {
return columnDefn + identitySuffix;
}
/**
* Return true if the table and column comments are included inline.
*/
public boolean isInlineComments() {
return inlineComments;
}
/**
* Write all the table columns converting to platform types as necessary.
*/
@@ -150,7 +164,7 @@ public class PlatformDdl {
* Return the drop foreign key clause.
*/
public String alterTableDropForeignKey(String tableName, String fkName) {
return "alter table " + tableName + " " + dropConstraintIfExists + " " + fkName;
return "alter table " + alterTableIfExists + tableName + " " + dropConstraintIfExists + " " + fkName;
}
/**
@@ -384,4 +398,27 @@ public class PlatformDdl {
protected boolean isTrue(Boolean value) {
return Boolean.TRUE.equals(value);
}
/**
* Add an inline table comment to the create table statement.
*/
public void inlineTableComment(DdlBuffer apply, String tableComment) throws IOException {
// do nothing by default (MySql only)
}
/**
* Add table comment as a separate statement (from the create table statement).
*/
public void addTableComment(DdlBuffer apply, String tableName, String tableComment) throws IOException {
apply.append(String.format("comment on table %s is '%s'", tableName, tableComment)).endOfStatement();
}
/**
* Add column comment as a separate statement.
*/
public void addColumnComment(DdlBuffer apply, String table, String column, String comment) throws IOException {
apply.append(String.format("comment on column %s.%s is '%s'", table, column, comment)).endOfStatement();
}
}
@@ -13,6 +13,7 @@ public class PostgresDdl extends PlatformDdl {
this.historyDdl = new PostgresHistoryDdl();
this.dropTableCascade = " cascade";
this.columnSetType = "type ";
this.alterTableIfExists = "if exists ";
}
/**
@@ -6,6 +6,9 @@ import com.avaje.ebean.dbmigration.migration.Migration;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
@@ -15,19 +18,6 @@ public class MigrationXmlReader {
private static final MigrationXmlReader INSTANCE = new MigrationXmlReader();
/**
* Read and return a Migration from an xml document at the given resource path.
*/
public static Migration readMaybe(String resourcePath) {
InputStream is = MigrationXmlReader.class.getResourceAsStream(resourcePath);
if (is == null) {
return null;
}
return INSTANCE.read(is);
}
/**
* Read and return a Migration from an xml document at the given resource path.
*/
@@ -41,10 +31,27 @@ public class MigrationXmlReader {
return INSTANCE.read(is);
}
/**
* Read and return a Migration from a migration xml file.
*/
public static Migration read(File migrationFile) {
try {
FileInputStream is = new FileInputStream(migrationFile);
try {
return read(is);
} finally {
is.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Read and return a Migration from an xml document.
*/
public Migration read(InputStream is) {
public static Migration read(InputStream is) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(Migration.class);
@@ -16,6 +16,7 @@ public class MColumn {
private String references;
private String foreignKeyName;
private String foreignKeyIndex;
private String comment;
private boolean historyExclude;
private boolean notnull;
@@ -43,6 +44,7 @@ public class MColumn {
this.checkConstraint = column.getCheckConstraint();
this.checkConstraintName = column.getCheckConstraintName();
this.defaultValue = column.getDefaultValue();
this.comment = column.getComment();
this.references = column.getReferences();
this.foreignKeyName = column.getForeignKeyName();
this.foreignKeyIndex = column.getForeignKeyIndex();
@@ -76,6 +78,7 @@ public class MColumn {
copy.checkConstraintName = checkConstraintName;
copy.defaultValue = defaultValue;
copy.references = references;
copy.comment = comment;
copy.foreignKeyName = foreignKeyName;
copy.foreignKeyIndex = foreignKeyIndex;
copy.historyExclude = historyExclude;
@@ -198,6 +201,19 @@ public class MColumn {
return uniqueOneToOne;
}
/**
* Return the column comment.
*/
public String getComment() {
return comment;
}
/**
* Set the column comment.
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
* Set the draftOnly status for this column.
@@ -213,6 +229,13 @@ public class MColumn {
return draftOnly;
}
/**
* Return true if this column should be included in History DB triggers etc.
*/
public boolean isIncludeInHistory() {
return !draftOnly && !historyExclude;
}
public Column createColumn() {
Column c = new Column();
@@ -230,6 +253,7 @@ public class MColumn {
c.setForeignKeyName(foreignKeyName);
c.setForeignKeyIndex(foreignKeyIndex);
c.setDefaultValue(defaultValue);
c.setComment(comment);
c.setUnique(unique);
c.setUniqueOneToOne(uniqueOneToOne);
@@ -325,6 +325,10 @@ public class MTable {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getTablespace() {
return tablespace;
}
@@ -1,12 +1,13 @@
package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.dbmigration.migration.Migration;
import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Set;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Build the model from the series of migrations.
@@ -17,26 +18,15 @@ public class MigrationModel {
private final ModelContainer model = new ModelContainer();
private final Set<String> readVersions = new LinkedHashSet<String>();
private final File modelDirectory;
private final String resourcePath;
private final String modelSuffix;
int nextMajorVersion;
private MigrationVersion lastVersion;
public MigrationModel(String resourcePath) {
this.resourcePath = normaliseResourcePath(resourcePath);
}
private String normaliseResourcePath(String resourcePath) {
if (resourcePath.endsWith("/")) {
// trim trailing slash
resourcePath = resourcePath.substring(0, resourcePath.length()-1);
}
if (resourcePath.startsWith("/")) {
// trim leading slash
resourcePath = resourcePath.substring(1);
}
return resourcePath;
public MigrationModel(File modelDirectory, String modelSuffix) {
this.modelDirectory = modelDirectory;
this.modelSuffix = modelSuffix;
}
/**
@@ -46,57 +36,41 @@ public class MigrationModel {
public ModelContainer read() {
readMigrations();
logger.info("read versions {}", readVersions);
return model;
}
/**
* Return the set of versions that were read.
*/
public Set<String> getReadVersions() {
return readVersions;
}
public int getNextMajorVersion() {
return nextMajorVersion;
}
private void readMigrations() {
for (int majorVersion = 1; majorVersion < 100; majorVersion++) {
if (!readMinorVersions(majorVersion)){
// no major.0 version so stopping
nextMajorVersion = majorVersion;
return;
// find all the migration xml files
File[] xmlFiles = modelDirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith(modelSuffix);
}
});
List<MigrationResource> resources = new ArrayList<MigrationResource>();
for (File xmlFile: xmlFiles) {
resources.add(new MigrationResource(xmlFile));
}
// sort into version order before applying
Collections.sort(resources);
for (MigrationResource migrationResource: resources) {
logger.debug("read {}", migrationResource);
model.apply(migrationResource.read());
}
// remember the last version
if (!resources.isEmpty()) {
lastVersion = resources.get(resources.size() - 1).getVersion();
}
}
private boolean readMinorVersions(int majorVersion) {
public String getNextVersion(String initialVersion) {
for (int minorVersion = 0; minorVersion < 100; minorVersion++) {
if (!readMigration(majorVersion, minorVersion)) {
// continue reading next major if minorVersion 0 was read
return (minorVersion > 0);
}
}
return true;
return lastVersion == null ? initialVersion : lastVersion.nextVersion();
}
private boolean readMigration(int majorVersion, int minorVersion) {
String version = majorVersion+"."+minorVersion;
String path = "/"+resourcePath+"/v"+version+".xml";
Migration migration = MigrationXmlReader.readMaybe(path);
if (migration == null) {
logger.debug("... no migration at path:{}", path);
return false;
}
readVersions.add(version);
logger.trace("... read migration v{}", version);
model.apply(migration);
return true;
}
}
@@ -0,0 +1,51 @@
package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.dbmigration.migration.Migration;
import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlReader;
import java.io.File;
/**
* Migration XML resource that holds the changes to be applied.
*/
public class MigrationResource implements Comparable<MigrationResource> {
private final File migrationFile;
private final MigrationVersion version;
/**
* Construct with a migration xml file.
*/
public MigrationResource(File migrationFile) {
this.migrationFile = migrationFile;
this.version = MigrationVersion.parse(migrationFile.getName());
}
public String toString() {
return migrationFile.getName();
}
/**
* Return the version associated with this resource.
*/
public MigrationVersion getVersion() {
return version;
}
/**
* Read and return the migration from the resource.
*/
public Migration read() {
return MigrationXmlReader.read(migrationFile);
}
/**
* Compare by underlying version.
*/
@Override
public int compareTo(MigrationResource other) {
return version.compareTo(other.version);
}
}
@@ -0,0 +1,86 @@
package com.avaje.ebean.dbmigration.model;
/**
* The version of a migration used so that migrations are processed in order.
*/
public class MigrationVersion implements Comparable<MigrationVersion> {
/**
* The raw version text.
*/
private final String raw;
/**
* The ordering parts.
*/
private final int[] ordering;
private MigrationVersion(String raw, int[] ordering) {
this.raw = raw;
this.ordering = ordering;
}
public String toString() {
return raw;
}
public String nextVersion() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ordering.length; i++) {
if (i < ordering.length -1 ) {
sb.append(ordering[i]).append(".");
} else {
sb.append(ordering[i]+1);
}
}
return sb.toString();
}
@Override
public int compareTo(MigrationVersion other) {
int otherLength = other.ordering.length;
for (int i = 0; i < ordering.length; i++) {
if (i >= otherLength) {
// considered greater
return 1;
}
if (ordering[i] != other.ordering[i]) {
return (ordering[i] > other.ordering[i]) ? 1 : -1;
}
}
// considered the same
return 0;
}
/**
* Parse the raw version string into a MigrationVersion.
*/
public static MigrationVersion parse(String raw) {
String value = raw.replace("__",".");
value = value.replace('_','.');
String[] sections = value.split("\\.");
int[] ordering = new int[sections.length];
int stopIndex = 0;
for (int i = 0; i < sections.length; i++) {
try {
ordering[i] = Integer.parseInt(sections[i]);
stopIndex++;
} catch (NumberFormatException e) {
// stop parsing
break;
}
}
int[] actualOrder = new int[stopIndex];
System.arraycopy(ordering, 0, actualOrder, 0, stopIndex);
return new MigrationVersion(raw, actualOrder);
}
}
@@ -1,5 +1,6 @@
package com.avaje.ebean.dbmigration.model;
import com.avaje.ebean.config.DbMigrationConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler;
@@ -24,20 +25,19 @@ public class PlatformDdlWriter {
private final String platformPrefix;
public PlatformDdlWriter(DatabasePlatform platform, ServerConfig serverConfig) {
this(platform, serverConfig, "");
}
private final DbMigrationConfig config;
public PlatformDdlWriter(DatabasePlatform platform, ServerConfig serverConfig, String platformPrefix) {
public PlatformDdlWriter(DatabasePlatform platform, ServerConfig serverConfig, String platformPrefix, DbMigrationConfig config) {
this.platform = platform;
this.serverConfig = serverConfig;
this.platformPrefix = platformPrefix;
this.config = config;
}
/**
* Write the migration as platform specific ddl.
*/
public void processMigration(Migration dbMigration, DdlWrite write, File writePath, int nextMajorVersion) throws IOException {
public void processMigration(Migration dbMigration, DdlWrite write, File writePath, String fullVersion) throws IOException {
DdlHandler handler = handler();
@@ -49,16 +49,16 @@ public class PlatformDdlWriter {
}
handler.generateExtra(write);
writePlatformDdl(write, writePath, nextMajorVersion);
writePlatformDdl(write, writePath, fullVersion);
}
/**
* Write the ddl files.
*/
protected void writePlatformDdl(DdlWrite write, File resourcePath, int migrationVersion) throws IOException {
protected void writePlatformDdl(DdlWrite write, File resourcePath, String fullVersion) throws IOException {
if (!write.isApplyEmpty()) {
FileWriter applyWriter = createWriter(resourcePath, migrationVersion, "apply.sql");
FileWriter applyWriter = createWriter(resourcePath, fullVersion, "", config.getApplySuffix());
try {
writeApplyDdl(applyWriter, write);
applyWriter.flush();
@@ -66,8 +66,8 @@ public class PlatformDdlWriter {
applyWriter.close();
}
if (!write.isApplyRollbackEmpty()) {
FileWriter applyRollbackWriter = createWriter(resourcePath, migrationVersion, "applyRollback.sql");
if (!config.isSuppressRollback() && !write.isApplyRollbackEmpty()) {
FileWriter applyRollbackWriter = createWriter(resourcePath, fullVersion, config.getRollbackPath(), config.getRollbackSuffix());
try {
writeApplyRollbackDdl(applyRollbackWriter, write);
applyRollbackWriter.flush();
@@ -78,7 +78,7 @@ public class PlatformDdlWriter {
}
if (!write.isDropEmpty()) {
FileWriter dropWriter = createWriter(resourcePath, migrationVersion, "drop.sql");
FileWriter dropWriter = createWriter(resourcePath, fullVersion, config.getDropPath(), config.getDropSuffix());
try {
writeDropDdl(dropWriter, write);
dropWriter.flush();
@@ -88,12 +88,28 @@ public class PlatformDdlWriter {
}
}
protected FileWriter createWriter(File resourcePath, int migrationVersion, String suffix) throws IOException {
protected FileWriter createWriter(File path, String fullVersion, String subPath, String suffix) throws IOException {
File applyFile = new File(resourcePath, "v" + migrationVersion + ".0-" + platformPrefix + suffix);
String fileName = fullVersion;
if (!platformPrefix.isEmpty()) {
fileName += "-"+platformPrefix;
}
if (subPath != null && !subPath.isEmpty()) {
path = subPath(path, subPath);
}
fileName += suffix;
File applyFile = new File(path, fileName);
return new FileWriter(applyFile);
}
protected File subPath(File path, String suffix) {
File subPath = new File(path, suffix);
if (!subPath.exists()) {
subPath.mkdirs();
}
return subPath;
}
/**
* Write the 'Apply' DDL buffers to the writer.
*/
@@ -35,6 +35,7 @@ public class ModelBuildBeanVisitor implements BeanVisitor {
}
MTable table = new MTable(descriptor.getBaseTable());
table.setComment(descriptor.getDbComment());
if (descriptor.isHistorySupport()) {
table.setWithHistory(true);
BeanProperty whenCreated = descriptor.findWhenCreatedProperty();
@@ -10,6 +10,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.type.ScalarType;
import java.util.Collection;
import java.util.List;
/**
* The context used during DDL generation.
@@ -134,13 +135,25 @@ public class ModelBuildContext {
/**
* Create the draft table for a given table.
*/
public void createDraft(MTable table) {
public void createDraft(MTable table, boolean draftable) {
MTable draftTable = table.createDraftTable();
draftTable.setPkName(primaryKeyName(draftTable.getName()));
if (draftable) {
// Add a FK from @Draftable live table back to it's draft table)
List<MColumn> pkCols = table.primaryKeyColumns();
if (pkCols.size() == 1) {
// only doing this for single column PK at this stage
MColumn pk = pkCols.get(0);
pk.setReferences(draftTable.getName() + "." + pk.getName());
pk.setForeignKeyName(foreignKeyConstraintName(table.getName(), pk.getName(), 0));
}
}
int fkCount = 0;
int ixCount = 0;
int uqCount = 0;
Collection<MColumn> cols = draftTable.getColumns().values();
for (MColumn col: cols) {
if (col.getForeignKeyName() != null) {
@@ -152,6 +165,13 @@ public class ModelBuildContext {
String[] indexCols = {col.getName()};
col.setForeignKeyIndex(foreignKeyIndexName(draftTable.getName(), indexCols, ++ixCount));
}
// adjust the unique constraint names
if (col.getUnique() != null){
col.setUnique(uniqueConstraintName(draftTable.getName(), col.getName(), ++uqCount));
}
if (col.getUniqueOneToOne() != null){
col.setUniqueOneToOne(uniqueConstraintName(draftTable.getName(), col.getName(), ++uqCount));
}
}
addTable(draftTable);
@@ -44,7 +44,7 @@ public class ModelBuildIntersectionTable {
buildFkConstraints();
if (manyProp.getTargetDescriptor().isDraftable()) {
ctx.createDraft(intersectionTable);
ctx.createDraft(intersectionTable, false);
}
}
@@ -112,7 +112,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
private void addDraftTable() {
if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) {
// create a 'Draft' table which looks very similar (change PK, FK etc)
ctx.createDraft(table);
ctx.createDraft(table, !beanDescriptor.isDraftableElement());
}
}
@@ -230,6 +230,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor {
}
MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p));
col.setComment(p.getDbComment());
col.setDraftOnly(p.isDraftOnly());
if (p.isId()) {
@@ -9,20 +9,6 @@ import java.util.Arrays;
*/
public class ClassUtil {
/**
* Return a new instance of the class using the default constructor.
*/
public static Object newInstance(String className) {
try {
Class<?> cls = Class.forName(className);
return cls.newInstance();
} catch (Exception e) {
String msg = "Error constructing " + className;
throw new IllegalArgumentException(msg, e);
}
}
/**
* Returns the raw type for the 2nd generic parameter for a subclass.
*/
@@ -1,146 +0,0 @@
package com.avaje.ebeaninternal.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wraps the caller and context class loaders.
* <p>
* Helper for ClassUtil.
* </p>
*
* @author rbygrave
*/
class ClassLoadContext {
private static final Logger logger = LoggerFactory.getLogger(ClassLoadContext.class);
private final ClassLoader callerLoader;
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;
}
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;
}
}
@@ -6,79 +6,60 @@ package com.avaje.ebeaninternal.api;
*/
public class ClassUtil {
/**
* Load a class taking into account a context class loader (if present).
*/
public static Class<?> forName(String name, Class<?> caller) throws ClassNotFoundException {
if (caller == null) {
caller = ClassUtil.class;
}
ClassLoadContext ctx = ClassLoadContext.of(caller, true);
return ctx.forName(name);
}
/**
* Return true if javax validation annotations like Size and NotNull are present.
*/
public static boolean isJavaxValidationAnnotationsPresent() {
return isPresent("javax.validation.constraints.NotNull", null);
}
/**
* Return true if Jackson annotations like JsonIgnore are present.
*/
public static boolean isJacksonAnnotationsPresent() {
return isPresent("com.fasterxml.jackson.annotation.JsonIgnore", null);
}
/**
* Return true if Jackson ObjectMapper is present.
*/
public static boolean isJacksonObjectMapperPresent() {
return isPresent("com.fasterxml.jackson.databind.ObjectMapper", null);
}
/**
* Return true if the given class is present.
*/
public static boolean isPresent(String className) {
return isPresent(className, null);
}
/**
* Return true if the given class is present.
*/
public static boolean isPresent(String className, Class<?> caller) {
try {
forName(className, caller);
return true;
} catch (Throwable ex) {
// Class or one of its dependencies is not present...
return false;
}
}
/**
* Return a new instance of the class using the default constructor.
*/
public static Object newInstance(String className) {
return newInstance(className, null);
}
/**
* Return a new instance of the class using the default constructor.
*/
public static Object newInstance(String className, Class<?> caller) {
try {
Class<?> cls = forName(className, caller);
Class<?> cls = forName(className);
return cls.newInstance();
} catch (Exception e) {
String msg = "Error constructing " + className;
throw new IllegalArgumentException(msg, e);
}
}
/**
* Load a class taking into account a context class loader (if present).
*/
public static Class<?> forName(String name) throws ClassNotFoundException {
return new ClassLoadContext().forName(name);
}
/**
* Helper to wrap the context and caller classLoaders (to use/try both).
*/
static class ClassLoadContext {
private final ClassLoader contextLoader;
private final ClassLoader callerLoader;
ClassLoadContext() {
this.callerLoader = ClassUtil.class.getClassLoader();
this.contextLoader = contextLoader();
}
ClassLoader contextLoader() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return (loader != null) ? loader: callerLoader;
}
public Class<?> forName(String name) throws ClassNotFoundException {
try {
return Class.forName(name, true, contextLoader);
} catch (ClassNotFoundException e) {
if (callerLoader == contextLoader) {
throw e;
} else {
return Class.forName(name, true, callerLoader);
}
}
}
}
}
@@ -1,153 +1,153 @@
package com.avaje.ebeaninternal.api;
import java.io.Serializable;
import java.util.Collection;
import java.util.TreeMap;
import java.util.TreeSet;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.el.ElPropertyDeploy;
import com.avaje.ebeaninternal.server.query.SplitName;
import com.avaje.ebeaninternal.server.query.SqlJoinType;
import java.io.Serializable;
import java.util.Collection;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Holds the joins needs to support the many where predicates.
* These joins are independent of any 'fetch' joins on the many.
*/
public class ManyWhereJoins implements Serializable {
private static final long serialVersionUID = -6490181101871795417L;
private final TreeMap<String,PropertyJoin> joins = new TreeMap<String,PropertyJoin>();
private static final long serialVersionUID = -6490181101871795417L;
private StringBuilder formulaProperties = new StringBuilder();
private boolean formulaWithJoin;
private final TreeMap<String, PropertyJoin> joins = new TreeMap<String, PropertyJoin>();
private StringBuilder formulaProperties = new StringBuilder();
private boolean formulaWithJoin;
/**
* 'Mode' indicating that joins added while this is true are required to be outer joins.
*/
private boolean requireOuterJoins;
/**
* Return the current 'mode' indicating if outer joins are currently required or not.
*/
public boolean isRequireOuterJoins() {
return requireOuterJoins;
}
private boolean requireOuterJoins;
/**
* Set the 'mode' to be that joins added are required to be outer joins.
* This is set during the evaluation of disjunction predicates.
*/
public void setRequireOuterJoins(boolean requireOuterJoins) {
this.requireOuterJoins = requireOuterJoins;
}
/**
* Return the current 'mode' indicating if outer joins are currently required or not.
*/
public boolean isRequireOuterJoins() {
return requireOuterJoins;
}
/**
* Add a many where join.
*/
public void add(ElPropertyDeploy elProp) {
String join = elProp.getElPrefix();
BeanProperty p = elProp.getBeanProperty();
if (p instanceof BeanPropertyAssocMany<?>){
join = addManyToJoin(join, p.getName());
}
if (join != null){
addJoin(join);
if (p != null) {
String secondaryTableJoinPrefix = p.getSecondaryTableJoinPrefix();
if (secondaryTableJoinPrefix != null) {
addJoin(join+"."+secondaryTableJoinPrefix);
}
}
addParentJoins(join);
}
}
/**
* For 'many' properties we also need to add the name of the
* many property to get the full logical name of the join.
*/
private String addManyToJoin(String join, String manyPropName){
if (join == null){
return manyPropName;
} else {
return join+"."+manyPropName;
}
}
private void addParentJoins(String join) {
String[] split = SplitName.split(join);
if (split[0] != null){
addJoin(split[0]);
addParentJoins(split[0]);
}
}
/**
* Set the 'mode' to be that joins added are required to be outer joins.
* This is set during the evaluation of disjunction predicates.
*/
public void setRequireOuterJoins(boolean requireOuterJoins) {
this.requireOuterJoins = requireOuterJoins;
}
private void addJoin(String property) {
SqlJoinType joinType = (requireOuterJoins) ? SqlJoinType.OUTER: SqlJoinType.INNER;
joins.put(property, new PropertyJoin(property, joinType));
}
/**
* Return true if there are no extra many where joins.
*/
public boolean isEmpty() {
return joins.isEmpty();
}
/**
* Return the set of many where joins.
*/
public Collection<PropertyJoin> getPropertyJoins() {
return joins.values();
}
/**
* Add a many where join.
*/
public void add(ElPropertyDeploy elProp) {
/**
* Return the set of property names for the many where joins.
*/
public TreeSet<String> getPropertyNames() {
TreeSet<String> propertyNames = new TreeSet<String>();
for (PropertyJoin join : joins.values()) {
propertyNames.add(join.getProperty());
String join = elProp.getElPrefix();
BeanProperty p = elProp.getBeanProperty();
if (p instanceof BeanPropertyAssocMany<?>) {
join = addManyToJoin(join, p.getName());
}
if (join != null) {
addJoin(join);
if (p != null) {
String secondaryTableJoinPrefix = p.getSecondaryTableJoinPrefix();
if (secondaryTableJoinPrefix != null) {
addJoin(join + "." + secondaryTableJoinPrefix);
}
}
return propertyNames;
addParentJoins(join);
}
}
/**
* In findRowCount query found a formula property with a join clause so building a select clause
* specifically for the findRowCount query.
*/
public void addFormulaWithJoin(String propertyName) {
if (formulaWithJoin) {
formulaProperties.append(",");
} else {
formulaProperties = new StringBuilder();
formulaWithJoin = true;
}
formulaProperties.append(propertyName);
/**
* For 'many' properties we also need to add the name of the
* many property to get the full logical name of the join.
*/
private String addManyToJoin(String join, String manyPropName) {
if (join == null) {
return manyPropName;
} else {
return join + "." + manyPropName;
}
public boolean isHasMany() {
return formulaWithJoin || !joins.isEmpty();
}
private void addParentJoins(String join) {
String[] split = SplitName.split(join);
if (split[0] != null) {
addJoin(split[0]);
addParentJoins(split[0]);
}
/**
* Return true if the findRowCount query just needs the id property in the select clause.
*/
public boolean isSelectId() {
return !formulaWithJoin;
}
private void addJoin(String property) {
SqlJoinType joinType = (requireOuterJoins) ? SqlJoinType.OUTER : SqlJoinType.INNER;
joins.put(property, new PropertyJoin(property, joinType));
}
/**
* Return true if there are no extra many where joins.
*/
public boolean isEmpty() {
return joins.isEmpty();
}
/**
* Return the set of many where joins.
*/
public Collection<PropertyJoin> getPropertyJoins() {
return joins.values();
}
/**
* Return the set of property names for the many where joins.
*/
public TreeSet<String> getPropertyNames() {
TreeSet<String> propertyNames = new TreeSet<String>();
for (PropertyJoin join : joins.values()) {
propertyNames.add(join.getProperty());
}
/**
* Return the formula properties to build the select clause for a findRowCount query.
*/
public String getFormulaProperties() {
return formulaProperties.toString();
return propertyNames;
}
/**
* In findRowCount query found a formula property with a join clause so building a select clause
* specifically for the findRowCount query.
*/
public void addFormulaWithJoin(String propertyName) {
if (formulaWithJoin) {
formulaProperties.append(",");
} else {
formulaProperties = new StringBuilder();
formulaWithJoin = true;
}
formulaProperties.append(propertyName);
}
public boolean isHasMany() {
return formulaWithJoin || !joins.isEmpty();
}
/**
* Return true if the findRowCount query just needs the id property in the select clause.
*/
public boolean isSelectId() {
return !formulaWithJoin;
}
/**
* Return the formula properties to build the select clause for a findRowCount query.
*/
public String getFormulaProperties() {
return formulaProperties.toString();
}
}
@@ -7,6 +7,7 @@ import com.avaje.ebean.config.ServerConfig;
*
* author: Richard Vowles - http://gplus.to/RichardVowles
*/
@Deprecated
public interface SpiEbeanPlugin {
/**
@@ -66,11 +66,6 @@ public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionL
*/
PersistenceContextScope getPersistenceContextScope(SpiQuery<?> query);
/**
* Return the DDL generator.
*/
DdlGenerator getDdlGenerator();
/**
* Clear the query execution statistics.
*/
@@ -96,6 +96,11 @@ public interface SpiQuery<T> extends Query<T> {
}
enum TemporalMode {
/**
* Includes soft deletes rows in the result.
*/
SOFT_DELETED,
/**
* Query runs against draft tables.
*/
@@ -183,6 +188,11 @@ public interface SpiQuery<T> extends Query<T> {
*/
boolean isAsDraft();
/**
* Return true if this query includes soft deleted rows.
*/
boolean isIncludeSoftDeletes();
/**
* Return the asOf Timestamp which the query should run as.
*/
@@ -198,6 +208,10 @@ public interface SpiQuery<T> extends Query<T> {
*/
List<String> getAsOfTableAlias();
void addSoftDeletePredicate(String softDeletePredicate);
List<String> getSoftDeletePredicates();
/**
* Return a listener that wants to be notified when the bean collection is
* first used.
@@ -0,0 +1,61 @@
package com.avaje.ebeaninternal.server.autotune.service;
import com.avaje.ebeaninternal.server.autotune.model.Autotune;
import java.util.Collection;
/**
* Event where all tuned query information is collected.
* <p>
* This is for writing the "all" file on shutdown when using runtime tuning.
* </p>
*/
public class AutoTuneAllCollection {
final Autotune document = new Autotune();
final BaseQueryTuner queryTuner;
/**
* Construct to collect/report all tuned queries.
*/
public AutoTuneAllCollection(BaseQueryTuner queryTuner) {
this.queryTuner = queryTuner;
loadAllTuned();
}
/**
* Return the number of origin elements in the document.
*/
public int size() {
return document.getOrigin().size();
}
/**
* Return the Autotune document object.
*/
public Autotune getDocument() {
return document;
}
/**
* Write the document as an xml file.
*/
public void writeFile(String filePrefix) {
AutoTuneXmlWriter writer = new AutoTuneXmlWriter();
writer.write(document, filePrefix);
}
/**
* Loads all the existing query tuning into the document.
*/
private void loadAllTuned() {
Collection<TunedQueryInfo> all = queryTuner.getAll();
for (TunedQueryInfo tuned: all) {
document.getOrigin().add(tuned.getOrigin());
}
}
}
@@ -0,0 +1,164 @@
package com.avaje.ebeaninternal.server.autotune.service;
import com.avaje.ebean.bean.ObjectGraphOrigin;
import com.avaje.ebeaninternal.server.autotune.AutoTuneCollection;
import com.avaje.ebeaninternal.server.autotune.model.Autotune;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import com.avaje.ebeaninternal.server.autotune.model.ProfileDiff;
import com.avaje.ebeaninternal.server.autotune.model.ProfileNew;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
/**
* Event where profiling information is collected and processed for differences
* relative to the current query tuning.
*/
public class AutoTuneDiffCollection {
final Autotune document = new Autotune();
final AutoTuneCollection profiling;
final BaseQueryTuner queryTuner;
final boolean updateTuning;
int newCount;
int diffCount;
/**
* Construct to collect/report the new/diff query tuning entries.
*/
public AutoTuneDiffCollection(AutoTuneCollection profiling, BaseQueryTuner queryTuner, boolean updateTuning) {
this.profiling = profiling;
this.queryTuner = queryTuner;
this.updateTuning = updateTuning;
}
/**
* Return true if there are no new or diff entries.
*/
public boolean isEmpty() {
return newCount == 0 && diffCount == 0;
}
/**
* Return the underlying Autotune document object.
*/
public Autotune getDocument() {
return document;
}
/**
* Return the number of diff entries.
*/
public int getDiffCount() {
return diffCount;
}
/**
* Return the number of new entries.
*/
public int getNewCount() {
return newCount;
}
/**
* Return the total new and diff entries.
*/
public int getChangeCount() {
return newCount + diffCount;
}
/**
* Write the underlying document as an xml file.
*/
public void writeFile(String filePrefix) {
AutoTuneXmlWriter writer = new AutoTuneXmlWriter();
writer.write(document, filePrefix);
}
/**
* Process checking profiling entries against existing query tuning.
*/
public boolean process() {
for (AutoTuneCollection.Entry entry : profiling.getEntries()) {
addToDocument(entry);
}
return isEmpty();
}
/**
* Check if the entry is new or diff and add as necessary.
*/
private void addToDocument(AutoTuneCollection.Entry entry) {
ObjectGraphOrigin point = entry.getOrigin();
OrmQueryDetail profileDetail = entry.getDetail();
// compare with the existing query tuning entry
OrmQueryDetail tuneDetail = queryTuner.get(point.getKey());
if (tuneDetail == null) {
addToDocumentNewEntry(entry, point);
} else if (!tuneDetail.isAutoTuneEqual(profileDetail)) {
addToDocumentDiffEntry(entry, point, tuneDetail);
}
}
/**
* Add as a diff entry.
*/
private void addToDocumentDiffEntry(AutoTuneCollection.Entry entry, ObjectGraphOrigin point, OrmQueryDetail tuneDetail) {
diffCount++;
Origin origin = createOrigin(entry, point, tuneDetail.toString());
ProfileDiff diff = document.getProfileDiff();
if (diff == null) {
diff = new ProfileDiff();
document.setProfileDiff(diff);
}
diff.getOrigin().add(origin);
}
/**
* Add as a "new" entry.
*/
private void addToDocumentNewEntry(AutoTuneCollection.Entry entry, ObjectGraphOrigin point) {
newCount++;
ProfileNew profileNew = document.getProfileNew();
if (profileNew == null) {
profileNew = new ProfileNew();
document.setProfileNew(profileNew);
}
Origin origin = createOrigin(entry, point, entry.getOriginalQuery());
profileNew.getOrigin().add(origin);
}
/**
* Create the XML Origin bean for the given entry and ObjectGraphOrigin.
*/
private Origin createOrigin(AutoTuneCollection.Entry entry, ObjectGraphOrigin point, String query) {
Origin origin = new Origin();
origin.setKey(point.getKey());
origin.setBeanType(point.getBeanType());
origin.setDetail(entry.getDetail().toString());
origin.setCallStack(point.getCallStack().description("\n"));
origin.setOriginal(query);
if (updateTuning) {
queryTuner.put(origin);
}
return origin;
}
}
@@ -7,12 +7,29 @@ import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Simple writer for output of the AutoTune Profiling as an XML document.
*/
public class AutoTuneXmlWriter {
/**
* Write the document as xml file with the given prefix.
*/
public void write(Autotune document, String filePrefix) {
SortAutoTuneDocument.sort(document);
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
String now = df.format(new Date());
// write the file with serverName and now suffix as we can output the profiling many times
File file = new File(filePrefix + "-" + now + ".xml");
write(document, file);
}
/**
* Write Profiling to a file as xml.
*/
@@ -7,9 +7,11 @@ import com.avaje.ebean.config.AutoTuneMode;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.api.SpiQuery;
import com.avaje.ebeaninternal.server.autotune.ProfilingListener;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
import javax.persistence.PersistenceException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -48,6 +50,21 @@ public class BaseQueryTuner {
this.skipAll = !queryTuning && !profiling;
}
/**
* Return all the current tuned query entries.
*/
public Collection<TunedQueryInfo> getAll() {
return tunedQueryInfoMap.values();
}
/**
* Put a query tuning entry.
*/
public void put(Origin origin) {
tunedQueryInfoMap.put(origin.getKey(), new TunedQueryInfo(origin));
}
/**
* Load the tuned query information.
*/
@@ -1,6 +1,5 @@
package com.avaje.ebeaninternal.server.autotune.service;
import com.avaje.ebean.bean.ObjectGraphOrigin;
import com.avaje.ebean.config.AutoTuneConfig;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
@@ -9,24 +8,11 @@ import com.avaje.ebeaninternal.server.autotune.AutoTuneCollection;
import com.avaje.ebeaninternal.server.autotune.AutoTuneService;
import com.avaje.ebeaninternal.server.autotune.model.Autotune;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import com.avaje.ebeaninternal.server.autotune.model.ProfileDiff;
import com.avaje.ebeaninternal.server.autotune.model.ProfileEmpty;
import com.avaje.ebeaninternal.server.autotune.model.ProfileNew;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetailParser;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.TimeUnit;
/**
* Implementation of the AutoTuneService which is comprised of profiling and query tuning.
@@ -35,6 +21,8 @@ public class DefaultAutoTuneService implements AutoTuneService {
private static final Logger logger = LoggerFactory.getLogger(DefaultAutoTuneService.class);
private final SpiEbeanServer server;
private final long defaultGarbageCollectionWait;
private final boolean skipCollectionOnShutdown;
@@ -53,14 +41,20 @@ public class DefaultAutoTuneService implements AutoTuneService {
private final String serverName;
private final int profilingUpdateFrequency;
private long runtimeChangeCount;
public DefaultAutoTuneService(SpiEbeanServer server, ServerConfig serverConfig) {
AutoTuneConfig config = serverConfig.getAutoTuneConfig();
this.server = server;
this.queryTuning = config.isQueryTuning();
this.profiling = config.isProfiling();
this.tuningFile = config.getQueryTuningFile();
this.profilingFile = config.getProfilingFile();
this.profilingUpdateFrequency = config.getProfilingUpdateFrequency();
this.serverName = server.getName();
this.profileManager = new ProfileManager(config, server);
this.queryTuner = new BaseQueryTuner(config, server, profileManager);
@@ -75,169 +69,120 @@ public class DefaultAutoTuneService implements AutoTuneService {
public void startup() {
if (queryTuning) {
File file = new File(tuningFile);
if (!file.exists()) {
logger.warn("AutoTune file {} not found - no automatic tuning will be applied", file.getAbsolutePath());
} else {
AutoTuneXmlReader reader = new AutoTuneXmlReader();
Autotune profiling = reader.read(file);
logger.info("AutoTune loading {} tuning entries", profiling.getOrigin().size());
for (Origin origin : profiling.getOrigin()) {
queryTuner.load(origin.getKey(), createTunedQueryInfo(origin));
}
loadTuningFile();
if (isRuntimeTuningUpdates()) {
// periodically gather and update query tuning
server.getBackgroundExecutor().executePeriodically(new ProfilingUpdate(), profilingUpdateFrequency, TimeUnit.SECONDS);
}
}
}
@NotNull
private TunedQueryInfo createTunedQueryInfo(Origin origin) {
OrmQueryDetail detail = new OrmQueryDetailParser(origin.getDetail()).parse();
return new TunedQueryInfo(detail);
/**
* Return true if the tuning should update periodically at runtime.
*/
private boolean isRuntimeTuningUpdates() {
return profilingUpdateFrequency > 0;
}
private void saveProfiling(boolean reset) {
private class ProfilingUpdate implements Runnable {
Autotune document = new Autotune();
AutoTuneCollection autoTuneCollection = profileManager.profilingCollection(reset);
List<AutoTuneCollection.Entry> entries = autoTuneCollection.getEntries();
// count "new" and "diff" profiling entries
AtomicInteger newCounter = new AtomicInteger();
AtomicInteger diffCounter = new AtomicInteger();
Set<String> profileKeys = new HashSet<String>();
for (AutoTuneCollection.Entry entry : entries) {
saveProfilingEntry(document, entry, newCounter, diffCounter);
profileKeys.add(entry.getOrigin().getKey());
@Override
public void run() {
runtimeTuningUpdate();
}
}
// report the origin keys that we didn't collect any profiling on
Set<String> tunerKeys = queryTuner.keySet();
for (String tuneKey : tunerKeys) {
if (!profileKeys.contains(tuneKey)) {
ProfileEmpty profileEmpty = document.getProfileEmpty();
if (profileEmpty == null) {
profileEmpty = new ProfileEmpty();
document.setProfileEmpty(profileEmpty);
}
Origin emptyOrigin = new Origin();
emptyOrigin.setKey(tuneKey);
profileEmpty.getOrigin().add(emptyOrigin);
}
}
int totalNew = newCounter.get();
int totalDiff = diffCounter.get();
if (totalNew == 0 && totalDiff == 0) {
logger.info("No new or diff entries for profiling server:{}", serverName);
/**
* Load tuning information from an existing tuning file.
*/
private void loadTuningFile() {
File file = new File(tuningFile);
if (!file.exists()) {
logger.warn("AutoTune file {} not found - no initial automatic query tuning", file.getAbsolutePath());
} else {
sortDocument(document);
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
String now = df.format(new Date());
// write the file with serverName and now suffix as we can output the profiling many times
File file = new File(profilingFile + "-" + serverName + "-" + now + ".xml");
AutoTuneXmlWriter writer = new AutoTuneXmlWriter();
writer.write(document, file);
logger.info("writing new:{} diff:{} profiling entries for server:{}", totalNew, totalDiff, serverName);
}
}
/**
* Set the diff and new entries by bean type followed by key.
*/
private void sortDocument(Autotune document) {
ProfileDiff profileDiff = document.getProfileDiff();
if (profileDiff != null) {
Collections.sort(profileDiff.getOrigin(), new OriginNameKeySort());
}
ProfileNew profileNew = document.getProfileNew();
if (profileNew != null) {
Collections.sort(profileNew.getOrigin(), new OriginNameKeySort());
}
ProfileEmpty profileEmpty = document.getProfileEmpty();
if (profileEmpty != null) {
Collections.sort(profileEmpty.getOrigin(), new OriginKeySort());
}
}
/**
* Comparator sort by bean type then key.
*/
class OriginNameKeySort implements Comparator<Origin> {
@Override
public int compare(Origin o1, Origin o2) {
int comp = o1.getBeanType().compareTo(o2.getBeanType());
if (comp == 0) {
comp = o1.getKey().compareTo(o2.getKey());
AutoTuneXmlReader reader = new AutoTuneXmlReader();
Autotune profiling = reader.read(file);
logger.info("AutoTune loading {} tuning entries", profiling.getOrigin().size());
for (Origin origin : profiling.getOrigin()) {
queryTuner.put(origin);
}
return comp;
}
}
/**
* Comparator sort by bean type then key.
* Collect profiling, check for new/diff to existing tuning and apply changes.
*/
class OriginKeySort implements Comparator<Origin> {
private void runtimeTuningUpdate() {
@Override
public int compare(Origin o1, Origin o2) {
return o1.getKey().compareTo(o2.getKey());
synchronized (this) {
try {
long start = System.currentTimeMillis();
AutoTuneCollection profiling = profileManager.profilingCollection(false);
AutoTuneDiffCollection event = new AutoTuneDiffCollection(profiling, queryTuner, true);
if (event.process()) {
long exeMillis = System.currentTimeMillis() - start;
logger.debug("No query tuning updates for server:{} executionMillis:{}", serverName, exeMillis);
} else {
// report the query tuning changes that have been made
runtimeChangeCount += event.getChangeCount();
event.writeFile(profilingFile + "-" + serverName + "-update");
long exeMillis = System.currentTimeMillis() - start;
logger.info("query tuning updates - new:{} diff:{} for server:{} executionMillis:{}", event.getNewCount(), event.getDiffCount(), serverName, exeMillis);
}
} catch (Throwable e) {
logger.error("Error collecting or applying automatic query tuning", e);
}
}
}
private void saveProfilingEntry(Autotune document, AutoTuneCollection.Entry entry, AtomicInteger newCount, AtomicInteger diffCount) {
private void saveProfilingOnShutdown(boolean reset) {
ObjectGraphOrigin point = entry.getOrigin();
OrmQueryDetail profileDetail = entry.getDetail();
synchronized (this) {
if (isRuntimeTuningUpdates()) {
runtimeTuningUpdate();
outputAllTuning();
// compare with the existing query tuning entry
OrmQueryDetail tuneDetail = queryTuner.get(point.getKey());
if (tuneDetail == null) {
// New entry
newCount.incrementAndGet();
ProfileNew profileNew = document.getProfileNew();
if (profileNew == null) {
profileNew = new ProfileNew();
document.setProfileNew(profileNew);
} else {
AutoTuneCollection profiling = profileManager.profilingCollection(reset);
AutoTuneDiffCollection event = new AutoTuneDiffCollection(profiling, queryTuner, false);
if (!event.process()) {
logger.info("No new or diff entries for profiling server:{}", serverName);
} else {
event.writeFile(profilingFile + "-" + serverName);
logger.info("writing new:{} diff:{} profiling entries for server:{}", event.getNewCount(), event.getDiffCount(), serverName);
}
}
Origin origin = createOrigin(entry, point);
origin.setOriginal(entry.getOriginalQuery());
profileNew.getOrigin().add(origin);
} else if (!tuneDetail.isAutoTuneEqual(profileDetail)) {
// Diff entry
diffCount.incrementAndGet();
Origin origin = createOrigin(entry, point);
origin.setOriginal(tuneDetail.toString());
ProfileDiff diff = document.getProfileDiff();
if (diff == null) {
diff = new ProfileDiff();
document.setProfileDiff(diff);
}
diff.getOrigin().add(origin);
}
}
/**
* Create the XML Origin bean for the given entry and ObjectGraphOrigin.
* Output all the query tuning (the "all" file).
* <p>
* This is the originally loaded tuning plus any tuning changes picked up and applied at runtime.
* </p>
* <p>
* This "all" file can be used as the next "ebean-autotune.xml" file.
* </p>
*/
@NotNull
private Origin createOrigin(AutoTuneCollection.Entry entry, ObjectGraphOrigin point) {
Origin origin = new Origin();
origin.setKey(point.getKey());
origin.setBeanType(point.getBeanType());
origin.setDetail(entry.getDetail().toString());
origin.setCallStack(point.getCallStack().description("\n"));
return origin;
private void outputAllTuning() {
if (runtimeChangeCount == 0) {
logger.info("no runtime query tuning changes for server:{}", serverName);
} else {
AutoTuneAllCollection event = new AutoTuneAllCollection(queryTuner);
int size = event.size();
event.writeFile(profilingFile + "-" + serverName + "-all");
logger.info("query tuning detected [{}] changes, writing all [{}] tuning entries for server:{}", runtimeChangeCount, size, serverName);
}
}
/**
@@ -252,7 +197,7 @@ public class DefaultAutoTuneService implements AutoTuneService {
public void shutdown() {
if (profiling && !skipCollectionOnShutdown) {
collectProfiling(-1);
saveProfiling(false);
saveProfilingOnShutdown(false);
}
}
@@ -118,7 +118,6 @@ public class ProfileManager implements ProfilingListener {
AutoTuneCollection req = new AutoTuneCollection();
for (ProfileOrigin origin : profileMap.values()) {
BeanDescriptor<?> desc = server.getBeanDescriptorById(origin.getOrigin().getBeanType());
if (desc != null) {
origin.profilingCollection(desc, req, reset);
@@ -0,0 +1,71 @@
package com.avaje.ebeaninternal.server.autotune.service;
import com.avaje.ebeaninternal.server.autotune.model.Autotune;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import com.avaje.ebeaninternal.server.autotune.model.ProfileDiff;
import com.avaje.ebeaninternal.server.autotune.model.ProfileEmpty;
import com.avaje.ebeaninternal.server.autotune.model.ProfileNew;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Sorts Autotune document by
*/
public class SortAutoTuneDocument {
/**
* Set the diff and new entries by bean type followed by key.
*/
public static void sort(Autotune document) {
ProfileDiff profileDiff = document.getProfileDiff();
if (profileDiff != null) {
Collections.sort(profileDiff.getOrigin(), NAME_KEY_SORT);
}
ProfileNew profileNew = document.getProfileNew();
if (profileNew != null) {
Collections.sort(profileNew.getOrigin(), NAME_KEY_SORT);
}
ProfileEmpty profileEmpty = document.getProfileEmpty();
if (profileEmpty != null) {
Collections.sort(profileEmpty.getOrigin(), KEY_SORT);
}
List<Origin> origins = document.getOrigin();
if (!origins.isEmpty()) {
Collections.sort(origins, NAME_KEY_SORT);
}
}
private static final OriginNameKeySort NAME_KEY_SORT = new OriginNameKeySort();
private static final OriginKeySort KEY_SORT = new OriginKeySort();
/**
* Comparator sort by bean type then key.
*/
private static class OriginNameKeySort implements Comparator<Origin> {
@Override
public int compare(Origin o1, Origin o2) {
int comp = o1.getBeanType().compareTo(o2.getBeanType());
if (comp == 0) {
comp = o1.getKey().compareTo(o2.getKey());
}
return comp;
}
}
/**
* Comparator sort by bean type then key.
*/
private static class OriginKeySort implements Comparator<Origin> {
@Override
public int compare(Origin o1, Origin o2) {
return o1.getKey().compareTo(o2.getKey());
}
}
}
@@ -1,7 +1,9 @@
package com.avaje.ebeaninternal.server.autotune.service;
import com.avaje.ebeaninternal.api.SpiQuery;
import com.avaje.ebeaninternal.server.autotune.model.Origin;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetailParser;
import java.io.Serializable;
@@ -11,10 +13,20 @@ import java.io.Serializable;
*/
public class TunedQueryInfo implements Serializable {
private final Origin origin;
private final OrmQueryDetail tunedDetail;
public TunedQueryInfo(OrmQueryDetail tunedDetail) {
this.tunedDetail = tunedDetail;
public TunedQueryInfo(Origin origin) {
this.origin = origin;
this.tunedDetail = new OrmQueryDetailParser(origin.getDetail()).parse();
}
/**
* Return the origin entry (includes call stack and bean type).
*/
public Origin getOrigin() {
return origin;
}
/**
@@ -401,8 +401,10 @@ public class DefaultBeanLoader {
ebi.setPersistenceContext(pc);
}
boolean draft = desc.isDraftInstance(bean);
if (embeddedOwnerIndex == -1) {
if (SpiQuery.Mode.LAZYLOAD_BEAN.equals(mode) && desc.isBeanCaching()) {
if (!draft && SpiQuery.Mode.LAZYLOAD_BEAN.equals(mode) && desc.isBeanCaching()) {
// lazy loading and the bean cache is active
if (desc.cacheBeanLoad(bean, ebi, id)) {
return;
@@ -415,10 +417,12 @@ public class DefaultBeanLoader {
SpiQuery<?> query = (SpiQuery<?>) server.createQuery(desc.getBeanType());
query.setLazyLoadProperty(ebi.getLazyLoadProperty());
if (draft) {
query.asDraft();
}
if (embeddedOwnerIndex > -1) {
String embeddedBeanPropertyName = ebi.getProperty(embeddedOwnerIndex);
query.select("id," + embeddedBeanPropertyName);
query.select("id," + ebi.getProperty(embeddedOwnerIndex));
}
// don't collect AutoTune usage profiling information
@@ -20,6 +20,7 @@ import com.avaje.ebeaninternal.server.cluster.ClusterManager;
import com.avaje.ebeaninternal.server.lib.ShutdownManager;
import com.avaje.ebeaninternal.server.lib.sql.DataSourceAlert;
import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool;
import com.avaje.ebeaninternal.server.lib.sql.DataSourcePoolListener;
import com.avaje.ebeaninternal.server.lib.sql.SimpleDataSourceAlert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,8 +47,6 @@ public class DefaultContainer implements SpiContainer {
private final JndiDataSourceLookup jndiDataSourceFactory;
// private final AtomicInteger serverId = new AtomicInteger(1);
public DefaultContainer(ContainerConfig containerConfig) {
this.clusterManager = new ClusterManager(containerConfig);
@@ -115,7 +114,6 @@ public class DefaultContainer implements SpiContainer {
ServerCacheManager cacheManager = getCacheManager(serverConfig);
// int uniqueServerId = serverId.incrementAndGet();
SpiBackgroundExecutor bgExecutor = createBackgroundExecutor(serverConfig);
XmlConfigLoader xmlConfigLoader = new XmlConfigLoader(null);
@@ -127,19 +125,6 @@ public class DefaultContainer implements SpiContainer {
cacheManager.init(server);
// if (serverConfig.isRegisterJmxMBeans()) {
// MBeanServer mbeanServer;
// ArrayList<?> list = MBeanServerFactory.findMBeanServer(null);
// if (list.size() == 0) {
// // probably not running in a server
// mbeanServer = MBeanServerFactory.createMBeanServer();
// } else {
// // use the first MBeanServer
// mbeanServer = (MBeanServer) list.get(0);
// }
// server.registerMBeans(mbeanServer, uniqueServerId);
// }
// generate and run DDL if required
// if there are any other tasks requiring action in their plugins, do them as well
if (!DbOffline.isRunningMigration()) {
@@ -311,7 +296,17 @@ public class DefaultContainer implements SpiContainer {
}
DataSourceAlert notify = new SimpleDataSourceAlert();
return new DataSourcePool(notify, config.getName(), dsConfig);
DataSourcePoolListener listener = createListener(config, dsConfig);
return new DataSourcePool(notify, config.getName(), dsConfig, listener);
}
/**
* Create and return a DataSourcePoolListener if it has been specified.
*/
private DataSourcePoolListener createListener(ServerConfig config, DataSourceConfig dsConfig) {
String poolListener = dsConfig.getPoolListener();
return poolListener != null ? (DataSourcePoolListener) config.getClassLoadConfig().newInstance(poolListener) : null;
}
/**
@@ -9,6 +9,7 @@ import com.avaje.ebean.bean.ObjectGraphNode;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.bean.PersistenceContext.WithOption;
import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebean.config.DbMigrationConfig;
import com.avaje.ebean.config.EncryptKeyManager;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
@@ -70,6 +71,7 @@ import com.avaje.ebeaninternal.util.ParamTypeHelper.TypeInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.NonUniqueResultException;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
@@ -140,7 +142,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
private final List<SpiServerPlugin> serverPlugins;
private DdlGenerator ddlGenerator;
private final DdlGenerator ddlGenerator;
private final ExpressionFactory expressionFactory;
@@ -233,6 +235,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
this.beanLoader = new DefaultBeanLoader(this);
this.jsonContext = config.createJsonContext(this);
this.serverPlugins = config.getPlugins();
this.ddlGenerator = new DdlGenerator(this, serverConfig);
// load normal plugins late and call setup on all
loadAndInitializePlugins(config.getServerConfig());
@@ -259,18 +262,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
for (SpiEbeanPlugin plugin : ServiceLoader.load(SpiEbeanPlugin.class)) {
spiPlugins.add(plugin);
plugin.setup(this, config);
if (plugin instanceof DdlGenerator) {
// backwards compatible
ddlGenerator = (DdlGenerator) plugin;
}
}
if (ddlGenerator == null) {
// ServiceLoader not finding ddlGenerator (typically OSGi)
ddlGenerator = new DdlGenerator();
spiPlugins.add(ddlGenerator);
ddlGenerator.setup(this, config);
}
ebeanPlugins = Collections.unmodifiableList(spiPlugins);
@@ -287,6 +278,9 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
* Execute all the plugins with an online flag indicating the DB is up or not.
*/
public void executePlugins(boolean online) {
ddlGenerator.execute(online);
for (SpiEbeanPlugin plugin : ebeanPlugins) {
plugin.execute(online);
}
@@ -335,10 +329,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return expressionFactory;
}
public DdlGenerator getDdlGenerator() {
return ddlGenerator;
}
@Override
public AutoTune getAutoTune() {
return autoTuneService;
@@ -372,6 +362,10 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
* Start any services after registering with the ClusterManager.
*/
public void start() {
DbMigrationConfig migrationConfig = serverConfig.getMigrationConfig();
if (migrationConfig != null) {
migrationConfig.generateOnStart(this);
}
}
/**
@@ -1218,13 +1212,16 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
// a query that is expected to return either 0 or 1 rows
List<T> list = findList(query, t);
return extractUnique(list);
}
if (list.size() == 0) {
private <T> T extractUnique(List<T> list) {
if (list.isEmpty()) {
return null;
} else if (list.size() > 1) {
throw new PersistenceException("Unique expecting 0 or 1 rows but got [" + list.size() + "]");
throw new NonUniqueResultException("Unique expecting 0 or 1 results but got [" + list.size() + "]");
} else {
return list.get(0);
}
@@ -1380,6 +1377,18 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return new LimitOffsetPagedList<T>(this, (SpiQuery<T>)query, pageIndex, pageSize);
}
@Override
public <T> PagedList<T> findPagedList(Query<T> query, Transaction transaction) {
SpiQuery<T> spiQuery = (SpiQuery<T>)query;
int maxRows = spiQuery.getMaxRows();
if (maxRows == 0) {
throw new PersistenceException("maxRows must be specified for findPagedList() query");
}
return new LimitOffsetPagedList<T>(this, spiQuery);
}
public <T> void findEach(Query<T> query, QueryEachConsumer<T> consumer, Transaction t) {
SpiOrmQueryRequest<T> request = createQueryRequest(Type.ITERATE, query, t);
@@ -1449,17 +1458,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
// no findId() method for SqlQuery...
// a query that is expected to return either 0 or 1 rows
List<SqlRow> list = findList(query, t);
if (list.size() == 0) {
return null;
} else if (list.size() > 1) {
String m = "Unique expecting 0 or 1 rows but got [" + list.size() + "]";
throw new PersistenceException(m);
} else {
return list.get(0);
}
return extractUnique(list);
}
public SqlFutureList findFutureList(SqlQuery query, Transaction t) {
@@ -1883,9 +1882,28 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
/**
* Delete the bean with the explicit transaction.
*/
public boolean delete(Object bean, Transaction t) {
return persister.delete(checkEntityBean(bean), t);
public boolean delete(Object bean, Transaction t) throws OptimisticLockException {
return persister.delete(checkEntityBean(bean), t, false);
}
@Override
public boolean deletePermanent(Object bean) throws OptimisticLockException {
return deletePermanent(bean, null);
}
@Override
public boolean deletePermanent(Object bean, Transaction t) throws OptimisticLockException {
return persister.delete(checkEntityBean(bean), t, true);
}
@Override
public int deleteAllPermanent(Collection<?> beans) {
return deleteAllInternal(beans.iterator(), null, true);
}
@Override
public int deleteAllPermanent(Collection<?> beans, Transaction t) {
return deleteAllInternal(beans.iterator(), t, true);
}
/**
@@ -1893,7 +1911,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
*/
@Override
public int deleteAll(Collection<?> beans) {
return deleteAllInternal(beans.iterator(), null);
return deleteAllInternal(beans.iterator(), null, false);
}
/**
@@ -1901,13 +1919,13 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
*/
@Override
public int deleteAll(Collection<?> beans, Transaction t) {
return deleteAllInternal(beans.iterator(), t);
return deleteAllInternal(beans.iterator(), t, false);
}
/**
* Delete all the beans in the iterator with an explicit transaction.
*/
private int deleteAllInternal(Iterator<?> it, Transaction t) {
private int deleteAllInternal(Iterator<?> it, Transaction t, boolean permanent) {
TransWrapper wrap = initTransIfRequired(t);
@@ -1917,7 +1935,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
int deleteCount = 0;
while (it.hasNext()) {
EntityBean bean = checkEntityBean(it.next());
persister.delete(bean, trans);
persister.delete(bean, trans, permanent);
deleteCount++;
}
@@ -54,6 +54,7 @@ import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
/**
* Used to extend the ServerConfig with additional objects used to configure and
@@ -165,6 +166,14 @@ public class InternalConfiguration {
* Return the list of plugins we collected during construction.
*/
public List<SpiServerPlugin> getPlugins() {
// find additional plugins via ServiceLoader ...
for (SpiServerPlugin plugin : ServiceLoader.load(SpiServerPlugin.class)) {
if (!plugins.contains(plugin)) {
plugins.add(plugin);
}
}
return plugins;
}
@@ -357,6 +366,6 @@ public class InternalConfiguration {
}
public GeneratedPropertyFactory getGeneratedPropertyFactory() {
return new GeneratedPropertyFactory(serverConfig.getCurrentUserProvider());
return new GeneratedPropertyFactory(serverConfig);
}
}
@@ -12,7 +12,7 @@ import com.avaje.ebeaninternal.server.persist.PersistExecute;
public abstract class PersistRequest extends BeanRequest implements BatchPostExecute {
public enum Type {
INSERT, UPDATE, DELETE, UPDATESQL, CALLABLESQL
INSERT, UPDATE, DELETE, SOFT_DELETE, DELETE_PERMANENT, UPDATESQL, CALLABLESQL
}
protected boolean persistCascade;
@@ -21,6 +21,7 @@ import com.avaje.ebeaninternal.server.persist.PersistExecute;
import com.avaje.ebeaninternal.server.transaction.BeanPersistIdMap;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -150,12 +151,29 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept);
this.publish = publish;
if (!publish && beanDescriptor.isDraftable()) {
if (isMarkDraftDirty(publish)) {
beanDescriptor.setDraftDirty(entityBean, true);
}
this.dirty = intercept.isDirty();
}
/**
* Return true if the draftDirty property should be set to true for this request.
*/
private boolean isMarkDraftDirty(boolean publish) {
return !publish && type != Type.DELETE && beanDescriptor.isDraftable();
}
/**
* Set the transaction from prior persist request.
* Only used when hard deleting draft & associated live beans.
*/
public void setTrans(SpiTransaction transaction) {
this.transaction = transaction;
this.createdTransaction = false;
this.persistCascade = transaction.isPersistCascade();
}
/**
* Init the transaction and also check for batch on cascade escalation.
*/
@@ -256,6 +274,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
beanDescriptor.cacheHandleUpdate(idValue, this);
break;
case DELETE:
case SOFT_DELETE:
// Bean deleted from cache early via postDelete()
break;
default:
@@ -435,6 +454,31 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
return beanDescriptor.isDraftable();
}
/**
* Return true if this request is a hard delete of a draftable bean.
* If this is true Ebean is expected to auto-publish and delete the associated live bean.
*/
public boolean isHardDeleteDraft() {
if (type == Type.DELETE && beanDescriptor.isDraftable() && !beanDescriptor.isDraftableElement()) {
// deleting a top level draftable bean
if (beanDescriptor.isLiveInstance(entityBean)) {
throw new PersistenceException("Explicit Delete is not allowed on a 'live' bean - only draft beans");
}
return true;
}
return false;
}
/**
* Checks for @Draftable entity beans with @Draft property that the bean is a 'draft'.
* Save or Update is not allowed to execute using 'live' beans - must use publish().
*/
public void checkDraft() {
if (beanDescriptor.isDraftable() && beanDescriptor.isLiveInstance(entityBean)) {
throw new PersistenceException("Save or update is not allowed on a 'live' bean - only draft beans");
}
}
/**
* Return the parent bean for cascading save with unidirectional relationship.
*/
@@ -479,6 +523,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
persistExecute.executeUpdateBean(this);
return -1;
case SOFT_DELETE:
prepareForSoftDelete();
persistExecute.executeUpdateBean(this);
return -1;
case DELETE:
return persistExecute.executeDeleteBean(this);
@@ -487,6 +536,14 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
}
/**
* Soft delete is executed as update so we want to set deleted=true property.
*/
private void prepareForSoftDelete() {
beanDescriptor.setSoftDeleteValue(entityBean);
}
@Override
public int executeOrQueue() {
@@ -533,6 +590,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
switch (type) {
case DELETE:
case SOFT_DELETE:
postDelete();
break;
case UPDATE:
@@ -601,6 +659,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
controller.postUpdate(this);
break;
case DELETE:
case SOFT_DELETE:
controller.postDelete(this);
break;
default:
@@ -622,6 +681,9 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
case DELETE:
transaction.logSummary("Deleted [" + name + "] [" + idValue + "]" + draft);
break;
case SOFT_DELETE:
transaction.logSummary("SoftDelete [" + name + "] [" + idValue + "]" + draft);
break;
default:
break;
}
@@ -692,6 +754,9 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
intercept.setLoadedProperty(i);
}
beanDescriptor.setEmbeddedOwner(entityBean);
if (!publish) {
beanDescriptor.setDraft(entityBean);
}
}
public boolean isReference() {
@@ -801,4 +866,12 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
public String getUpdateTable() {
return publish ? beanDescriptor.getBaseTable() : beanDescriptor.getDraftTable();
}
/**
* Return true if this is a soft delete request.
*/
public boolean isSoftDelete() {
return Type.SOFT_DELETE == type;
}
}
@@ -66,7 +66,7 @@ public interface Persister {
/**
* Delete the bean.
*/
boolean delete(EntityBean entityBean, Transaction t);
boolean delete(EntityBean entityBean, Transaction t, boolean permanent);
/**
* Delete multiple beans given a collection of Id values.
@@ -12,6 +12,7 @@ import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.bean.EntityBeanIntercept;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.config.EncryptKey;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.IdGenerator;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.event.BeanFindController;
@@ -149,8 +150,16 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final String baseTableVersionsBetween;
private final boolean historySupport;
private final BeanProperty softDeleteProperty;
private final boolean softDelete;
private final String draftTable;
/**
* DB table comment.
*/
private final String dbComment;
/**
* Set to true if read auditing is on for this bean type.
*/
@@ -160,6 +169,8 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private final boolean draftableElement;
private final BeanProperty draft;
private final BeanProperty draftDirty;
/**
@@ -316,8 +327,9 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
private String idBinderIdSql;
private String deleteByIdSql;
private String deleteByIdInSql;
private String softDeleteByIdSql;
private String softDeleteByIdInSql;
private final String name;
@@ -390,13 +402,17 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
this.baseTable = InternString.intern(deploy.getBaseTable());
this.baseTableAsOf = deploy.getBaseTableAsOf();
this.baseTableVersionsBetween = deploy.getBaseTableVersionsBetween();
this.dbComment = deploy.getDbComment();
this.autoTunable = EntityType.ORM.equals(entityType) && (beanFinder == null);
// helper object used to derive lists of properties
DeployBeanPropertyLists listHelper = new DeployBeanPropertyLists(owner, this, deploy);
this.softDeleteProperty = listHelper.getSoftDeleteProperty();
this.softDelete = (softDeleteProperty != null);
this.idProperty = listHelper.getId();
this.versionProperty = listHelper.getVersionProperty();
this.draft = listHelper.getDraft();
this.draftDirty = listHelper.getDraftDirty();
this.propMap = listHelper.getPropertyMap();
this.propertiesTransient = listHelper.getTransients();
@@ -500,6 +516,13 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
}
}
/**
* Return the ServerConfig.
*/
public ServerConfig getServerConfig() {
return owner.getServerConfig();
}
/**
* Set the server. Primarily so that the Many's can lazy load.
*/
@@ -621,6 +644,14 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
deleteByIdSql = "delete from " + baseTable + " where " + idEqualsSql;
deleteByIdInSql = "delete from " + baseTable + " where " + idBinderInLHSSqlNoAlias + " ";
if (softDelete) {
softDeleteByIdSql = "update " + baseTable + " set " + getSoftDeleteDbSet() + " where " + idEqualsSql;
softDeleteByIdInSql = "update " + baseTable + " set " + getSoftDeleteDbSet() + " where " + idBinderInLHSSqlNoAlias + " ";
} else {
softDeleteByIdSql = null;
softDeleteByIdInSql = null;
}
if (!isEmbedded()) {
// parse every named update up front into sql dml
for (DeployNamedUpdate namedUpdate : namedUpdates.values()) {
@@ -669,6 +700,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
case INSERT:
return changeLogFilter.includeInsert(request) ? insertBeanChange(request): null;
case UPDATE:
case SOFT_DELETE:
return changeLogFilter.includeUpdate(request) ? updateBeanChange(request): null;
case DELETE:
return changeLogFilter.includeDelete(request) ? deleteBeanChange(request) :null;
@@ -710,11 +742,11 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
cacheHelp.initialise();
}
public SqlUpdate deleteById(Object id, List<Object> idList) {
public SqlUpdate deleteById(Object id, List<Object> idList, boolean softDelete) {
if (id != null) {
return deleteById(id);
return deleteById(id, softDelete);
} else {
return deleteByIdList(idList);
return deleteByIdList(idList, softDelete);
}
}
@@ -729,9 +761,10 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
* Return SQL that can be used to delete a list of Id's without any optimistic
* concurrency checking.
*/
private SqlUpdate deleteByIdList(List<Object> idList) {
private SqlUpdate deleteByIdList(List<Object> idList, boolean softDelete) {
StringBuilder sb = new StringBuilder(deleteByIdInSql);
String baseSql = softDelete ? softDeleteByIdInSql : deleteByIdInSql;
StringBuilder sb = new StringBuilder(baseSql);
String inClause = idBinder.getIdInValueExprDelete(idList.size());
sb.append(inClause);
@@ -746,9 +779,10 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
* Return SQL that can be used to delete by Id without any optimistic
* concurrency checking.
*/
private SqlUpdate deleteById(Object id) {
private SqlUpdate deleteById(Object id, boolean softDelete) {
DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(deleteByIdSql);
String baseSql = softDelete ? softDeleteByIdSql : deleteByIdSql;
DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(baseSql);
Object[] bindValues = idBinder.getBindValues(id);
for (int i = 0; i < bindValues.length; i++) {
@@ -1175,6 +1209,16 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return deleteRecurseSkippable;
}
/**
* Return true if delete can use a single SQL statement.
*
* This implies cascade delete does not continue depth wise and that this is no
* associated L2 bean caching.
*/
public boolean isDeleteByStatement() {
return deleteRecurseSkippable && !isBeanCaching();
}
/**
* Find a property annotated with @WhenCreated or @CreatedTimestamp.
*/
@@ -1898,6 +1942,13 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return EntityType.SQL.equals(entityType);
}
/**
* Return the DB comment for the base table.
*/
public String getDbComment() {
return dbComment;
}
/**
* Return the base table. Only properties mapped to the base table are by
* default persisted.
@@ -1932,6 +1983,22 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return readAuditing;
}
public boolean isSoftDelete() {
return softDelete;
}
public void setSoftDeleteValue(EntityBean bean) {
softDeleteProperty.setSoftDeleteValue(bean);
}
public String getSoftDeleteDbSet() {
return softDeleteProperty.getSoftDeleteDbSet();
}
public String getSoftDeletePredicate(String tableAlias) {
return softDeleteProperty.getSoftDeleteDbPredicate(tableAlias);
}
/**
* Return true if this entity type is draftable.
*/
@@ -1946,6 +2013,38 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
return draftableElement;
}
/**
* Set the draft to true for this entity bean instance.
* This bean is being loaded via asDraft() query.
*/
public void setDraft(EntityBean entityBean) {
if (draft != null) {
draft.setValue(entityBean, true);
}
}
/**
* Return true if the bean is considered a 'draft' instance (not 'live').
*/
public boolean isDraftInstance(EntityBean entityBean) {
if (draft != null) {
return Boolean.TRUE == draft.getValue(entityBean);
}
// no draft property - so return false
return false;
}
/**
* Return true if the bean is draftable and considered a 'live' instance.
*/
public boolean isLiveInstance(EntityBean entityBean) {
if (draft != null) {
return Boolean.FALSE == draft.getValue(entityBean);
}
// no draft property - so return false
return false;
}
/**
* If there is a @DraftDirty property set it's value on the bean.
*/
@@ -2217,7 +2316,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, SpiBeanType<T> {
*/
public void appendOrderById(SpiQuery<T> query) {
if (idProperty != null) {
if (idProperty != null && !idProperty.isEmbedded()) {
OrderBy<T> orderBy = query.getOrderBy();
if (orderBy == null || orderBy.isEmpty()) {
query.order().asc(idProperty.getName());
@@ -125,6 +125,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
private final String serverName;
private final ServerConfig serverConfig;
private Map<Class<?>, DeployBeanInfo<?>> deplyInfoMap = new HashMap<Class<?>, DeployBeanInfo<?>>();
private final Map<Class<?>, BeanTable> beanTableMap = new HashMap<Class<?>, BeanTable>();
@@ -178,8 +180,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
*/
public BeanDescriptorManager(InternalConfiguration config) {
ServerConfig serverConfig = config.getServerConfig();
this.serverConfig = config.getServerConfig();
this.serverName = InternString.intern(serverConfig.getName());
this.cacheManager = config.getCacheManager();
this.xmlConfig = config.getXmlConfig();
@@ -240,6 +241,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
return (historySupport == null ) ? serverConfig.getAsOfViewSuffix() : historySupport.getVersionsBetweenSuffix(serverConfig.getAsOfViewSuffix());
}
@Override
public ServerConfig getServerConfig() {
return serverConfig;
}
@SuppressWarnings("unchecked")
public <T> BeanDescriptor<T> getBeanDescriptor(Class<T> entityType) {
return (BeanDescriptor<T>) descMap.get(entityType.getName());
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebean.config.EncryptKey;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebeaninternal.server.deploy.id.IdBinder;
/**
@@ -12,26 +13,31 @@ import com.avaje.ebeaninternal.server.deploy.id.IdBinder;
*/
public interface BeanDescriptorMap {
/**
* Return the name of the server/database.
*/
String getServerName();
/**
* Return the name of the server/database.
*/
String getServerName();
/**
* Return the Cache Manager.
*/
ServerCacheManager getCacheManager();
/**
* Return the ServerConfig.
*/
ServerConfig getServerConfig();
/**
* Return the BeanDescriptor for a given class.
*/
<T> BeanDescriptor<T> getBeanDescriptor(Class<T> entityType);
/**
* Return the Cache Manager.
*/
ServerCacheManager getCacheManager();
/**
* Return the Encrypt key given the table and column name.
*/
EncryptKey getEncryptKey(String tableName, String columnName);
IdBinder createIdBinder(BeanProperty id);
/**
* Return the BeanDescriptor for a given class.
*/
<T> BeanDescriptor<T> getBeanDescriptor(Class<T> entityType);
/**
* Return the Encrypt key given the table and column name.
*/
EncryptKey getEncryptKey(String tableName, String columnName);
IdBinder createIdBinder(BeanProperty id);
}
@@ -22,6 +22,7 @@ import com.avaje.ebeaninternal.server.text.json.ReadJson;
import com.avaje.ebeaninternal.server.text.json.WriteJson;
import com.avaje.ebeaninternal.server.type.DataBind;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypeBoolean;
import com.avaje.ebeaninternal.util.ValueUtil;
import com.fasterxml.jackson.core.JsonToken;
import org.slf4j.Logger;
@@ -210,6 +211,11 @@ public class BeanProperty implements ElPropertyValue {
*/
final String dbColumnDefn;
/**
* Database DDL column comment.
*/
final String dbComment;
/**
* DB Constraint (typically check constraint on enum)
*/
@@ -223,12 +229,20 @@ public class BeanProperty implements ElPropertyValue {
final boolean jsonDeserialize;
final boolean draft;
final boolean draftOnly;
final boolean draftDirty;
final boolean draftReset;
final boolean softDelete;
final String softDeleteDbSet;
final String softDeleteDbPredicate;
final boolean indexed;
final String indexName;
@@ -255,10 +269,10 @@ public class BeanProperty implements ElPropertyValue {
this.dbInsertable = deploy.isDbInsertable();
this.dbUpdatable = deploy.isDbUpdateable();
this.excludedFromHistory = deploy.isExcludedFromHistory();
this.draft = deploy.isDraft();
this.draftDirty = deploy.isDraftDirty();
this.draftOnly = deploy.isDraftOnly();
this.draftReset = deploy.isDraftReset();
this.secondaryTable = deploy.isSecondaryTable();
if (secondaryTable) {
this.secondaryTableJoin = new TableJoin(deploy.getSecondaryTableJoin());
@@ -289,6 +303,7 @@ public class BeanProperty implements ElPropertyValue {
this.setter = deploy.getSetter();
this.dbColumn = tableAliasIntern(descriptor, deploy.getDbColumn(), false, null);
this.dbComment = deploy.getDbComment();
this.sqlFormulaJoin = InternString.intern(deploy.getSqlFormulaJoin());
this.sqlFormulaSelect = InternString.intern(deploy.getSqlFormulaSelect());
this.formula = sqlFormulaSelect != null;
@@ -302,6 +317,16 @@ public class BeanProperty implements ElPropertyValue {
this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null);
this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), dbEncrypted, dbColumn);
this.softDelete = deploy.isSoftDelete();
if (softDelete) {
ScalarTypeBoolean.BooleanBase boolType = (ScalarTypeBoolean.BooleanBase)scalarType;
this.softDeleteDbSet = dbColumn+"="+boolType.getDbTrueLiteral();
this.softDeleteDbPredicate = "."+dbColumn+","+boolType.getDbFalseLiteral()+")="+boolType.getDbFalseLiteral();
} else {
this.softDeleteDbSet = null;
this.softDeleteDbPredicate = null;
}
this.jsonSerialize = deploy.isJsonSerialize();
this.jsonDeserialize = deploy.isJsonDeserialize();
}
@@ -342,9 +367,13 @@ public class BeanProperty implements ElPropertyValue {
this.formula = false;
this.excludedFromHistory = source.excludedFromHistory;
this.draft = source.draft;
this.draftDirty = source.draftDirty;
this.draftOnly = source.draftOnly;
this.draftReset = source.draftReset;
this.softDelete = source.softDelete;
this.softDeleteDbSet = source.softDeleteDbSet;
this.softDeleteDbPredicate = source.softDeleteDbPredicate;
this.fetchEager = source.fetchEager;
this.unidirectionalShadow = source.unidirectionalShadow;
this.discriminator = source.discriminator;
@@ -354,6 +383,7 @@ public class BeanProperty implements ElPropertyValue {
this.secondaryTableJoin = source.secondaryTableJoin;
this.secondaryTableJoinPrefix = source.secondaryTableJoinPrefix;
this.dbComment = source.dbComment;
this.dbBind = source.getDbBind();
this.dbEncrypted = source.isDbEncrypted();
this.dbEncryptedType = source.getDbEncryptedType();
@@ -616,6 +646,30 @@ public class BeanProperty implements ElPropertyValue {
}
}
/**
* Return the DB literal expression to set the deleted state to true.
*/
public String getSoftDeleteDbSet() {
return softDeleteDbSet;
}
/**
* Return the DB literal predicate used to filter out soft deleted rows from a query.
*/
public String getSoftDeleteDbPredicate(String tableAlias) {
// use coalesce to handle null values from optional relationships
return "coalesce(" + tableAlias + softDeleteDbPredicate;
}
/**
* Set the soft delete property value on the bean without invoking lazy loading.
*/
public void setSoftDeleteValue(EntityBean bean) {
// assumes boolean deleted true being set which is ok limitation for now
setValue(bean, true);
bean._ebean_getIntercept().setChangedProperty(propertyIndex);
}
/**
* Set the value of the property without interception or
* PropertyChangeSupport.
@@ -952,6 +1006,13 @@ public class BeanProperty implements ElPropertyValue {
return dbColumn;
}
/**
* Return the comment for the associated DB column.
*/
public String getDbComment() {
return dbComment;
}
/**
* Return the database jdbc data type this is mapped to.
*/
@@ -1041,6 +1102,14 @@ public class BeanProperty implements ElPropertyValue {
return draftOnly;
}
/**
* Return true if this property is a boolean flag on a draftable bean
* indicating if the instance is a draft or live bean.
*/
public boolean isDraft() {
return draft;
}
/**
* 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.
@@ -1056,6 +1125,13 @@ public class BeanProperty implements ElPropertyValue {
return draftReset;
}
/**
* Return true if this property is the soft delete property.
*/
public boolean isSoftDelete() {
return softDelete;
}
/**
* Return true if this property should be included in an Insert.
*/
@@ -85,8 +85,8 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty {
public void initialise() {
// this *MUST* execute after the BeanDescriptor is
// put into the map to stop infinite recursion
if (!isTransient){
targetDescriptor = descriptor.getBeanDescriptor(targetType);
targetDescriptor = descriptor.getBeanDescriptor(targetType);
if (!isTransient){
targetIdBinder = targetDescriptor.getIdBinder();
targetInheritInfo = targetDescriptor.getInheritInfo();
saveRecurseSkippable = targetDescriptor.isSaveRecurseSkippable();
@@ -800,7 +800,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, ArrayList<Object> excludeDetailIds) {
IntersectionRow row = new IntersectionRow(tableJoin.getTable());
IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor);
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
row.setExcludeIds(excludeDetailIds, getTargetDescriptor());
}
@@ -887,7 +887,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
if (help != null) {
help.jsonWrite(ctx, name, value, include != null);
} else {
if (isTransient) {
if (isTransient && targetDescriptor == null) {
ctx.writeValueUsingObjectMapper(name, value);
} else {
Collection<?> collection = (Collection<?>)value;
@@ -31,7 +31,8 @@ public class BeanPropertyAssocManyJsonHelp {
*/
public BeanPropertyAssocManyJsonHelp(BeanPropertyAssocMany<?> many) {
this.many = many;
this.jsonTransient = !ClassUtil.isJacksonObjectMapperPresent() ? null : new BeanPropertyAssocManyJsonTransient();
boolean objectMapperPresent = many.getBeanDescriptor().getServerConfig().getClassLoadConfig().isJacksonObjectMapperPresent();
this.jsonTransient = !objectMapperPresent ? null : new BeanPropertyAssocManyJsonTransient();
}
/**
@@ -15,13 +15,21 @@ public class IntersectionRow {
private final String tableName;
private final BeanDescriptor<?> targetDescriptor;
private final LinkedHashMap<String, Object> values = new LinkedHashMap<String, Object>();
private ArrayList<Object> excludeIds;
private BeanDescriptor<?> excludeDescriptor;
public IntersectionRow(String tableName, BeanDescriptor<?> targetDescriptor) {
this.tableName = tableName;
this.targetDescriptor = targetDescriptor;
}
public IntersectionRow(String tableName) {
this.tableName = tableName;
this.targetDescriptor = null;
}
/**
@@ -64,22 +72,20 @@ public class IntersectionRow {
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
}
public SqlUpdate createDelete(EbeanServer server) {
public SqlUpdate createDelete(EbeanServer server, boolean softDelete) {
BindParams bindParams = new BindParams();
StringBuilder sb = new StringBuilder();
sb.append("delete from ").append(tableName).append(" where ");
int count = 0;
for (Map.Entry<String, Object> entry : values.entrySet()) {
if (count++ > 0) {
sb.append(" and ");
}
sb.append(entry.getKey());
sb.append(" = ?");
bindParams.setParameter(count, entry.getValue());
if (softDelete) {
sb.append("update ").append(tableName).append(" set ");
sb.append(targetDescriptor.getSoftDeleteDbSet());
} else {
sb.append("delete from ").append(tableName);
}
sb.append(" where ");
int count = setBindParams(bindParams, sb);
if (excludeIds != null) {
IdInExpression idIn = new IdInExpression(excludeIds);
@@ -108,6 +114,13 @@ public class IntersectionRow {
StringBuilder sb = new StringBuilder();
sb.append("delete from ").append(tableName).append(" where ");
setBindParams(bindParams, sb);
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
}
private int setBindParams(BindParams bindParams, StringBuilder sb) {
int count = 0;
for (Map.Entry<String, Object> entry : values.entrySet()) {
if (count++ > 0) {
@@ -120,6 +133,6 @@ public class IntersectionRow {
bindParams.setParameter(count, entry.getValue());
}
return new DefaultSqlUpdate(server, sb.toString(), bindParams);
return count;
}
}
@@ -3,7 +3,9 @@ package com.avaje.ebeaninternal.server.deploy.generatedproperty;
import java.math.BigDecimal;
import java.util.HashSet;
import com.avaje.ebean.config.ClassLoadConfig;
import com.avaje.ebean.config.CurrentUserProvider;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
/**
@@ -13,9 +15,9 @@ public class GeneratedPropertyFactory {
private final CounterFactory counterFactory = new CounterFactory();
private final InsertTimestampFactory insertFactory = new InsertTimestampFactory();
private final InsertTimestampFactory insertFactory;
private final UpdateTimestampFactory updateFactory = new UpdateTimestampFactory();
private final UpdateTimestampFactory updateFactory;
private final HashSet<String> numberTypes = new HashSet<String>();
@@ -23,8 +25,15 @@ public class GeneratedPropertyFactory {
private final GeneratedWhoCreated generatedWhoCreated;
public GeneratedPropertyFactory(CurrentUserProvider currentUserProvider) {
private final ClassLoadConfig classLoadConfig;
public GeneratedPropertyFactory(ServerConfig serverConfig) {
this.classLoadConfig = serverConfig.getClassLoadConfig();
this.insertFactory = new InsertTimestampFactory(classLoadConfig);
this.updateFactory = new UpdateTimestampFactory(classLoadConfig);
CurrentUserProvider currentUserProvider = serverConfig.getCurrentUserProvider();
if (currentUserProvider != null) {
generatedWhoCreated = new GeneratedWhoCreated(currentUserProvider);
generatedWhoModified = new GeneratedWhoModified(currentUserProvider);
@@ -44,7 +53,11 @@ public class GeneratedPropertyFactory {
numberTypes.add(BigDecimal.class.getName());
}
private boolean isNumberType(String typeClassName) {
public ClassLoadConfig getClassLoadConfig() {
return classLoadConfig;
}
private boolean isNumberType(String typeClassName) {
return numberTypes.contains(typeClassName);
}
@@ -9,7 +9,7 @@ import java.util.Map;
import javax.persistence.PersistenceException;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebean.config.ClassLoadConfig;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
/**
@@ -21,25 +21,25 @@ public class InsertTimestampFactory {
final Map<Class<?>, GeneratedProperty> map = new HashMap<Class<?>, GeneratedProperty>();
public InsertTimestampFactory() {
public InsertTimestampFactory(ClassLoadConfig classLoadConfig) {
map.put(Timestamp.class, new GeneratedInsertTimestamp());
map.put(java.util.Date.class, new GeneratedInsertDate());
map.put(Long.class, longTime);
map.put(long.class, longTime);
if (ClassUtil.isPresent("java.time.LocalDate", this.getClass())) {
if (classLoadConfig.isJavaTimePresent()) {
map.put(LocalDateTime.class, new GeneratedInsertJavaTime.LocalDT());
map.put(OffsetDateTime.class, new GeneratedInsertJavaTime.OffsetDT());
map.put(ZonedDateTime.class, new GeneratedInsertJavaTime.ZonedDT());
}
if (ClassUtil.isPresent("org.joda.time.LocalDateTime", this.getClass())) {
if (classLoadConfig.isJodaTimePresent()) {
map.put(org.joda.time.LocalDateTime.class, new GeneratedInsertJodaTime.LocalDT());
map.put(org.joda.time.DateTime.class, new GeneratedInsertJodaTime.DateTimeDT());
}
}
public void setInsertTimestamp(DeployBeanProperty property) {
public void setInsertTimestamp(DeployBeanProperty property) {
property.setGeneratedProperty(createInsertTimestamp(property));
}
@@ -9,7 +9,7 @@ import java.util.Map;
import javax.persistence.PersistenceException;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebean.config.ClassLoadConfig;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
/**
@@ -21,18 +21,18 @@ public class UpdateTimestampFactory {
final Map<Class<?>, GeneratedProperty> map = new HashMap<Class<?>, GeneratedProperty>();
public UpdateTimestampFactory() {
public UpdateTimestampFactory(ClassLoadConfig classLoadConfig) {
map.put(Timestamp.class, new GeneratedUpdateTimestamp());
map.put(java.util.Date.class, new GeneratedUpdateDate());
map.put(Long.class, longTime);
map.put(long.class, longTime);
if (ClassUtil.isPresent("java.time.LocalDate", this.getClass())) {
if (classLoadConfig.isJavaTimePresent()) {
map.put(LocalDateTime.class, new GeneratedUpdateJavaTime.LocalDT());
map.put(OffsetDateTime.class, new GeneratedUpdateJavaTime.OffsetDT());
map.put(ZonedDateTime.class, new GeneratedUpdateJavaTime.ZonedDT());
}
if (ClassUtil.isPresent("org.joda.time.LocalDateTime", this.getClass())) {
if (classLoadConfig.isJodaTimePresent()) {
map.put(org.joda.time.LocalDateTime.class, new GeneratedUpdateJodaTime.LocalDT());
map.put(org.joda.time.DateTime.class, new GeneratedUpdateJodaTime.DateTimeDT());
}
@@ -165,6 +165,9 @@ public class DeployBeanDescriptor<T> {
private ChangeLogFilter changeLogFilter;
private String dbComment;
/**
* Construct the BeanDescriptor.
*/
@@ -207,6 +210,14 @@ public class DeployBeanDescriptor<T> {
return readAuditing;
}
public void setDbComment(String dbComment) {
this.dbComment = dbComment;
}
public String getDbComment() {
return dbComment;
}
public void setDraftable() {
draftable = true;
}
@@ -1,6 +1,7 @@
package com.avaje.ebeaninternal.server.deploy.meta;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.SoftDelete;
import com.avaje.ebean.annotation.UpdatedTimestamp;
import com.avaje.ebean.annotation.WhenCreated;
import com.avaje.ebean.annotation.WhenModified;
@@ -185,10 +186,15 @@ public class DeployBeanProperty {
private boolean excludedFromHistory;
private boolean draft;
private boolean draftOnly;
private boolean draftDirty;
private boolean draftReset;
private boolean softDelete;
private String dbComment;
public DeployBeanProperty(DeployBeanDescriptor<?> desc, Class<?> propertyType, ScalarType<?> scalarType, ScalarTypeConverter<?, ?> typeConverter) {
this.desc = desc;
this.propertyType = propertyType;
@@ -221,6 +227,8 @@ public class DeployBeanProperty {
return AUDITCOLUMN_ORDER;
} else if (field.getAnnotation(Version.class) != null) {
return VERSIONCOLUMN_ORDER;
} else if (field.getAnnotation(SoftDelete.class) != null) {
return VERSIONCOLUMN_ORDER;
}
return 0;
}
@@ -652,6 +660,7 @@ public class DeployBeanProperty {
return false;
}
}
/**
* Return true if this property is based on a secondary table.
*/
@@ -848,6 +857,15 @@ public class DeployBeanProperty {
this.excludedFromHistory = true;
}
public void setDraft() {
this.draft = true;
this.isTransient = true;
}
public boolean isDraft() {
return draft;
}
public void setDraftOnly() {
this.draftOnly = true;
}
@@ -859,6 +877,7 @@ public class DeployBeanProperty {
public void setDraftDirty() {
this.draftOnly = true;
this.draftDirty = true;
this.nullable = false;
}
public boolean isDraftDirty() {
@@ -872,4 +891,21 @@ public class DeployBeanProperty {
public boolean isDraftReset() {
return draftReset;
}
public void setSoftDelete() {
this.softDelete = true;
this.nullable = false;
}
public boolean isSoftDelete() {
return softDelete;
}
public void setDbComment(String dbComment) {
this.dbComment = dbComment;
}
public String getDbComment() {
return dbComment;
}
}
@@ -28,6 +28,8 @@ public class DeployBeanPropertyLists {
private BeanProperty versionProperty;
private BeanProperty draft;
private BeanProperty draftDirty;
private final BeanDescriptor<?> desc;
@@ -136,6 +138,9 @@ public class DeployBeanPropertyLists {
private void allocateToList(BeanProperty prop) {
if (prop.isTransient()) {
transients.add(prop);
if (prop.isDraft()) {
draft = prop;
}
return;
}
if (prop.isId()) {
@@ -293,6 +298,20 @@ public class DeployBeanPropertyLists {
return draftDirty;
}
public BeanProperty getDraft() {
return draft;
}
public BeanProperty getSoftDeleteProperty() {
for (BeanProperty prop: nonManys) {
if (prop.isSoftDelete()) {
return prop;
}
}
return null;
}
/**
* Mode used to determine which BeanPropertyAssoc to include.
*/
@@ -1,5 +1,7 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
@@ -9,6 +11,7 @@ import javax.persistence.UniqueConstraint;
import com.avaje.ebean.annotation.CacheStrategy;
import com.avaje.ebean.annotation.CacheTuning;
import com.avaje.ebean.annotation.DbComment;
import com.avaje.ebean.annotation.Draftable;
import com.avaje.ebean.annotation.DraftableElement;
import com.avaje.ebean.annotation.EntityConcurrencyMode;
@@ -25,22 +28,60 @@ import com.avaje.ebeaninternal.server.deploy.CompoundUniqueConstraint;
import com.avaje.ebeaninternal.server.deploy.DeployNamedQuery;
import com.avaje.ebeaninternal.server.deploy.DeployNamedUpdate;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Read the class level deployment annotations.
*/
public class AnnotationClass extends AnnotationParser {
private static final Logger logger = LoggerFactory.getLogger(AnnotationClass.class);
private final String asOfViewSuffix;
private final String versionsBetweenSuffix;
/**
* Create for normal early parse of class level annotations.
*/
public AnnotationClass(DeployBeanInfo<?> info, boolean validationAnnotations, String asOfViewSuffix, String versionsBetweenSuffix) {
super(info, validationAnnotations);
this.asOfViewSuffix = asOfViewSuffix;
this.versionsBetweenSuffix = versionsBetweenSuffix;
}
/**
* Create to parse AttributeOverride annotations which is run last
* after all the properties/fields have been parsed fully.
*/
public AnnotationClass(DeployBeanInfo<?> info) {
super(info, false);
this.asOfViewSuffix = null;
this.versionsBetweenSuffix = null;
}
/**
* Parse any AttributeOverride set on the class.
*/
public void parseAttributeOverride() {
Class<?> cls = descriptor.getBeanType();
AttributeOverride override = cls.getAnnotation(AttributeOverride.class);
if (override != null) {
String propertyName = override.name();
Column column = override.column();
if (column != null) {
DeployBeanProperty beanProperty = descriptor.getBeanProperty(propertyName);
if (beanProperty == null) {
logger.error("AttributeOverride property [" + propertyName + "] not found on " + descriptor.getFullName());
} else {
readColumn(column, beanProperty);
}
}
}
}
/**
* Read the class level deployment annotations.
*/
@@ -120,6 +161,11 @@ public class AnnotationClass extends AnnotationParser {
descriptor.setHistorySupport();
}
DbComment comment = cls.getAnnotation(DbComment.class);
if (comment != null) {
descriptor.setDbComment(comment.value());
}
UpdateMode updateMode = cls.getAnnotation(UpdateMode.class);
if (updateMode != null) {
descriptor.setUpdateChangesOnly(updateMode.updateChangesOnly());
@@ -149,6 +149,9 @@ public class AnnotationFields extends AnnotationParser {
util.setLobType(prop);
}
if (get(prop, Draft.class) != null) {
prop.setDraft();
}
if (get(prop, DraftOnly.class) != null) {
prop.setDraftOnly();
}
@@ -158,7 +161,15 @@ public class AnnotationFields extends AnnotationParser {
if (get(prop, DraftReset.class) != null) {
prop.setDraftReset();
}
SoftDelete softDelete = get(prop, SoftDelete.class);
if (softDelete != null) {
prop.setSoftDelete();
}
DbComment comment = get(prop, DbComment.class);
if (comment != null) {
prop.setDbComment(comment.value());
}
DbJson dbJson = get(prop, DbJson.class);
if (dbJson != null) {
util.setDbJsonType(prop, dbJson);
@@ -449,32 +460,5 @@ public class AnnotationFields extends AnnotationParser {
}
}
private void readColumn(Column columnAnn, DeployBeanProperty prop) {
if (!isEmpty(columnAnn.name())) {
String dbColumn = databasePlatform.convertQuotedIdentifiers(columnAnn.name());
prop.setDbColumn(dbColumn);
}
prop.setDbInsertable(columnAnn.insertable());
prop.setDbUpdateable(columnAnn.updatable());
prop.setNullable(columnAnn.nullable());
prop.setUnique(columnAnn.unique());
if (columnAnn.precision() > 0) {
prop.setDbLength(columnAnn.precision());
} else if (columnAnn.length() != 255) {
// set default 255 on DbTypeMap
prop.setDbLength(columnAnn.length());
}
prop.setDbScale(columnAnn.scale());
prop.setDbColumnDefn(columnAnn.columnDefinition());
String baseTable = descriptor.getBaseTable();
String tableName = columnAnn.table();
if (!"".equals(tableName) && !tableName.equalsIgnoreCase(baseTable)) {
// its on a secondary table...
prop.setSecondaryTable(tableName);
}
}
}
@@ -5,9 +5,11 @@ import java.util.HashMap;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import com.avaje.ebeaninternal.server.deploy.BeanCascadeInfo;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
/**
@@ -66,4 +68,31 @@ public abstract class AnnotationParser extends AnnotationBase {
}
protected void readColumn(Column columnAnn, DeployBeanProperty prop) {
if (!isEmpty(columnAnn.name())) {
String dbColumn = databasePlatform.convertQuotedIdentifiers(columnAnn.name());
prop.setDbColumn(dbColumn);
}
prop.setDbInsertable(columnAnn.insertable());
prop.setDbUpdateable(columnAnn.updatable());
prop.setNullable(columnAnn.nullable());
prop.setUnique(columnAnn.unique());
if (columnAnn.precision() > 0) {
prop.setDbLength(columnAnn.precision());
} else if (columnAnn.length() != 255) {
// set default 255 on DbTypeMap
prop.setDbLength(columnAnn.length());
}
prop.setDbScale(columnAnn.scale());
prop.setDbColumnDefn(columnAnn.columnDefinition());
String baseTable = descriptor.getBaseTable();
String tableName = columnAnn.table();
if (!"".equals(tableName) && !tableName.equalsIgnoreCase(baseTable)) {
// its on a secondary table...
prop.setSecondaryTable(tableName);
}
}
}
@@ -1,6 +1,5 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
@@ -37,8 +36,8 @@ public class ReadAnnotations {
this.generatedPropFactory = generatedPropFactory;
this.asOfViewSuffix = asOfViewSuffix;
this.versionsBetweenSuffix = versionsBetweenSuffix;
this.javaxValidationAnnotations = ClassUtil.isJavaxValidationAnnotationsPresent();
this.jacksonAnnotations = ClassUtil.isJacksonAnnotationsPresent();
this.javaxValidationAnnotations = generatedPropFactory.getClassLoadConfig().isJavaxValidationAnnotationsPresent();
this.jacksonAnnotations = generatedPropFactory.getClassLoadConfig().isJacksonAnnotationsPresent();
}
/**
@@ -80,6 +79,8 @@ public class ReadAnnotations {
// dependent on field level annotations
new AnnotationSql(info, javaxValidationAnnotations).parse();
new AnnotationClass(info).parseAttributeOverride();
} catch (RuntimeException e) {
throw new RuntimeException("Error reading annotations for " + info, e);
}
@@ -1,13 +1,13 @@
package com.avaje.ebeaninternal.server.expression;
import java.util.Collection;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import java.util.Collection;
class InExpression extends AbstractExpression {
private static final long serialVersionUID = 3150665801693551260L;
@@ -54,10 +54,8 @@ class InExpression extends AbstractExpression {
public void addSql(SpiExpressionRequest request) {
if (values.length == 0) {
if (!not) {
// 'no match' for in empty collection
request.append("1=0");
}
String expr = not ? "1=1" : "1=0";
request.append(expr);
return;
}
@@ -234,6 +234,16 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
return exprList.asOf(asOf);
}
@Override
public Query<T> asDraft() {
return exprList.asDraft();
}
@Override
public Query<T> includeSoftDeletes() {
return exprList.includeSoftDeletes();
}
@Override
public List<Version<T>> findVersions() {
return exprList.findVersions();
@@ -304,6 +314,11 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
return exprList.findPagedList(pageIndex, pageSize);
}
@Override
public PagedList<T> findPagedList() {
return exprList.findPagedList();
}
@Override
public int findRowCount() {
return exprList.findRowCount();
@@ -557,7 +572,7 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
}
@Override
public ExpressionList<T> raw(String raw, Object[] values) {
public ExpressionList<T> raw(String raw, Object... values) {
return exprList.raw(raw, values);
}
@@ -1,5 +1,12 @@
package com.avaje.ebeaninternal.server.lib.sql;
import com.avaje.ebean.config.DataSourceConfig;
import com.avaje.ebeaninternal.api.ClassUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
@@ -12,15 +19,6 @@ import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.avaje.ebean.config.DataSourceConfig;
import com.avaje.ebeaninternal.api.ClassUtil;
/**
* A robust DataSource.
* <p>
@@ -171,10 +169,14 @@ public class DataSourcePool implements DataSource {
private final Runnable heartbeatRunnable = new HeartBeatRunnable();
public DataSourcePool(DataSourceAlert notify, String name, DataSourceConfig params) {
this(notify, name, params, null);
}
public DataSourcePool(DataSourceAlert notify, String name, DataSourceConfig params, DataSourcePoolListener listener) {
this.notify = notify;
this.name = name;
this.poolListener = createPoolListener(params.getPoolListener());
this.poolListener = listener;
this.autoCommit = params.isAutoCommit();
this.transactionIsolation = params.getIsolationLevel();
@@ -238,28 +240,13 @@ public class DataSourcePool implements DataSource {
throw new SQLFeatureNotSupportedException("We do not support java.util.logging");
}
/**
* Create the DataSourcePoolListener if there is one.
*/
private DataSourcePoolListener createPoolListener(String cn) {
if (cn == null) {
return null;
}
try {
return (DataSourcePoolListener) ClassUtil.newInstance(cn, this.getClass());
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private void initialise() throws SQLException {
// Ensure database driver is loaded
try {
ClassUtil.forName(this.databaseDriver, this.getClass());
ClassUtil.forName(this.databaseDriver);
} catch (Throwable e) {
throw new PersistenceException("Problem loading Database Driver [" + this.databaseDriver + "]: "
+ e.getMessage(), e);
throw new PersistenceException("Problem loading Database Driver [" + this.databaseDriver + "]: " + e.getMessage(), e);
}
String transIsolation = TransactionIsolation.getLevelDescription(transactionIsolation);
@@ -45,27 +45,12 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex
protected void configureQuery(SpiQuery<?> query, String lazyLoadProperty) {
// propagate the readOnly state
if (parent.isReadOnly() != null) {
query.setReadOnly(parent.isReadOnly());
}
// propagate the asOf and lazy loading mode
query.setDisableLazyLoading(parent.isDisableLazyLoading());
query.asOf(parent.getAsOf());
parent.propagateQueryState(query);
query.setParentNode(objectGraphNode);
query.setLazyLoadProperty(lazyLoadProperty);
if (parent.isAsDraft()) {
query.asDraft();
}
if (parent.isDisableReadAudit()) {
query.setDisableReadAuditing();
}
if (queryProps != null) {
queryProps.configureBeanQuery(query);
}
if (parent.isUseAutoTune()) {
query.setAutoTune(true);
}
}
protected void register(EntityBeanIntercept ebi) {
@@ -43,6 +43,7 @@ public class DLoadContext implements LoadContext {
private final int defaultBatchSize;
private final boolean disableLazyLoading;
private final boolean disableReadAudit;
private final boolean includeSoftDeletes;
/**
* The path relative to the root of the object graph.
@@ -67,6 +68,7 @@ public class DLoadContext implements LoadContext {
SpiQuery<?> query = request.getQuery();
this.asOf = query.getAsOf();
this.asDraft = query.isAsDraft();
this.includeSoftDeletes = query.isIncludeSoftDeletes();
this.readOnly = query.isReadOnly();
this.disableReadAudit = query.isDisableReadAudit();
this.disableLazyLoading = query.isDisableLazyLoading();
@@ -201,10 +203,6 @@ public class DLoadContext implements LoadContext {
return new ObjectGraphNode(origin, path);
}
public boolean isUseAutoTune() {
return useProfiling;
}
protected String getFullPath(String path) {
if (relativePath == null) {
return path;
@@ -225,34 +223,6 @@ public class DLoadContext implements LoadContext {
return readOnly;
}
/**
* Return the 'as of' timestamp that should propagate to secondary queries.
*/
protected Timestamp getAsOf() {
return asOf;
}
/**
* Return true if the root query is a 'asDraft' query that should propagate to secondary queries.
*/
protected boolean isAsDraft() {
return asDraft;
}
/**
* Return true if disable read auditing should propagate to secondary queries.
*/
protected boolean isDisableReadAudit() {
return disableReadAudit;
}
/**
* Return true if disable lazy loading should propagate to secondary queries.
*/
protected boolean isDisableLazyLoading() {
return disableLazyLoading;
}
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
@@ -335,4 +305,26 @@ public class DLoadContext implements LoadContext {
return desc.getBeanPropertyFromPath(path);
}
/**
* Propagate the original query settings (draft, asOf etc) to the secondary queries.
*/
public void propagateQueryState(SpiQuery<?> query) {
if (readOnly != null) {
query.setReadOnly(readOnly);
}
query.setDisableLazyLoading(disableLazyLoading);
query.asOf(asOf);
if (asDraft) {
query.asDraft();
}
if (includeSoftDeletes) {
query.includeSoftDeletes();
}
if (disableReadAudit) {
query.setDisableReadAuditing();
}
if (useProfiling) {
query.setAutoTune(true);
}
}
}
@@ -56,27 +56,11 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex
public void configureQuery(SpiQuery<?> query) {
// propagate the readOnly state
if (parent.isReadOnly() != null) {
query.setReadOnly(parent.isReadOnly());
}
// propagate the asOf and lazy loading mode
query.setDisableLazyLoading(parent.isDisableLazyLoading());
query.asOf(parent.getAsOf());
parent.propagateQueryState(query);
query.setParentNode(objectGraphNode);
if (parent.isAsDraft()) {
query.asDraft();
}
if (parent.isDisableReadAudit()) {
query.setDisableReadAuditing();
}
if (queryProps != null) {
queryProps.configureBeanQuery(query);
}
if (parent.isUseAutoTune()) {
query.setAutoTune(true);
}
}
public BeanPropertyAssocMany<?> getBeanProperty() {
@@ -138,6 +138,7 @@ public class BatchedBeanHolder {
return inserts.size();
case UPDATE:
case SOFT_DELETE:
if (updates == null) {
updates = new ArrayList<PersistRequest>();
}
@@ -33,7 +33,6 @@ import com.avaje.ebeaninternal.server.deploy.ManyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.Collection;
@@ -60,8 +59,6 @@ import java.util.Set;
*/
public final class DefaultPersister implements Persister {
private static final Logger SUM = LoggerFactory.getLogger("org.avaje.ebean.SUM");
private static final Logger PUB = LoggerFactory.getLogger("org.avaje.ebean.PUB");
private static final Logger logger = LoggerFactory.getLogger(DefaultPersister.class);
@@ -337,9 +334,10 @@ public final class DefaultPersister implements Persister {
/**
* Recursively delete the bean. This calls back to the EbeanServer.
*/
private void deleteRecurse(Object detailBean, Transaction t) {
// NB: a new PersistRequest is made
server.delete(detailBean, t);
private void deleteRecurse(EntityBean detailBean, Transaction t, boolean softDelete) {
Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE_PERMANENT;
deleteRequest(createRequest(detailBean, t, deleteType));
}
/**
@@ -356,6 +354,7 @@ public final class DefaultPersister implements Persister {
PersistRequestBean<?> req = createRequest(entityBean, t, PersistRequest.Type.UPDATE);
req.setDeleteMissingChildren(deleteMissingChildren);
req.checkDraft();
try {
req.initTransIfRequiredWithBatchCascade();
if (req.isReference()) {
@@ -500,23 +499,34 @@ public final class DefaultPersister implements Persister {
* Delete the bean with the explicit transaction.
* Return false if the delete is executed without OCC and 0 rows were deleted.
*/
public boolean delete(EntityBean bean, Transaction t) {
public boolean delete(EntityBean bean, Transaction t, boolean permanent) {
PersistRequestBean<EntityBean> request = createRequest(bean, t, Type.DELETE);
boolean deleted = deleteRequest(request);
Type deleteType = permanent ? Type.DELETE_PERMANENT : Type.DELETE;
PersistRequestBean<EntityBean> originalRequest = createRequest(bean, t, deleteType);
if (request.isDraftable()) {
// we have just deleting a draft bean so now we need to delete the
// associated 'live' bean. This is effectively an 'automatic publish'.
deleteRequest(createRequest(request.createReference(), t, Type.DELETE, true));
if (originalRequest.isHardDeleteDraft()) {
// a hard delete of a draftable bean so first we need to delete the associated 'live' bean
// due to FK constraint and then after that execute the original delete of the draft bean
return deleteRequest(createPublishRequest(originalRequest.createReference(), t, Type.DELETE_PERMANENT, true), originalRequest);
} else {
// normal delete or soft delete
return deleteRequest(originalRequest);
}
return deleted;
}
/**
* Execute the delete request returning true if a delete occurred.
*/
private boolean deleteRequest(PersistRequestBean<?> req) {
return deleteRequest(req, null);
}
/**
* Execute the delete request support a second delete request for live and draft permanent delete.
* A common transaction is used across both requests.
*/
private boolean deleteRequest(PersistRequestBean<?> req, PersistRequestBean<?> draftReq) {
if (req.isRegisteredForDeleteBean()) {
// skip deleting bean. Used where cascade is on
@@ -530,6 +540,11 @@ public final class DefaultPersister implements Persister {
try {
req.initTransIfRequiredWithBatchCascade();
boolean deleted = delete(req);
if (draftReq != null) {
// delete the 'draft' bean ('live' bean deleted first)
draftReq.setTrans(req.getTransaction());
deleted = delete(draftReq);
}
req.commitTransIfRequired();
req.flushBatchOnCascade();
@@ -541,10 +556,10 @@ public final class DefaultPersister implements Persister {
}
}
private void deleteList(List<?> beanList, Transaction t) {
private void deleteList(List<?> beanList, Transaction t, boolean softDelete) {
for (int i = 0; i < beanList.size(); i++) {
EntityBean bean = (EntityBean) beanList.get(i);
delete(bean, t);
deleteRecurse((EntityBean) beanList.get(i), t, softDelete);
//delete((EntityBean) beanList.get(i), t);
}
}
@@ -565,7 +580,7 @@ public final class DefaultPersister implements Persister {
idList.add(descriptor.convertId(id));
}
delete(descriptor, null, idList, transaction);
delete(descriptor, null, idList, transaction, descriptor.isSoftDelete());
}
/**
@@ -577,13 +592,13 @@ public final class DefaultPersister implements Persister {
// convert to appropriate type if required
id = descriptor.convertId(id);
return delete(descriptor, id, null, transaction);
return delete(descriptor, id, null, transaction, descriptor.isSoftDelete());
}
/**
* Delete by Id or a List of Id's.
*/
private int delete(BeanDescriptor<?> descriptor, Object id, List<Object> idList, Transaction transaction) {
private int delete(BeanDescriptor<?> descriptor, Object id, List<Object> idList, Transaction transaction, boolean softDelete) {
SpiTransaction t = (SpiTransaction) transaction;
if (t.isPersistCascade()) {
@@ -592,14 +607,14 @@ public final class DefaultPersister implements Persister {
// We actually need to execute a query to get the foreign key values
// as they are required for the delete cascade. Query back just the
// Id and the appropriate foreign key values
Query<?> q = deleteRequiresQuery(descriptor, propImportDelete);
Query<?> q = deleteRequiresQuery(descriptor, propImportDelete, softDelete);
if (idList != null) {
q.where().idIn(idList);
if (t.isLogSummary()) {
t.logSummary("-- DeleteById of " + descriptor.getName() + " ids[" + idList + "] requires fetch of foreign key values");
}
List<?> beanList = server.findList(q, t);
deleteList(beanList, t);
deleteList(beanList, t, softDelete);
return beanList.size();
} else {
@@ -611,7 +626,7 @@ public final class DefaultPersister implements Persister {
if (bean == null) {
return 0;
} else {
delete(bean, t);
deleteRecurse(bean, t, softDelete);
return 1;
}
}
@@ -623,12 +638,15 @@ public final class DefaultPersister implements Persister {
BeanPropertyAssocOne<?>[] expOnes = descriptor.propertiesOneExportedDelete();
for (int i = 0; i < expOnes.length; i++) {
BeanDescriptor<?> targetDesc = expOnes[i].getTargetDescriptor();
if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) {
SqlUpdate sqlDelete = expOnes[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
List<Object> childIds = expOnes[i].findIdsByParentId(id, idList, t);
deleteChildrenById(t, targetDesc, childIds);
// only cascade soft deletes when supported by target
if (!softDelete || targetDesc.isSoftDelete()) {
if (!softDelete && targetDesc.isDeleteByStatement()) {
SqlUpdate sqlDelete = expOnes[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
List<Object> childIds = expOnes[i].findIdsByParentId(id, idList, t);
deleteChildrenById(t, targetDesc, childIds, softDelete);
}
}
}
@@ -636,32 +654,37 @@ public final class DefaultPersister implements Persister {
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyDelete();
for (int i = 0; i < manys.length; i++) {
BeanDescriptor<?> targetDesc = manys[i].getTargetDescriptor();
if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) {
// we can just delete children with a single statement
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
// we need to fetch the Id's to delete (recurse or notify L2 cache)
List<Object> childIds = manys[i].findIdsByParentId(id, idList, t, null);
if (!childIds.isEmpty()) {
delete(targetDesc, null, childIds, t);
// only cascade soft deletes when supported by target
if (!softDelete || targetDesc.isSoftDelete()) {
if (!softDelete && targetDesc.isDeleteByStatement()) {
// we can just delete children with a single statement
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
executeSqlUpdate(sqlDelete, t);
} else {
// we need to fetch the Id's to delete (recurse or notify L2 cache)
List<Object> childIds = manys[i].findIdsByParentId(id, idList, t, null);
if (!childIds.isEmpty()) {
delete(targetDesc, null, childIds, t, softDelete);
}
}
}
}
}
// ManyToMany's ... delete from intersection table
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyToMany();
for (int i = 0; i < manys.length; i++) {
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
if (t.isLogSummary()) {
t.logSummary("-- Deleting intersection table entries: " + manys[i].getFullBeanName());
if (!softDelete) {
// ManyToMany's ... delete from intersection table
BeanPropertyAssocMany<?>[] manys = descriptor.propertiesManyToMany();
for (int i = 0; i < manys.length; i++) {
SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList);
if (t.isLogSummary()) {
t.logSummary("-- Deleting intersection table entries: " + manys[i].getFullBeanName());
}
executeSqlUpdate(sqlDelete, t);
}
executeSqlUpdate(sqlDelete, t);
}
// delete the bean(s)
SqlUpdate deleteById = descriptor.deleteById(id, idList);
SqlUpdate deleteById = descriptor.deleteById(id, idList, softDelete);
if (t.isLogSummary()) {
if (idList != null) {
t.logSummary("-- Deleting " + descriptor.getName() + " Ids: " + idList);
@@ -695,7 +718,7 @@ public final class DefaultPersister implements Persister {
* We need to create and execute a query to get the foreign key values as
* the delete cascades to them (foreign keys).
*/
private Query<?> deleteRequiresQuery(BeanDescriptor<?> desc, BeanPropertyAssocOne<?>[] propImportDelete) {
private Query<?> deleteRequiresQuery(BeanDescriptor<?> desc, BeanPropertyAssocOne<?>[] propImportDelete, boolean softDelete) {
Query<?> q = server.createQuery(desc.getBeanType());
StringBuilder sb = new StringBuilder(30);
@@ -704,6 +727,10 @@ public final class DefaultPersister implements Persister {
}
q.setAutoTune(false);
q.select(sb.toString());
if (!softDelete) {
// hard delete so we want this query to include logically deleted rows (if any)
q.includeSoftDeletes();
}
return q;
}
@@ -924,7 +951,7 @@ public final class DefaultPersister implements Persister {
EntityBean eb = (EntityBean) removedBean;
if (eb._ebean_getIntercept().isLoaded()) {
// only delete if the bean was loaded meaning that it is known to exist in the DB
deleteRequest(createRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish()));
deleteRequest(createPublishRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish()));
}
}
}
@@ -1031,7 +1058,7 @@ public final class DefaultPersister implements Persister {
}
}
// deleting missing children - children not in our collected detailIds
deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds);
deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false);
}
t.depth(-1);
@@ -1175,7 +1202,7 @@ public final class DefaultPersister implements Persister {
// the object from the 'other' side of the ManyToMany
// build a intersection row for 'delete'
IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherDelete, saveManyPropRequest.isPublish());
SqlUpdate sqlDelete = intRow.createDelete(server);
SqlUpdate sqlDelete = intRow.createDelete(server, false);
executeSqlUpdate(sqlDelete, t);
}
}
@@ -1206,6 +1233,7 @@ public final class DefaultPersister implements Persister {
BeanDescriptor<?> desc = request.getBeanDescriptor();
EntityBean parentBean = request.getEntityBean();
boolean softDelete = request.isSoftDelete();
BeanPropertyAssocOne<?>[] expOnes = desc.propertiesOneExportedDelete();
if (expOnes.length > 0) {
@@ -1213,16 +1241,19 @@ public final class DefaultPersister implements Persister {
DeleteUnloadedForeignKeys unloaded = null;
for (int i = 0; i < expOnes.length; i++) {
BeanPropertyAssocOne<?> prop = expOnes[i];
if (request.isLoadedProperty(prop)) {
Object detailBean = prop.getValue(parentBean);
if (detailBean != null) {
deleteRecurse(detailBean, t);
// for soft delete check cascade type also supports soft delete
if (!softDelete || prop.getTargetDescriptor().isSoftDelete()) {
if (request.isLoadedProperty(prop)) {
Object detailBean = prop.getValue(parentBean);
if (detailBean != null) {
deleteRecurse((EntityBean)detailBean, t, softDelete);
}
} else {
if (unloaded == null) {
unloaded = new DeleteUnloadedForeignKeys(server, request);
}
unloaded.add(prop);
}
} else {
if (unloaded == null) {
unloaded = new DeleteUnloadedForeignKeys(server, request);
}
unloaded.add(prop);
}
}
if (unloaded != null) {
@@ -1235,30 +1266,34 @@ public final class DefaultPersister implements Persister {
BeanPropertyAssocMany<?>[] manys = desc.propertiesManyDelete();
for (int i = 0; i < manys.length; i++) {
if (manys[i].isManyToMany()) {
// delete associated rows from intersection table
deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish());
if (!softDelete) {
// delete associated rows from intersection table (but not during soft delete)
deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish());
}
} else {
if (ModifyListenMode.REMOVALS.equals(manys[i].getModifyListenMode())) {
// PrivateOwned ...
Object details = manys[i].getValue(parentBean);
if (details instanceof BeanCollection<?>) {
Set<?> modifyRemovals = ((BeanCollection<?>) details).getModifyRemovals();
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
// if soft delete then check target also supports soft delete
if (!softDelete || manys[i].getTargetDescriptor().isSoftDelete()) {
Object details = manys[i].getValue(parentBean);
if (details instanceof BeanCollection<?>) {
Set<?> modifyRemovals = ((BeanCollection<?>) details).getModifyRemovals();
if (modifyRemovals != null && !modifyRemovals.isEmpty()) {
// delete the orphans that have been removed from the collection
for (Object detail : modifyRemovals) {
EntityBean detailBean = (EntityBean) detail;
if (manys[i].hasId(detailBean)) {
deleteRecurse(detailBean, t);
// delete the orphans that have been removed from the collection
for (Object detail : modifyRemovals) {
EntityBean detailBean = (EntityBean) detail;
if (manys[i].hasId(detailBean)) {
deleteRecurse(detailBean, t, softDelete);
}
}
}
}
}
}
deleteManyDetails(t, desc, parentBean, manys[i], null);
deleteManyDetails(t, desc, parentBean, manys[i], null, softDelete);
}
}
@@ -1275,23 +1310,25 @@ public final class DefaultPersister implements Persister {
* </p>
*/
private void deleteManyDetails(SpiTransaction t, BeanDescriptor<?> desc, EntityBean parentBean,
BeanPropertyAssocMany<?> many, ArrayList<Object> excludeDetailIds) {
BeanPropertyAssocMany<?> many, ArrayList<Object> excludeDetailIds, boolean softDelete) {
if (many.getCascadeInfo().isDelete()) {
// cascade delete the beans in the collection
BeanDescriptor<?> targetDesc = many.getTargetDescriptor();
if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) {
// Just delete all the children with one statement
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
SqlUpdate sqlDelete = intRow.createDelete(server);
executeSqlUpdate(sqlDelete, t);
if (!softDelete || targetDesc.isSoftDelete()) {
if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) {
// Just delete all the children with one statement
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
SqlUpdate sqlDelete = intRow.createDelete(server, softDelete);
executeSqlUpdate(sqlDelete, t);
} else {
// Delete recurse using the Id values of the children
Object parentId = desc.getId(parentBean);
List<Object> idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds);
if (!idsByParentId.isEmpty()) {
deleteChildrenById(t, targetDesc, idsByParentId);
} else {
// Delete recurse using the Id values of the children
Object parentId = desc.getId(parentBean);
List<Object> idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds);
if (!idsByParentId.isEmpty()) {
deleteChildrenById(t, targetDesc, idsByParentId, softDelete);
}
}
}
}
@@ -1302,7 +1339,7 @@ public final class DefaultPersister implements Persister {
* <p>
* Will use delete by object if the child entity has manyToMany relationships.
*/
private void deleteChildrenById(SpiTransaction t, BeanDescriptor<?> targetDesc, List<Object> childIds) {
private void deleteChildrenById(SpiTransaction t, BeanDescriptor<?> targetDesc, List<Object> childIds, boolean softDelete) {
if (targetDesc.propertiesManyToMany().length > 0) {
// convert into a list of reference objects and perform delete by object
@@ -1310,11 +1347,11 @@ public final class DefaultPersister implements Persister {
for (Object id : childIds) {
refList.add(targetDesc.createReference(null, id));
}
deleteList(refList, t);
deleteList(refList, t, softDelete);
} else {
// perform delete by statement if possible
delete(targetDesc, null, childIds, t);
delete(targetDesc, null, childIds, t, softDelete);
}
}
@@ -1375,9 +1412,7 @@ public final class DefaultPersister implements Persister {
*/
private void deleteAssocOne(PersistRequestBean<?> request) {
BeanDescriptor<?> desc = request.getBeanDescriptor();
BeanPropertyAssocOne<?>[] ones = desc.propertiesOneImportedDelete();
BeanPropertyAssocOne<?>[] ones = request.getBeanDescriptor().propertiesOneImportedDelete();
for (int i = 0; i < ones.length; i++) {
BeanPropertyAssocOne<?> prop = ones[i];
if (request.isLoadedProperty(prop)) {
@@ -1385,7 +1420,7 @@ public final class DefaultPersister implements Persister {
if (detailBean != null) {
EntityBean detail = (EntityBean) detailBean;
if (prop.hasId(detail)) {
deleteRecurse(detail, request.getTransaction());
deleteRecurse(detail, request.getTransaction(), request.isSoftDelete());
}
}
}
@@ -1426,18 +1461,25 @@ public final class DefaultPersister implements Persister {
* perform an insert, update or delete.
*/
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, PersistRequest.Type type) {
return createRequest(bean, t, type, false);
return createRequestInternal(bean, t, type, false, false);
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) {
private <T> PersistRequestBean<T> createPublishRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) {
return createRequestInternal(bean, t, type, false, publish);
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private <T> PersistRequestBean<T> createRequestInternal(T bean, Transaction t, PersistRequest.Type type, boolean saveRecurse, boolean publish) {
BeanManager<T> mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
}
return createRequest(bean, t, null, mgr, type, false, publish);
return createRequest(bean, t, null, mgr, type, saveRecurse, publish);
}
/**
@@ -1471,6 +1513,13 @@ public final class DefaultPersister implements Persister {
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, Object parentBean, BeanManager<?> mgr,
PersistRequest.Type type, boolean saveRecurse, boolean publish) {
if (type == Type.DELETE_PERMANENT) {
type = Type.DELETE;
} else if (type == Type.DELETE && mgr.getBeanDescriptor().isSoftDelete()) {
// automatically convert to soft delete for types that support it
type = Type.SOFT_DELETE;
}
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, saveRecurse, publish);
}
@@ -84,7 +84,7 @@ public final class InsertMeta {
this.selectLastInsertedId = desc.getSelectLastInsertedId();
}
this.sqlNullId = genSql(true, tableName, false);
this.sqlDraftNullId = desc.isDraftable() ? genSql(false, draftTableName, true) : sqlNullId;
this.sqlDraftNullId = desc.isDraftable() ? genSql(true, draftTableName, true) : sqlNullId;
}
}
@@ -268,6 +268,8 @@ public class CQueryBuilder {
SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query));
if (query.isAsOfQuery()) {
sqlTree.addAsOfTableAlias(query);
} else if (SpiQuery.TemporalMode.CURRENT == query.getTemporalMode()) {
sqlTree.addSoftDeletePredicate(query);
}
SqlLimitResponse res = buildSql(null, request, predicates, sqlTree);
@@ -478,6 +480,23 @@ public class CQueryBuilder {
}
}
if (!query.isIncludeSoftDeletes()) {
List<String> softDeletePredicates = query.getSoftDeletePredicates();
if (softDeletePredicates != null) {
if (!hasWhere) {
sb.append(" where ");
} else {
sb.append("and ");
}
for (int i = 0; i < softDeletePredicates.size(); i++) {
if (i > 0) {
sb.append(" and ");
}
sb.append(softDeletePredicates.get(i));
}
}
}
if (dbOrderBy != null) {
sb.append(" order by ").append(dbOrderBy);
}
@@ -21,7 +21,9 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
private final SpiQuery<T> query;
private final int pageSize;
private final int firstRow;
private final int maxRows;
private final int pageIndex;
@@ -33,11 +35,29 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
private List<T> list;
/**
* Construct with pageIndex/pageSize.
*/
public LimitOffsetPagedList(EbeanServer server, SpiQuery<T> query, int pageIndex, int pageSize) {
this.server = server;
this.query = query;
this.pageSize = pageSize;
this.maxRows = pageSize;
this.firstRow = pageIndex * pageSize;
this.pageIndex = pageIndex;
query.setFirstRow(firstRow);
query.setMaxRows(pageSize);
}
/**
* Construct with firstRow/maxRows.
*/
public LimitOffsetPagedList(EbeanServer server, SpiQuery<T> query) {
this.server = server;
this.query = query;
this.maxRows = query.getMaxRows();
this.firstRow = query.getFirstRow();
this.pageIndex = 0;
}
public void loadRowCount() {
@@ -56,8 +76,6 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
public List<T> getList() {
synchronized (monitor) {
if (list == null) {
query.setFirstRow(pageIndex * pageSize);
query.setMaxRows(pageSize);
list = server.findList(query, null);
}
return list;
@@ -70,7 +88,7 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
if (rowCount == 0) {
return 0;
} else {
return ((rowCount - 1) / pageSize) + 1;
return ((rowCount - 1) / maxRows) + 1;
}
}
@@ -94,11 +112,11 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
}
public boolean hasNext() {
return pageIndex < (getTotalPageCount() - 1);
return (firstRow + maxRows) < getTotalRowCount();
}
public boolean hasPrev() {
return pageIndex > 0;
return firstRow > 0;
}
public int getPageIndex() {
@@ -106,13 +124,13 @@ public class LimitOffsetPagedList<T> implements PagedList<T> {
}
public int getPageSize() {
return pageSize;
return maxRows;
}
public String getDisplayXtoYofZ(String to, String of) {
int first = pageIndex * pageSize + 1;
int last = first + getList().size() - 1;
int first = firstRow + 1;
int last = firstRow + getList().size();
int total = getTotalRowCount();
return first + to + last + of + total;
@@ -89,6 +89,13 @@ public class SqlTree {
rootNode.addAsOfTableAlias(query);
}
/**
* Recurse through the tree adding soft delete predicates as necessary.
*/
public void addSoftDeletePredicate(SpiQuery<?> query) {
rootNode.addSoftDeletePredicate(query);
}
/**
* Build a select expression chain for RawSql.
*/
@@ -603,7 +603,7 @@ public class SqlTreeBuilder {
// no extra join required for embedded beans
return null;
}
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, assocProp);
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, assocProp, elGetValue.containsMany());
joinRegister.put(propertyName, extraJoin);
return extraJoin;
}
@@ -39,6 +39,11 @@ public interface SqlTreeNode {
*/
void addAsOfTableAlias(SpiQuery<?> query);
/**
* Recurse through the tree adding soft delete predicates if necessary.
*/
void addSoftDeletePredicate(SpiQuery<?> query);
/**
* Load the appropriate information from the SqlSelectReader.
* <p>
@@ -301,6 +301,9 @@ public class SqlTreeNodeBean implements SqlTreeNode {
if (readId && !temporalVersions) {
createListProxies(localDesc, ctx, localBean);
}
if (temporalMode == SpiQuery.TemporalMode.DRAFT) {
localDesc.setDraft(localBean);
}
localDesc.postLoad(localBean);
EntityBeanIntercept ebi = localBean._ebean_getIntercept();
@@ -480,6 +483,16 @@ public class SqlTreeNodeBean implements SqlTreeNode {
ctx.popJoin();
}
public void addSoftDeletePredicate(SpiQuery<?> query) {
if (desc.isSoftDelete()) {
query.addSoftDeletePredicate(desc.getSoftDeletePredicate(baseTableAlias));
}
for (int i = 0; i < children.length; i++) {
children[i].addSoftDeletePredicate(query);
}
}
public void addAsOfTableAlias(SpiQuery<?> query) {
// if history on this bean type add it's alias
// for each alias we add an effect date predicate
@@ -29,11 +29,14 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode {
private final boolean manyJoin;
private final boolean pathContainsMany;
private List<SqlTreeNodeExtraJoin> children;
public SqlTreeNodeExtraJoin(String prefix, BeanPropertyAssoc<?> assocBeanProperty) {
public SqlTreeNodeExtraJoin(String prefix, BeanPropertyAssoc<?> assocBeanProperty, boolean pathContainsMany) {
this.prefix = prefix;
this.assocBeanProperty = assocBeanProperty;
this.pathContainsMany = pathContainsMany;
this.manyJoin = assocBeanProperty instanceof BeanPropertyAssocMany<?>;
}
@@ -47,6 +50,11 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode {
// nothing to do here
}
@Override
public void addSoftDeletePredicate(SpiQuery<?> query) {
// nothing to do here
}
/**
* Return true if the extra join is a many join.
* <p>
@@ -90,13 +98,16 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode {
}
}
if (!manyToMany) {
if (pathContainsMany) {
// "promote" to left outer as the path contains a many
assocBeanProperty.addJoin(SqlJoinType.OUTER, prefix, ctx);
} else if (!manyToMany) {
assocBeanProperty.addJoin(joinType, prefix, ctx);
}
if (children != null) {
if (manyJoin) {
if (manyJoin || pathContainsMany) {
// if AUTO then make all descendants use OUTER JOIN
joinType = joinType.autoToOuter();
}
@@ -44,6 +44,11 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode {
// do nothing here ...
}
@Override
public void addSoftDeletePredicate(SpiQuery<?> query) {
// do nothing here ...
}
/**
* Append to the FROM clause for this node.
*/

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