Compare commits

..
Author SHA1 Message Date
rob bygrave 7ec5fe6a85 [maven-release-plugin] prepare release ebean-parent-12.6.6 2021-01-24 17:42:38 +13:00
rob bygrave 9ea5505214 Bump to ebean agent 12.6.6 2021-01-24 17:40:13 +13:00
Robin Bygrave 75d3b566a7 Merge branch 'FOCONIS-prePersist-postPersist-fix-batch-flush-12' 2021-01-22 14:01:19 +13:00
Robin Bygrave e38059f52c #2149 - Fix for ADD: jdbc batch flush doesnt execute sql statements created in lifecycle methods (e.g. beans saved in PostInsert etc)
Note that the foconis fork has a onPersist extension which is removed from the test.
2021-01-22 14:00:48 +13:00
Robin Bygrave 72331aa945 Merge branch 'prePersist-postPersist-fix-batch-flush-12' of https://github.com/FOCONIS/ebean into FOCONIS-prePersist-postPersist-fix-batch-flush-12 2021-01-22 13:14:05 +13:00
Robin Bygrave e497c54f82 No effective change - modify test redis ClusterTest to have 20ms wait 2021-01-21 17:01:36 +13:00
Robin Bygrave b44091ff3a #2147 Fix for ADD: multiple to many outer joins cause wrong count in distinct count…
Copy of Rolands fix in FOCONIS branch. This also brings over some of the extra tests found there.

Note that the SQL is slightly different from the FOCONIS branch in that there is additional foreign key columns included in the sub-query select clause.
2021-01-21 16:56:42 +13:00
Robin Bygrave 2ab889f889 Merge branch 'FOCONIS-count-distinct-with-two-joins-12' 2021-01-21 14:50:32 +13:00
Robin Bygrave 23541e661e Merge branch 'count-distinct-with-two-joins-12' of https://github.com/FOCONIS/ebean into FOCONIS-count-distinct-with-two-joins-12 2021-01-21 14:45:56 +13:00
trojo 6778e37652 ADD: batch flash doesnt execute sql statements created in lifecycle methods 2021-01-20 17:43:14 +01:00
rob bygrave 5eac31283d #2148 - PersistenceException: No ScalarType registered for class java.util.LinkedHashMap 2021-01-20 23:31:45 +13:00
trojo 38016773d4 added comments 2021-01-20 10:47:53 +01:00
trojo ad80eb267f ADD: multiple to many outer joins cause wrong count in distinct count queries 2021-01-20 09:57:25 +01:00
rob bygrave 449e0832e0 Merge branch 'FOCONIS-cache-with-lazyload-wrong-12' 2021-01-20 21:37:57 +13:00
rob bygrave f28f5c9a01 #2146 - no change - Tidy test only TestWithCacheAndLazyLoad 2021-01-20 21:37:29 +13:00
rob bygrave 9dd28d8879 #2146 - Fix for Cache with lazy load doesnt work
When the list is empty() it has already been loaded and loading should skip the l2 cache
2021-01-20 21:10:49 +13:00
rob bygrave b47eec99a5 Merge branch 'cache-with-lazyload-wrong-12' of https://github.com/FOCONIS/ebean into FOCONIS-cache-with-lazyload-wrong-12 2021-01-20 20:36:18 +13:00
Robin Bygrave 99bb55efa0 No change - update ebean-test test configuration only for windows 2021-01-20 17:22:00 +13:00
Robin Bygrave 4408eb618a No change - update test only - TestHistoryOneToMany with wait for when run on windows 2021-01-20 17:11:07 +13:00
rob bygrave 9ccd1b1b27 No effective change - tidy whitespace in SqlTreeBuilder and SqlTreeNodeExtraJoin 2021-01-20 00:10:09 +13:00
rob bygrave 0c1657fef5 #2145 - SoftDelete predicate missing on join to ManyToOne supporting predicate expression 2021-01-20 00:00:27 +13:00
Noemi Szemenyei ee393c6485 ADD: testcase for wrong lazyload with cache 2021-01-19 11:41:19 +01:00
rob bygrave 7f68b9d846 No effective change - tidy whitespace in BeanDescriptorCacheHelp 2021-01-19 22:48:29 +13:00
rob bygrave 2035a3344e Merge branch 'FOCONIS-persistcontext-wrong' 2021-01-19 22:37:45 +13:00
rob bygrave ade36582a0 #2144 - Fix for NPE via Wrong beans in PersistContext avoid proper lazy load 2021-01-19 22:37:15 +13:00
Roland Praml a4b8be61e3 ADD: some suggestions/debug hints 2021-01-11 11:50:25 +01:00
Roland Praml b7feb62ecd ADD: testcase for broken lazyload 2021-01-11 11:50:05 +01:00
rob bygrave 7e48f7c8eb [maven-release-plugin] prepare for next development iteration 2021-01-05 18:02:21 +13:00
rob bygrave 501f8111fe [maven-release-plugin] prepare release ebean-parent-12.6.5 2021-01-05 18:02:09 +13:00
rob bygrave d8f4503d98 #2141 - Fix for failing test, revert of fix for #2127 2021-01-05 17:56:55 +13:00
Rob BygraveandGitHub 0d83978c58 Merge pull request #2141 from tobias-/failing_cascade_test_case_2
Failing testcase that shows wipe of all old children on save
2021-01-05 17:30:56 +13:00
Tobias 69e56be459 Failing testcase that shows wipe of all old children on save
When adding a child to a newly saved bean (A), all other children are
killed on save of A.
2020-12-28 15:35:15 +01:00
rob bygrave 35a721039d [maven-release-plugin] prepare for next development iteration 2020-12-23 20:25:47 +13:00
rob bygrave 98b4ac5029 [maven-release-plugin] prepare release ebean-parent-12.6.4 2020-12-23 20:25:22 +13:00
rob bygrave fe874c946d #2140 - Bump ebean-migration dependency of ebean-ddl-generator to 12.4.0 (update ebean-test) 2020-12-23 20:22:50 +13:00
rob bygrave 53386e9bf4 Update ebean-core-type name and description only 2020-12-22 16:12:20 +13:00
rob bygrave 5435055f5c Add ebean-redis, ebean-postgis and ebean-core-type to ebean-bom 2020-12-22 16:08:59 +13:00
rob bygrave 583e10f5c6 #2139 - Fix for - Failing testcase that shows wipe of all children on save (related to #2127) 2020-12-22 15:49:37 +13:00
Rob BygraveandGitHub 5393d709a3 Merge pull request #2139 from tobias-/failing_cascade_test_case
Failing testcase that shows wipe of all children on save
2020-12-22 14:55:12 +13:00
Tobias fc1c9392aa Failing testcase that shows wipe of all children on save
When adding a newly saved bean (A) to another bean (B), all children
in A are wiped on saving B.
2020-12-21 15:32:40 +01:00
rob bygrave 6b2c48d4f6 #2135 - ebean doest not support Spring @Transactional(propagation = Propagation.REQUIRES_NEW) 2020-12-21 16:40:19 +13:00
rob bygrave 400ae3cf4b #2138 - Mark Transaction.flushBatch() as deprecated - migrate to flush(); 2020-12-21 14:58:04 +13:00
rob bygrave f22a322104 No effective change - Refactor tidy AbstractSqlQueryRequest after #2137
- Rename trans to transaction
- Tidy whitespace
2020-12-21 14:18:13 +13:00
rob bygrave e98ff952bf #2137 - Expected Transaction flush on SqlQuery and DtoQuery not occurring 2020-12-21 14:11:28 +13:00
rob bygrave 573e1b553c [maven-release-plugin] prepare for next development iteration 2020-12-18 15:04:05 +13:00
rob bygrave a674d0d696 [maven-release-plugin] prepare release ebean-parent-12.6.3 2020-12-18 15:03:53 +13:00
Rob BygraveandGitHub 5393161c16 Merge pull request #2132 from ebean-orm/feature/redisModule
Add ebean-redis as module
2020-12-18 14:58:07 +13:00
rob bygrave 1bdc6d8f7f #2134 - Modify ChangeLog to send the last set of changes Pre Commit (rather than post commit). Expose this for spring transactions. 2020-12-18 14:54:44 +13:00
rob bygrave e9081a176f #2133 - Spring transactions not getting ChangeLog changeSet as part of postCommit() processing 2020-12-18 14:14:43 +13:00
rob bygrave 3e7db2887f Add ebean-redis as module 2020-12-17 14:38:54 +13:00
rob bygrave fcf14ac222 Add ebean-redis as module 2020-12-17 11:56:34 +13:00
Rob BygraveandGitHub 55324653a5 #2127 - Adding a new list of children to @OneToMany with orphanRemoval true does not delete existing orphans (#2131) 2020-12-16 15:36:44 +13:00
SnōwballandGitHub a132f09b03 Ignore javax.persistence.Transient in query beans (#2129)
* Add failing tests for transient types

* Ignore jpa @Transient fields in query beans

Any field marked with jpa @Transient should be ignored
2020-12-15 12:40:06 +13:00
rob bygrave 48eac84930 #2128 - Bump avaje-config dependency to 1.3 - Fix for - Repeated calls to Config.get() with no value, expect to return passed default value 2020-12-14 09:31:08 +13:00
rob bygrave 948619a125 [maven-release-plugin] prepare for next development iteration 2020-12-09 13:11:19 +13:00
107 changed files with 3644 additions and 476 deletions
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean api</name>
@@ -32,7 +32,7 @@
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-config</artifactId>
<version>1.2</version>
<version>1.3</version>
</dependency>
<!--
@@ -519,11 +519,11 @@ public interface Transaction extends AutoCloseable {
void flush() throws PersistenceException;
/**
* This is a synonym for flush() and will be deprecated.
* Deprecated - migrate to flush().
* <p>
* flush() is preferred as it matches the JPA flush() method.
* </p>
*/
@Deprecated
void flushBatch() throws PersistenceException;
/**
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<!-- <parent>-->
<!-- <groupId>org.avaje</groupId>-->
@@ -14,7 +14,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-parent-12.6.2</tag>
<tag>ebean-parent-12.6.6</tag>
</scm>
<name>ebean autotune</name>
@@ -26,7 +26,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
+32 -14
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean bom</name>
@@ -18,8 +18,8 @@
<ebean-migration.version>12.4.0</ebean-migration.version>
<ebean-test-docker.version>4.1</ebean-test-docker.version>
<ebean-datasource.version>7.0</ebean-datasource.version>
<ebean-agent.version>12.6.2</ebean-agent.version>
<ebean-maven-plugin.version>12.6.2</ebean-maven-plugin.version>
<ebean-agent.version>12.6.6</ebean-agent.version>
<ebean-maven-plugin.version>12.6.6</ebean-maven-plugin.version>
</properties>
<dependencyManagement>
@@ -81,72 +81,90 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-externalmapping-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-externalmapping-xml</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-autotune</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>12.6.6</version>
</dependency>
</dependencies>
</dependencyManagement>
+4 -2
View File
@@ -4,10 +4,12 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<artifactId>ebean-core-type</artifactId>
<name>ebean core type</name>
<description>ebean scalar types api</description>
<properties>
<jackson-core.version>2.11.3</jackson-core.version>
@@ -19,7 +21,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -15,7 +15,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-parent-12.6.2</tag>
<tag>ebean-parent-12.6.6</tag>
</scm>
<properties>
@@ -70,7 +70,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>12.2.0</version>
<version>12.4.0</version>
<scope>test</scope>
</dependency>
@@ -92,19 +92,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-externalmapping-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
@@ -301,7 +301,7 @@
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>12.5.0</version>
<version>12.6.6</version>
<executions>
<execution>
<id>test</id>
@@ -318,4 +318,20 @@ public interface SpiTransaction extends Transaction {
* Return true if explicitly set to skip cache (ignores skipOnWrite).
*/
boolean isSkipCacheExplicit();
/**
* Fire pre commit processing/listeners.
*/
void preCommit();
/**
* Fire post commit events and listeners.
*/
void postCommit();
/**
* Fire post rollback events and listeners.
*/
void postRollback(Throwable cause);
}
@@ -418,4 +418,18 @@ public abstract class SpiTransactionProxy implements SpiTransaction {
transaction.flushBatchOnCollection();
}
@Override
public void preCommit() {
transaction.preCommit();
}
@Override
public void postCommit() {
transaction.postCommit();
}
@Override
public void postRollback(Throwable cause) {
transaction.postRollback(cause);
}
}
@@ -27,7 +27,7 @@ public abstract class AbstractSqlQueryRequest {
protected final SpiEbeanServer server;
protected SpiTransaction trans;
protected SpiTransaction transaction;
private boolean createdTransaction;
@@ -47,18 +47,18 @@ public abstract class AbstractSqlQueryRequest {
AbstractSqlQueryRequest(SpiEbeanServer server, SpiSqlBinding query, Transaction t) {
this.server = server;
this.query = query;
this.trans = (SpiTransaction) t;
this.transaction = (SpiTransaction) t;
}
/**
* Create a transaction if none currently exists.
*/
public void initTransIfRequired() {
if (trans == null) {
trans = server.currentServerTransaction();
if (trans == null || !trans.isActive()) {
if (transaction == null) {
transaction = server.currentServerTransaction();
if (transaction == null || !transaction.isActive()) {
// create a local readOnly transaction
trans = server.createReadOnlyTransaction(null);
transaction = server.createReadOnlyTransaction(null);
createdTransaction = true;
}
}
@@ -69,20 +69,18 @@ public abstract class AbstractSqlQueryRequest {
*/
public void endTransIfRequired() {
if (createdTransaction) {
trans.commit();
transaction.commit();
}
}
public EbeanServer getServer() {
return server;
}
public SpiTransaction getTransaction() {
return trans;
protected void flushJdbcBatchOnQuery() {
if (transaction.isFlushOnQuery()) {
transaction.flush();
}
}
public boolean isLogSql() {
return trans.isLogSql();
return transaction.isLogSql();
}
/**
@@ -120,7 +118,6 @@ public abstract class AbstractSqlQueryRequest {
* Prepare the SQL taking into account named bind parameters.
*/
private void prepareSql() {
String sql = query.getQuery();
BindParams bindParams = query.getBindParams();
if (!bindParams.isEmpty()) {
@@ -131,7 +128,6 @@ public abstract class AbstractSqlQueryRequest {
}
private String limitOffset(String sql) {
int firstRow = query.getFirstRow();
int maxRows = query.getMaxRows();
if (firstRow > 0 || maxRows > 0) {
@@ -149,10 +145,8 @@ public abstract class AbstractSqlQueryRequest {
}
protected void executeAsSql(Binder binder) throws SQLException {
prepareSql();
Connection conn = trans.getInternalConnection();
Connection conn = transaction.getInternalConnection();
pstmt = conn.prepareStatement(sql);
if (query.getTimeout() > 0) {
pstmt.setQueryTimeout(query.getTimeout());
@@ -160,14 +154,12 @@ public abstract class AbstractSqlQueryRequest {
if (query.getBufferFetchSizeHint() > 0) {
pstmt.setFetchSize(query.getBufferFetchSizeHint());
}
BindParams bindParams = query.getBindParams();
if (!bindParams.isEmpty()) {
this.bindLog = binder.bind(bindParams, pstmt, conn);
}
if (isLogSql()) {
trans.logSql(Str.add(TrimLogSql.trim(sql), "; --bind(", bindLog, ")"));
transaction.logSql(Str.add(TrimLogSql.trim(sql), "; --bind(", bindLog, ")"));
}
setResultSet(pstmt.executeQuery(), null);
@@ -215,7 +215,6 @@ class DefaultBeanLoader {
desc.contextPut(pc, id, bean);
ebi.setPersistenceContext(pc);
}
boolean draft = desc.isDraftInstance(bean);
if (embeddedOwnerIndex == -1) {
@@ -245,7 +244,6 @@ class DefaultBeanLoader {
// and put the data into the original bean
query.setUsageProfiling(false);
query.setPersistenceContext(pc);
query.setMode(mode);
query.setId(id);
@@ -50,7 +50,7 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
ormQuery.setManualId();
// execute the underlying ORM query returning the ResultSet
SpiResultSet result = server.findResultSet(ormQuery, trans);
SpiResultSet result = server.findResultSet(ormQuery, transaction);
this.pstmt = result.getStatement();
this.sql = ormQuery.getGeneratedSql();
setResultSet(result.getResultSet(), ormQuery.getQueryPlanKey());
@@ -88,14 +88,17 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
}
public void findEach(Consumer<T> consumer) {
flushJdbcBatchOnQuery();
queryEngine.findEach(this, consumer);
}
public void findEachWhile(Predicate<T> consumer) {
flushJdbcBatchOnQuery();
queryEngine.findEachWhile(this, consumer);
}
public List<T> findList() {
flushJdbcBatchOnQuery();
return queryEngine.findList(this);
}
@@ -388,7 +388,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
@Override
public void preGetterTrigger(int propertyIndex) {
if (flushBatchOnGetter(propertyIndex)) {
transaction.flushBatch();
transaction.flush();
}
}
@@ -73,13 +73,13 @@ public final class PersistRequestUpdateSql extends PersistRequest {
/**
* Add this request to BatchControl to flush later.
*/
public void addToFlushQueue(boolean early) {
public void addToFlushQueue(int pos) {
BatchControl control = transaction.getBatchControl();
if (control == null) {
control = persistExecute.createBatchControl(transaction);
}
flushQueue = true;
control.addToFlushQueue(this, early);
control.addToFlushQueue(this, pos);
}
@Override
@@ -104,18 +104,14 @@ public interface Persister {
void executeOrQueue(SpiSqlUpdate update, SpiTransaction t, boolean queue);
/**
* Queue the SqlUpdate for early execution (with JDBC batch).
* Queue the SqlUpdate for execution with position 0, 1 or 2 defining
* when it executes relative to the flush of beans .
*/
void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t);
void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t, int pos);
/**
* Queue the SqlUpdate for late execution (with JDBC batch).
* Add the statement to JDBC batch for later execution via executeBatch.
*/
void addToFlushQueueLast(SpiSqlUpdate update, SpiTransaction t);
/**
* Add the statement to JDBC batch for later execution via executeBatch.
*/
void addBatch(SpiSqlUpdate sqlUpdate, SpiTransaction transaction);
/**
@@ -56,35 +56,43 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
}
boolean findEachRow(RowConsumer mapper) {
flushJdbcBatchOnQuery();
queryEngine.findEachRow(this, mapper);
return true;
}
<T> List<T> findListMapper(RowMapper<T> mapper) {
flushJdbcBatchOnQuery();
return queryEngine.findListMapper(this, mapper);
}
<T> T findOneMapper(RowMapper<T> mapper) {
flushJdbcBatchOnQuery();
return queryEngine.findOneMapper(this, mapper);
}
public <T> List<T> findSingleAttributeList(Class<T> cls) {
flushJdbcBatchOnQuery();
return queryEngine.findSingleAttributeList(this, cls);
}
public <T> T findSingleAttribute(Class<T> cls) {
flushJdbcBatchOnQuery();
return queryEngine.findSingleAttribute(this, cls);
}
public void findEach(Consumer<SqlRow> consumer) {
flushJdbcBatchOnQuery();
queryEngine.findEach(this, consumer);
}
public void findEachWhile(Predicate<SqlRow> consumer) {
flushJdbcBatchOnQuery();
queryEngine.findEach(this, consumer);
}
public List<SqlRow> findList() {
flushJdbcBatchOnQuery();
return queryEngine.findList(this);
}
@@ -92,9 +100,7 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
* Build the list of property names.
*/
private String[] getPropertyNames() throws SQLException {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnsPlusOne = metaData.getColumnCount() + 1;
ArrayList<String> propNames = new ArrayList<>(columnsPlusOne - 1);
for (int i = 1; i < columnsPlusOne; i++) {
@@ -107,9 +113,7 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
* Read and return the next SqlRow.
*/
public SqlRow createNewRow() throws SQLException {
rows++;
SqlRow sqlRow = queryEngine.createSqlRow(estimateCapacity);
int index = 0;
for (String propertyName : propertyNames) {
@@ -121,9 +125,9 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
}
public void logSummary() {
if (trans.isLogSummary()) {
if (transaction.isLogSummary()) {
long micros = (System.nanoTime() - startNano) / 1000L;
trans.logSummary("SqlQuery rows[" + rows + "] micros[" + micros + "] bind[" + bindLog + "]");
transaction.logSummary("SqlQuery rows[" + rows + "] micros[" + micros + "] bind[" + bindLog + "]");
}
}
@@ -136,7 +140,6 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
}
public <T> List<T> mapList(RowMapper<T> mapper) throws SQLException {
List<T> list = new ArrayList<>();
while (next()) {
list.add(mapper.map(resultSet, rows++));
@@ -1872,6 +1872,17 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
return query.setId(id).findOne();
}
/**
* Create a reference with a check for the bean in the persistence context.
*/
public EntityBean createReference(Boolean readOnly, Object id, PersistenceContext pc) {
Object refBean = contextGet(pc, id);
if (refBean == null) {
refBean = createReference(readOnly, false, id, pc);
}
return (EntityBean)refBean;
}
/**
* Create a reference bean based on the id.
*/
@@ -89,7 +89,6 @@ final class BeanDescriptorCacheHelp<T> {
BeanDescriptorCacheHelp(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
this.desc = desc;
this.beanType = desc.rootBeanType;
this.cacheName = beanType.getSimpleName();
@@ -99,7 +98,6 @@ final class BeanDescriptorCacheHelp<T> {
this.cacheSharableBeans = cacheSharableBeans;
this.propertiesOneImported = propertiesOneImported;
this.naturalKey = cacheOptions.getNaturalKey();
if (!cacheOptions.isEnableQueryCache()) {
this.queryCache = null;
} else {
@@ -133,7 +131,6 @@ final class BeanDescriptorCacheHelp<T> {
void deriveNotifyFlags() {
cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null);
cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes();
if (logger.isDebugEnabled()) {
if (cacheNotifyOnAll || cacheNotifyOnDelete) {
String notifyMode = cacheNotifyOnAll ? "All" : "Delete";
@@ -292,7 +289,6 @@ final class BeanDescriptorCacheHelp<T> {
* Try to load the bean collection from cache return true if successful.
*/
boolean manyPropLoad(BeanPropertyAssocMany<?> many, BeanCollection<?> bc, Object parentId, Boolean readOnly) {
if (many.isElementCollection()) {
// held as part of the bean cache so skip
return false;
@@ -307,14 +303,12 @@ final class BeanDescriptorCacheHelp<T> {
EntityBean ownerBean = bc.getOwnerBean();
EntityBeanIntercept ebi = ownerBean._ebean_getIntercept();
PersistenceContext persistenceContext = ebi.getPersistenceContext();
BeanDescriptor<?> targetDescriptor = many.getTargetDescriptor();
List<Object> idList = entry.getIdList();
bc.checkEmptyLazyLoad();
for (Object id : idList) {
Object refBean = targetDescriptor.createReference(readOnly, false, id, persistenceContext);
many.add(bc, (EntityBean) refBean);
many.add(bc, targetDescriptor.createReference(readOnly, id, persistenceContext));
}
return true;
}
@@ -323,7 +317,6 @@ final class BeanDescriptorCacheHelp<T> {
* Put the beanCollection into the cache.
*/
void manyPropPut(BeanPropertyAssocMany<?> many, Object details, Object parentId) {
if (many.isElementCollection()) {
CachedBeanData data = (CachedBeanData) beanCache.get(parentId);
if (data != null) {
@@ -351,7 +344,6 @@ final class BeanDescriptorCacheHelp<T> {
}
void cachePutManyIds(Object parentId, String manyName, CachedManyIds entry) {
ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, manyName);
if (manyLog.isDebugEnabled()) {
manyLog.debug(" PUT {}({}).{} - ids:{}", cacheName, parentId, manyName, entry);
@@ -360,7 +352,6 @@ final class BeanDescriptorCacheHelp<T> {
}
private CachedManyIds createManyIds(BeanPropertyAssocMany<?> many, Object details) {
Collection<?> actualDetails = BeanCollectionUtil.getActualDetails(details);
if (actualDetails == null) {
return null;
@@ -378,7 +369,6 @@ final class BeanDescriptorCacheHelp<T> {
* Hit the bean cache with the given ids returning the hits.
*/
BeanCacheResult<T> cacheIdLookup(PersistenceContext context, Collection<?> ids) {
Set<Object> keys = new HashSet<>(ids.size());
for (Object id : ids) {
keys.add(desc.cacheKey(id));
@@ -395,7 +385,6 @@ final class BeanDescriptorCacheHelp<T> {
T bean = convertToBean(entry.getKey(), false, context, cachedBeanData);
result.add(bean, desc.getBeanId(bean));
}
return result;
}
@@ -403,14 +392,12 @@ final class BeanDescriptorCacheHelp<T> {
* Use natural keys to hit the bean cache and return resulting hits.
*/
BeanCacheResult<T> naturalKeyLookup(PersistenceContext context, Set<Object> keys) {
if (context == null) {
context = new DefaultPersistenceContext();
}
// naturalKey -> Id map
Map<Object, Object> naturalKeyMap = naturalKeyCache.getAll(keys);
if (natLog.isTraceEnabled()) {
natLog.trace(" MLOOKUP {}({}) - hits:{}", cacheName, keys, naturalKeyMap);
}
@@ -433,15 +420,12 @@ final class BeanDescriptorCacheHelp<T> {
}
// process the hits into beans etc
for (Map.Entry<Object, Object> entry : beanDataMap.entrySet()) {
Object id = entry.getKey();
CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue();
T bean = convertToBean(id, false, context, cachedBeanData);
Object naturalKey = reverseMap.get(id);
result.add(bean, naturalKey);
}
return result;
}
@@ -452,7 +436,6 @@ final class BeanDescriptorCacheHelp<T> {
if (context == null) {
context = new DefaultPersistenceContext();
}
// Not using a loadContext for beans coming out of L2 cache
// so that means no batch lazy loading for these beans
EntityBean entityBean = (EntityBean) bean;
@@ -522,7 +505,6 @@ final class BeanDescriptorCacheHelp<T> {
* Put a bean into the bean cache.
*/
void beanCachePut(EntityBean bean) {
if (desc.inheritInfo != null) {
desc.descOf(bean.getClass()).cacheBeanPutDirect(bean);
} else {
@@ -531,7 +513,6 @@ final class BeanDescriptorCacheHelp<T> {
}
void beanCachePutAllDirect(Collection<EntityBean> beans) {
Map<Object, Object> natKeys = null;
if (naturalKey != null) {
natKeys = new LinkedHashMap<>();
@@ -566,15 +547,12 @@ final class BeanDescriptorCacheHelp<T> {
* Put the bean into the bean cache.
*/
void beanCachePutDirect(EntityBean bean) {
CachedBeanData beanData = beanExtractData(desc, bean);
String key = desc.cacheKeyForBean(bean);
if (beanLog.isDebugEnabled()) {
beanLog.debug(" PUT {}({}) data:{}", cacheName, key, beanData);
}
getBeanCache().put(key, beanData);
if (naturalKey != null) {
String naturalKey = calculateNaturalKey(beanData);
if (naturalKey != null) {
@@ -618,7 +596,6 @@ final class BeanDescriptorCacheHelp<T> {
* Return a bean from the bean cache.
*/
private T beanCacheGetInternal(String key, Boolean readOnly, PersistenceContext context) {
CachedBeanData data = (CachedBeanData) getBeanCache().get(key);
if (data == null) {
if (beanLog.isTraceEnabled()) {
@@ -646,7 +623,6 @@ final class BeanDescriptorCacheHelp<T> {
return (T) bean;
}
}
return (T) loadBean(id, readOnly, data, context);
}
@@ -654,7 +630,6 @@ final class BeanDescriptorCacheHelp<T> {
* Load the entity bean taking into account inheritance.
*/
private EntityBean loadBean(Object id, Boolean readOnly, CachedBeanData data, PersistenceContext context) {
String discValue = data.getDiscValue();
if (discValue == null) {
return loadBeanDirect(id, readOnly, data, context);
@@ -674,7 +649,6 @@ final class BeanDescriptorCacheHelp<T> {
* Load the entity bean from cache data given this is the root bean type.
*/
EntityBean loadBeanDirect(Object id, Boolean readOnly, CachedBeanData data, PersistenceContext context) {
id = desc.convertId(id);
EntityBean bean = null;
if (context == null) {
@@ -682,7 +656,6 @@ final class BeanDescriptorCacheHelp<T> {
} else {
bean = (EntityBean) desc.contextGet(context, id);
}
if (bean == null) {
bean = desc.createEntityBean();
desc.setId(id, bean);
@@ -698,7 +671,6 @@ final class BeanDescriptorCacheHelp<T> {
}
CachedBeanDataToBean.load(desc, bean, data, context);
if (desc.isReadAuditing()) {
desc.readAuditBean("l2", "", bean);
}
@@ -709,7 +681,6 @@ final class BeanDescriptorCacheHelp<T> {
* Load the embedded bean checking for inheritance.
*/
EntityBean embeddedBeanLoad(CachedBeanData data, PersistenceContext context) {
String discValue = data.getDiscValue();
if (discValue == null) {
return embeddedBeanLoadDirect(data, context);
@@ -746,25 +717,20 @@ final class BeanDescriptorCacheHelp<T> {
* Load a batch of entities from L2 bean cache checking the lazy loaded property is loaded.
*/
Set<EntityBeanIntercept> beanCacheLoadAll(List<EntityBeanIntercept> list, PersistenceContext context, int lazyLoadProperty, String propertyName) {
Map<Object, EntityBeanIntercept> ebis = new HashMap<>();
for (EntityBeanIntercept ebi : list) {
ebis.put(desc.cacheKeyForBean(ebi.getOwner()), ebi);
}
Map<Object, Object> hits = getBeanCache().getAll(ebis.keySet());
if (beanLog.isTraceEnabled()) {
beanLog.trace(" MLOAD {}({}) - got hits ({})", cacheName, ebis.keySet(), hits.size());
}
Set<EntityBeanIntercept> loaded = new HashSet<>();
Iterator<Map.Entry<Object, Object>> iterator = hits.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Object, Object> hit = iterator.next();
Object key = hit.getKey();
EntityBeanIntercept ebi = ebis.remove(key);
CachedBeanData cacheData = (CachedBeanData) hit.getValue();
@@ -774,7 +740,6 @@ final class BeanDescriptorCacheHelp<T> {
beanLog.trace(" load {}({}) - cache miss on property({})", cacheName, key, propertyName);
}
iterator.remove();
} else {
CachedBeanDataToBean.load(desc, ebi.getOwner(), cacheData, context);
loaded.add(ebi);
@@ -783,11 +748,9 @@ final class BeanDescriptorCacheHelp<T> {
}
}
}
if (!ebis.isEmpty() && beanLog.isTraceEnabled()) {
beanLog.trace(" load {}({}) - cache miss", cacheName, ebis.keySet());
}
return loaded;
}
@@ -795,7 +758,6 @@ final class BeanDescriptorCacheHelp<T> {
* Returns true if it managed to populate/load the single bean from the cache.
*/
boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, String key, PersistenceContext context) {
CachedBeanData cacheData = (CachedBeanData) getBeanCache().get(key);
if (cacheData == null) {
if (beanLog.isTraceEnabled()) {
@@ -810,7 +772,6 @@ final class BeanDescriptorCacheHelp<T> {
}
return false;
}
CachedBeanDataToBean.load(desc, bean, cacheData, context);
if (beanLog.isDebugEnabled()) {
beanLog.debug(" LOAD {}({}) - hit", cacheName, key);
@@ -932,7 +893,6 @@ final class BeanDescriptorCacheHelp<T> {
* Apply changes to the bean cache entry.
*/
void cacheBeanUpdate(String key, Map<String, Object> changes, boolean updateNaturalKey, long version) {
ServerCache cache = getBeanCache();
CachedBeanData existingData = (CachedBeanData) cache.get(key);
if (existingData != null) {
@@ -952,7 +912,6 @@ final class BeanDescriptorCacheHelp<T> {
}
cache.put(key, newData);
}
if (updateNaturalKey) {
Object oldKey = calculateNaturalKey(existingData);
if (oldKey != null) {
@@ -262,10 +262,16 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
/**
* Return true if the target side has soft delete.
*/
@Override
public boolean isTargetSoftDelete() {
return targetDescriptor.isSoftDelete();
}
@Override
public String getSoftDeletePredicate(String tableAlias) {
return targetDescriptor.getSoftDeletePredicate(tableAlias);
}
/**
* Return true if REFRESH should cascade.
*/
@@ -56,7 +56,6 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
}
private void configureQuery(SpiQuery<?> query, String lazyLoadProperty) {
if (cache) {
query.setBeanCacheMode(CacheMode.ON);
}
@@ -70,7 +69,6 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
}
protected void register(EntityBeanIntercept ebi) {
if (currentBuffer.isFull()) {
currentBuffer = createBuffer(secondaryBatchSize);
}
@@ -198,8 +196,10 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
// lazy load property was a Many
return;
}
if (context.hitCache) {
if (list.isEmpty()) {
// re-add to the batch and lazy load from DB skipping l2 cache
list.add(ebi);
} else if (context.hitCache) {
Set<EntityBeanIntercept> hits = context.desc.cacheBeanLoadAll(list, persistenceContext, ebi.getLazyLoadPropertyIndex(), ebi.getLazyLoadProperty());
list.removeAll(hits);
if (list.isEmpty() || hits.contains(ebi)) {
@@ -81,8 +81,7 @@ public final class BatchControl {
*/
private int bufferMax;
private Queue earlyQueue;
private Queue lateQueue;
private Queue[] queues = new Queue[3];
/**
* Create for a given transaction, PersistExecute, default size and getGeneratedKeys.
@@ -271,9 +270,10 @@ public final class BatchControl {
}
private void flushBuffer(boolean reset) throws BatchedSqlException {
flushQueue(queues[0]);
flushInternal(reset);
flushQueue(earlyQueue);
flushQueue(lateQueue);
flushQueue(queues[1]);
flushQueue(queues[2]);
}
private void flushQueue(Queue queue) throws BatchedSqlException {
@@ -298,18 +298,7 @@ public final class BatchControl {
// Nothing in queue to flush
return;
}
// convert entry map to array for sorting
BatchedBeanHolder[] bsArray = getBeanHolderArray();
// sort the entries by depth
Arrays.sort(bsArray, depthComparator);
if (transaction.isLogSummary()) {
transaction.logSummary("BatchControl flush " + Arrays.toString(bsArray));
}
for (BatchedBeanHolder beanHolder : bsArray) {
beanHolder.executeNow();
}
executeAll();
persistedBeans.clear();
if (reset) {
beanHoldMap.clear();
@@ -323,6 +312,32 @@ public final class BatchControl {
}
}
private void executeAll() throws BatchedSqlException {
do {
// convert entry map to array for sorting
BatchedBeanHolder[] bsArray = getBeanHolderArray();
Arrays.sort(bsArray, depthComparator);
if (transaction.isLogSummary()) {
transaction.logSummary("BatchControl flush " + Arrays.toString(bsArray));
}
for (BatchedBeanHolder beanHolder : bsArray) {
beanHolder.executeNow();
}
} while (!isBeanHoldersEmpty());
}
/**
* Return if all bean holders are empty.
*/
private boolean isBeanHoldersEmpty() {
for (BatchedBeanHolder beanHolder : beanHoldMap.values()) {
if (!beanHolder.isEmpty()) {
return false;
}
}
return true;
}
/**
* Return an entry for the given type description. The type description is
* typically the bean class name (or table name for MapBeans).
@@ -368,20 +383,11 @@ public final class BatchControl {
/**
* Add a SqlUpdate request to execute after flush.
*/
public void addToFlushQueue(PersistRequestUpdateSql request, boolean early) {
if (early) {
// add it to the early queue
if (earlyQueue == null) {
earlyQueue = new Queue();
}
earlyQueue.add(request);
} else {
// add it to the late queue
if (lateQueue == null) {
lateQueue = new Queue();
}
lateQueue.add(request);
public void addToFlushQueue(PersistRequestUpdateSql request, int pos) {
if (queues[pos] == null) {
queues[pos] = new Queue();
}
queues[pos].add(request);
}
private static class Queue {
@@ -47,6 +47,11 @@ class BatchedBeanHolder {
*/
private ArrayList<PersistRequest> deletes;
/**
* The holder is empty
*/
private boolean empty = true;
/**
* Create a new entry with a given type and depth.
*/
@@ -63,6 +68,13 @@ class BatchedBeanHolder {
return order;
}
/**
* Returns if the BeanHolder is empty.
*/
public boolean isEmpty() {
return empty;
}
/**
* Execute all the persist requests in this entry.
* <p>
@@ -90,6 +102,7 @@ class BatchedBeanHolder {
updates = new ArrayList<>();
control.executeNow(bufferedUpdates);
}
empty = true;
}
@Override
@@ -112,9 +125,8 @@ class BatchedBeanHolder {
* Add the request to the appropriate persist list.
*/
public int append(PersistRequestBean<?> request) {
empty = false;
request.setBatched();
switch (request.getType()) {
case INSERT:
if (inserts == null) {
@@ -141,24 +141,17 @@ public final class DefaultPersister implements Persister {
@Override
public void executeOrQueue(SpiSqlUpdate update, SpiTransaction t, boolean queue) {
if (queue) {
addToFlushQueue(update, t, false);
addToFlushQueue(update, t, 2);
} else {
executeSqlUpdate(update, t);
}
}
@Override
public void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t) {
addToFlushQueue(update, t, true);
}
@Override
public void addToFlushQueueLast(SpiSqlUpdate update, SpiTransaction t) {
addToFlushQueue(update, t, false);
}
private void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t, boolean early) {
new PersistRequestUpdateSql(server, update, t, persistExecute).addToFlushQueue(early);
/**
* Add to the flush queue in position 0, 1 or 2.
*/
public void addToFlushQueue(SpiSqlUpdate update, SpiTransaction t, int pos) {
new PersistRequestUpdateSql(server, update, t, persistExecute).addToFlushQueue(pos);
}
/**
@@ -963,7 +956,7 @@ public final class DefaultPersister implements Persister {
void deleteManyIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, SpiTransaction t, boolean publish, boolean queue) {
SpiSqlUpdate sqlDelete = deleteAllIntersection(bean, many, publish);
if (queue) {
addToFlushQueue(sqlDelete, t, true);
addToFlushQueue(sqlDelete, t, 1);
} else {
executeSqlUpdate(sqlDelete, t);
}
@@ -1237,7 +1230,6 @@ public final class DefaultPersister implements Persister {
@SuppressWarnings({"unchecked"})
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, Object parentBean, BeanManager<?> mgr,
PersistRequest.Type type, int flags) {
// no delete requests come here
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, flags);
}
@@ -1252,7 +1244,6 @@ public final class DefaultPersister implements Persister {
@SuppressWarnings({"unchecked"})
private <T> PersistRequestBean<T> createDeleteRequest(Object bean, Transaction t, PersistRequest.Type type, int flags) {
BeanManager<T> mgr = getBeanManager(bean);
if (type == Type.DELETE_PERMANENT) {
type = Type.DELETE;
@@ -1281,7 +1272,6 @@ public final class DefaultPersister implements Persister {
*/
@SuppressWarnings("unchecked")
private <T> BeanManager<T> getBeanManager(Object bean) {
BeanManager<T> mgr = (BeanManager<T>) beanDescriptorManager.getBeanManager(bean.getClass());
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
@@ -56,7 +56,7 @@ class MergeNodeAssocManyToMany extends MergeNode {
IntersectionTable intersectionTable = many.intersectionTable();
if (!deletions.isEmpty()) {
transaction.flushBatch();
transaction.flush();
SqlUpdate delete = intersectionTable.delete(server, false);
for (EntityBean deletion : deletions) {
@@ -67,7 +67,7 @@ class MergeNodeAssocManyToMany extends MergeNode {
}
if (!additions.isEmpty()) {
transaction.flushBatch();
transaction.flush();
SqlUpdate insert = intersectionTable.insert(server, false);
for (EntityBean addition : additions) {
@@ -49,7 +49,7 @@ abstract class SaveManyBase implements SaveMany {
void preElementCollectionUpdate() {
if (!insertedParent) {
request.preElementCollectionUpdate();
persister.addToFlushQueue(many.deleteByParentId(request.getBeanId(), null), transaction);
persister.addToFlushQueue(many.deleteByParentId(request.getBeanId(), null), transaction, 1);
}
}
@@ -37,7 +37,7 @@ public class SaveManyBeans extends SaveManyBase {
private final boolean saveRecurseSkippable;
private final DeleteMode deleteMode;
private final boolean untouchedBeanCollection;
private Collection<?> collection;
private final Collection<?> collection;
private int sortOrder;
SaveManyBeans(DefaultPersister persister, boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
@@ -49,6 +49,7 @@ public class SaveManyBeans extends SaveManyBase {
this.saveRecurseSkippable = many.isSaveRecurseSkippable();
this.deleteMode = targetDescriptor.isSoftDelete() ? DeleteMode.SOFT : DeleteMode.HARD;
this.untouchedBeanCollection = untouchedBeanCollection();
this.collection = cascade ? BeanCollectionUtil.getActualEntries(value) : null;
}
/**
@@ -108,7 +109,6 @@ public class SaveManyBeans extends SaveManyBase {
private void saveAssocManyDetails() {
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
collection = BeanCollectionUtil.getActualEntries(value);
if (collection != null) {
processDetails();
}
@@ -210,6 +210,18 @@ public class SaveManyBeans extends SaveManyBase {
return true;
}
private boolean hasNewOrDirtyBeans() {
if (collection == null) {
return false;
}
for (Object bean : collection) {
if (bean instanceof EntityBean && ((EntityBean) bean)._ebean_getIntercept().isNewOrDirty()) {
return true;
}
}
return false;
}
/**
* Collect the Id values of the details to remove 'missing children' for stateless updates.
*/
@@ -235,9 +247,6 @@ public class SaveManyBeans extends SaveManyBase {
/**
* Save the additions and removals from a ManyToMany collection as inserts
* and deletes from the intersection table.
* <p>
* This is done via MapBeans.
* </p>
*/
private void saveAssocManyIntersection() {
if (value == null) {
@@ -336,9 +345,14 @@ public class SaveManyBeans extends SaveManyBase {
}
private void removeAssocManyOrphans() {
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
if (value instanceof BeanCollection<?>) {
if (value == null) {
return;
}
if (!(value instanceof BeanCollection<?>)) {
// if (!insertedParent && cascade && hasNewOrDirtyBeans()) {
// persister.addToFlushQueue(many.deleteByParentId(request.getBeanId(), null), transaction, 0);
// }
} else {
BeanCollection<?> c = (BeanCollection<?>) value;
Set<?> modifyRemovals = c.getModifyRemovals();
if (insertedParent) {
@@ -48,7 +48,7 @@ class SaveManyElementCollection extends SaveManyBase {
final SpiSqlUpdate sqlInsert = proto.copy();
sqlInsert.setParameter(parentId);
many.bindElementValue(sqlInsert, value);
persister.addToFlushQueueLast(sqlInsert, transaction);
persister.addToFlushQueue(sqlInsert, transaction, 2);
}
resetModifyState();
postElementCollectionUpdate();
@@ -51,7 +51,7 @@ class SaveManyElementCollectionMap extends SaveManyBase {
sqlInsert.setParameter(parentId);
sqlInsert.setParameter(entry.getKey());
many.bindElementValue(sqlInsert, entry.getValue());
persister.addToFlushQueueLast(sqlInsert, transaction);
persister.addToFlushQueue(sqlInsert, transaction, 2);
}
resetModifyState();
postElementCollectionUpdate();
@@ -571,8 +571,8 @@ class CQueryBuilder {
private final CQueryPredicates predicates;
private final SqlTree select;
private final boolean updateStatement;
private final boolean distinct;
private final boolean countSingleAttribute;
private final String dbOrderBy;
private boolean useSqlLimiter;
private boolean hasWhere;
@@ -590,6 +590,7 @@ class CQueryBuilder {
this.updateStatement = updateStatement;
this.distinct = query.isDistinct() || select.isSqlDistinct();
this.dbOrderBy = predicates.getDbOrderBy();
this.countSingleAttribute = query.isCountDistinct() && query.isSingleAttribute();
}
private void appendSelect() {
@@ -601,8 +602,13 @@ class CQueryBuilder {
if (!useSqlLimiter) {
appendSelectDistinct();
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
sb.append("r1.attribute_, count(*) from (select ").append(select.getSelectSql()).append(" as attribute_");
if (countSingleAttribute) {
sb.append("r1.attribute_, count(*) from (select ");
if (distinct) {
sb.append("distinct t0.");
sb.append(request.getBeanDescriptor().getIdProperty().getDbColumn()).append(", ");
}
sb.append(select.getSelectSql()).append(" as attribute_");
} else {
sb.append(select.getSelectSql());
}
@@ -621,7 +627,7 @@ class CQueryBuilder {
private void appendSelectDistinct() {
sb.append("select ");
if (distinct) {
if (distinct && !countSingleAttribute) {
if (request.isInlineCountDistinct()) {
sb.append("count(");
}
@@ -730,7 +736,7 @@ class CQueryBuilder {
sb.append(" order by ").append(dbOrderBy);
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
if (countSingleAttribute) {
sb.append(") r1 group by r1.attribute_");
sb.append(toSql(query.getCountDistinctOrder()));
}
@@ -58,7 +58,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
// before we perform a query, we need to flush any
// previous persist requests that are queued/batched.
// The query may read data affected by those requests.
t.flushBatch();
t.flush();
}
}
@@ -36,4 +36,13 @@ public interface STreePropertyAssoc extends STreeProperty {
*/
void setValue(EntityBean parentBean, Object contextBean);
/**
* Return true if the associated type has soft delete.
*/
boolean isTargetSoftDelete();
/**
* Return the soft delete predicate.
*/
String getSoftDeletePredicate(String tableAlias);
}
@@ -74,7 +74,6 @@ public final class SqlTreeBuilder {
* Construct for RawSql query.
*/
SqlTreeBuilder(OrmQueryRequest<?> request, CQueryPredicates predicates, OrmQueryDetail queryDetail, boolean rawNoId) {
this.rawSql = true;
this.desc = request.getBeanDescriptor();
this.rawNoId = rawNoId;
@@ -97,7 +96,6 @@ public final class SqlTreeBuilder {
* to the root node.
*/
SqlTreeBuilder(String columnAliasPrefix, CQueryBuilder builder, OrmQueryRequest<?> request, CQueryPredicates predicates) {
this.rawSql = false;
this.rawNoId = false;
this.desc = request.getBeanDescriptor();
@@ -108,11 +106,9 @@ public final class SqlTreeBuilder {
this.includeJoin = query.getM2mIncludeJoin();
this.manyWhereJoins = query.getManyWhereJoins();
this.queryDetail = query.getDetail();
this.predicates = predicates;
this.alias = new SqlTreeAlias(request.getBaseTableAlias(), temporalMode);
this.distinctOnPlatform = builder.isPlatformDistinctOn();
String fromForUpdate = builder.fromForUpdate(query);
CQueryHistorySupport historySupport = builder.getHistorySupport(query);
CQueryDraftSupport draftSupport = builder.getDraftSupport(query);
@@ -124,10 +120,8 @@ public final class SqlTreeBuilder {
* Build based on the includes and using the BeanJoinTree.
*/
public SqlTree build() {
// build the appropriate chain of SelectAdapter's
buildRoot(desc);
// build the actual String
String distinctOn = null;
String selectSql = null;
@@ -149,7 +143,6 @@ public final class SqlTreeBuilder {
}
private String buildSelectClause() {
if (rawSql) {
return "Not Used";
}
@@ -158,7 +151,6 @@ public final class SqlTreeBuilder {
}
private String buildGroupByClause() {
if (rawSql || !rootNode.isAggregation()) {
return null;
}
@@ -168,7 +160,6 @@ public final class SqlTreeBuilder {
}
private String buildDistinctOn() {
if (rawSql || !distinctOnPlatform || !sqlDistinct || Type.COUNT == query.getType()) {
return null;
}
@@ -207,7 +198,6 @@ public final class SqlTreeBuilder {
}
private String buildWhereClause() {
if (rawSql) {
return "Not Used";
}
@@ -216,7 +206,6 @@ public final class SqlTreeBuilder {
}
private String buildFromClause() {
if (rawSql) {
return "Not Used";
}
@@ -225,17 +214,13 @@ public final class SqlTreeBuilder {
}
private void buildRoot(STreeType desc) {
rootNode = buildSelectChain(null, null, desc, null);
if (!rawSql) {
alias.addJoin(queryDetail.getFetchPaths(), desc);
alias.addJoin(predicates.getPredicateIncludes(), desc);
alias.addManyWhereJoins(manyWhereJoins.getPropertyNames());
// build set of table alias
alias.buildAlias();
predicates.parseTableAlias(alias);
}
}
@@ -246,9 +231,7 @@ public final class SqlTreeBuilder {
*/
private SqlTreeNode buildSelectChain(String prefix, STreePropertyAssoc prop,
STreeType desc, List<SqlTreeNode> joinList) {
List<SqlTreeNode> myJoinList = new ArrayList<>();
List<STreePropertyAssocOne> extraProps = new ArrayList<>();
for (STreePropertyAssocOne one : desc.propsOne()) {
String propPrefix = SplitName.add(prefix, one.getName());
@@ -295,7 +278,6 @@ public final class SqlTreeBuilder {
* </p>
*/
private void addManyWhereJoins(List<SqlTreeNode> myJoinList) {
Collection<PropertyJoin> includes = manyWhereJoins.getPropertyJoins();
for (PropertyJoin joinProp : includes) {
STreePropertyAssoc beanProperty = (STreePropertyAssoc) desc.findPropertyFromPath(joinProp.getProperty());
@@ -311,7 +293,6 @@ public final class SqlTreeBuilder {
}
private SqlTreeNode buildNode(String prefix, STreePropertyAssoc prop, STreeType desc, List<SqlTreeNode> myList, SqlTreeProperties props) {
if (prefix == null) {
buildExtraJoins(desc, myList);
@@ -340,13 +321,10 @@ public final class SqlTreeBuilder {
* already in select clause.
*/
private void buildExtraJoins(STreeType desc, List<SqlTreeNode> myList) {
if (rawSql) {
return;
}
Set<String> predicateIncludes = predicates.getPredicateIncludes();
if (predicateIncludes == null) {
return;
}
@@ -362,8 +340,7 @@ public final class SqlTreeBuilder {
// look for predicateIncludes that are not in selectIncludes and add
// them as extra joins to the query
IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes);
IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes, temporalMode);
Collection<SqlTreeNodeExtraJoin> extraJoins = extraJoinDistill.getExtraJoinRootNodes();
if (!extraJoins.isEmpty()) {
// add extra joins required to support predicates
@@ -389,7 +366,6 @@ public final class SqlTreeBuilder {
* </p>
*/
private void addPropertyToSubQuery(SqlTreeProperties selectProps, STreeType desc, String propName) {
STreeProperty p = desc.findProperty(propName);
if (p == null) {
logger.error("property [" + propName + "]not found on " + desc + " for query - excluding it.");
@@ -402,13 +378,10 @@ public final class SqlTreeBuilder {
p = ((STreePropertyAssoc) p).target().findProperty(name);
}
}
selectProps.add(p);
}
private void addProperty(SqlTreeProperties selectProps, STreeType desc,
OrmQueryProperties queryProps, String propName) {
private void addProperty(SqlTreeProperties selectProps, STreeType desc, OrmQueryProperties queryProps, String propName) {
if (subQuery) {
addPropertyToSubQuery(selectProps, desc, propName);
return;
@@ -471,7 +444,6 @@ public final class SqlTreeBuilder {
}
private SqlTreeProperties getBaseSelectPartial(STreeType desc, OrmQueryProperties queryProps) {
SqlTreeProperties selectProps = new SqlTreeProperties();
// add properties in the order in which they appear
// in the query. Gives predictable sql/properties for
@@ -502,7 +474,6 @@ public final class SqlTreeBuilder {
}
private SqlTreeProperties getBaseSelect(STreeType desc, OrmQueryProperties queryProps) {
boolean partial = queryProps != null && !queryProps.allProperties();
if (partial) {
return getBaseSelectPartial(desc, queryProps);
@@ -541,13 +512,10 @@ public final class SqlTreeBuilder {
* Return true if this many node should be included in the query.
*/
private boolean isIncludeMany(String propName, STreePropertyAssocMany manyProp) {
if (queryDetail.isJoinsEmpty()) {
return false;
}
if (queryDetail.includesPath(propName)) {
if (manyProperty != null) {
// only one many associated allowed to be included in fetch
if (logger.isDebugEnabled()) {
@@ -555,7 +523,6 @@ public final class SqlTreeBuilder {
}
return false;
}
manyProperty = manyProp;
return true;
}
@@ -571,7 +538,6 @@ public final class SqlTreeBuilder {
* </p>
*/
private boolean isIncludeBean(String prefix) {
if (queryDetail.includesPath(prefix)) {
// explicitly included
String[] splitNames = SplitName.split(prefix);
@@ -592,26 +558,20 @@ public final class SqlTreeBuilder {
*/
private static class IncludesDistiller {
private final STreeType desc;
private final Set<String> selectIncludes;
private final Set<String> predicateIncludes;
private final SpiQuery.TemporalMode temporalMode;
/**
* Contains the 'root' extra joins. We only return the roots back.
*/
private final Map<String, SqlTreeNodeExtraJoin> joinRegister = new HashMap<>();
/**
* Register of all the extra join nodes.
*/
private final Map<String, SqlTreeNodeExtraJoin> rootRegister = new HashMap<>();
private final STreeType desc;
private IncludesDistiller(STreeType desc, Set<String> selectIncludes,
Set<String> predicateIncludes) {
Set<String> predicateIncludes, SpiQuery.TemporalMode temporalMode) {
this.desc = desc;
this.selectIncludes = selectIncludes;
this.predicateIncludes = predicateIncludes;
this.temporalMode = temporalMode;
}
/**
@@ -623,26 +583,21 @@ public final class SqlTreeBuilder {
* </p>
*/
private Collection<SqlTreeNodeExtraJoin> getExtraJoinRootNodes() {
String[] extras = findExtras();
if (extras.length == 0) {
return rootRegister.values();
}
// sort so we process only getting the leaves
// excluding nodes between root and the leaf
Arrays.sort(extras);
// reverse order so get the leaves first...
for (String extra : extras) {
createExtraJoin(extra);
}
return rootRegister.values();
}
private void createExtraJoin(String includeProp) {
SqlTreeNodeExtraJoin extraJoin = createJoinLeaf(includeProp);
if (extraJoin != null) {
// add the extra join...
@@ -650,7 +605,6 @@ public final class SqlTreeBuilder {
// find root of this extra join... linking back to the
// parents (creating the tree) as it goes.
SqlTreeNodeExtraJoin root = findExtraJoinRoot(includeProp, extraJoin);
// register the root because these are the only ones we
// return back.
rootRegister.put(root.getName(), root);
@@ -661,12 +615,11 @@ public final class SqlTreeBuilder {
* Create a SqlTreeNodeExtraJoin, register and return it.
*/
private SqlTreeNodeExtraJoin createJoinLeaf(String propertyName) {
ExtraJoin extra = desc.extraJoin(propertyName);
if (extra == null) {
return null;
} else {
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, extra.getProperty(), extra.isContainsMany());
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, extra.getProperty(), extra.isContainsMany(), temporalMode);
joinRegister.put(propertyName, extraJoin);
return extraJoin;
}
@@ -713,9 +666,7 @@ public final class SqlTreeBuilder {
* by the select.
*/
private String[] findExtras() {
List<String> extras = new ArrayList<>();
for (String predProp : predicateIncludes) {
if (!selectIncludes.contains(predProp)) {
extras.add(predProp);
@@ -24,19 +24,17 @@ import java.util.Set;
class SqlTreeNodeExtraJoin implements SqlTreeNode {
private final STreePropertyAssoc assocBeanProperty;
private final SpiQuery.TemporalMode temporalMode;
private final String prefix;
private final boolean manyJoin;
private final boolean pathContainsMany;
private List<SqlTreeNodeExtraJoin> children;
SqlTreeNodeExtraJoin(String prefix, STreePropertyAssoc assocBeanProperty, boolean pathContainsMany) {
SqlTreeNodeExtraJoin(String prefix, STreePropertyAssoc assocBeanProperty, boolean pathContainsMany, SpiQuery.TemporalMode temporalMode) {
this.prefix = prefix;
this.assocBeanProperty = assocBeanProperty;
this.pathContainsMany = pathContainsMany;
this.temporalMode = temporalMode;
this.manyJoin = assocBeanProperty instanceof STreePropertyAssocMany;
}
@@ -113,9 +111,7 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode {
@Override
public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) {
boolean manyToMany = false;
if (assocBeanProperty instanceof STreePropertyAssocMany) {
STreePropertyAssocMany manyProp = (STreePropertyAssocMany) assocBeanProperty;
if (manyProp.hasJoinTable()) {
@@ -144,15 +140,16 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode {
assocBeanProperty.appendFrom(ctx, joinType);
}
joinType = assocBeanProperty.addJoin(joinType, prefix, ctx);
if (assocBeanProperty.isTargetSoftDelete() && temporalMode != SpiQuery.TemporalMode.SOFT_DELETED) {
ctx.append(" and ").append(assocBeanProperty.getSoftDeletePredicate(ctx.getTableAlias(prefix)));
}
}
if (children != null) {
if (manyJoin || pathContainsMany) {
// if AUTO then make all descendants use OUTER JOIN
joinType = joinType.autoToOuter();
}
for (SqlTreeNodeExtraJoin child : children) {
child.appendFrom(ctx, joinType);
}
@@ -3,6 +3,9 @@ package io.ebeaninternal.server.querydefn;
import io.ebean.ExpressionList;
import io.ebean.ProfileLocation;
import io.ebean.UpdateQuery;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
/**
* Default implementation of UpdateQuery.
@@ -10,23 +13,30 @@ import io.ebean.UpdateQuery;
public class DefaultUpdateQuery<T> implements UpdateQuery<T> {
private final OrmUpdateProperties values = new OrmUpdateProperties();
private final DefaultOrmQuery<T> query;
private final BeanDescriptor<T> descriptor;
public DefaultUpdateQuery(DefaultOrmQuery<T> query) {
this.query = query;
this.descriptor = query.getBeanDescriptor();
query.setUpdateProperties(values);
}
@Override
public UpdateQuery<T> set(String property, Object value) {
values.set(property, value);
if (value == null) {
values.setNull(property);
} else {
final BeanProperty beanProperty = descriptor.getBeanProperty(property);
final ScalarType<Object> scalarType = (beanProperty == null) ? null: beanProperty.getScalarType();
values.set(property, value, scalarType);
}
return this;
}
@Override
public UpdateQuery<T> setNull(String property) {
values.set(property, null);
values.setNull(property);
return this;
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.server.deploy.DeployParser;
import io.ebeaninternal.server.persist.Binder;
import io.ebeaninternal.server.type.DataBind;
@@ -53,9 +54,11 @@ public class OrmUpdateProperties {
private static class SimpleValue extends Value {
final Object value;
final ScalarType<Object> scalarType;
SimpleValue(Object value) {
SimpleValue(Object value, ScalarType<Object> scalarType) {
this.value = value;
this.scalarType = scalarType;
}
@Override
@@ -70,7 +73,11 @@ public class OrmUpdateProperties {
@Override
public void bind(Binder binder, DataBind dataBind) throws SQLException {
binder.bindObject(dataBind, value);
if (scalarType != null) {
scalarType.bind(dataBind, value);
} else {
binder.bindObject(dataBind, value);
}
dataBind.append(value).append(",");
}
}
@@ -115,16 +122,12 @@ public class OrmUpdateProperties {
*/
private final LinkedHashMap<String, Value> values = new LinkedHashMap<>();
/**
* Normal set property.
*/
public void set(String propertyName, Object value) {
if (value == null) {
values.put(propertyName, NULL_VALUE);
public void set(String propertyName, Object value, ScalarType<Object> scalarType) {
values.put(propertyName, new SimpleValue(value, scalarType));
}
} else {
values.put(propertyName, new SimpleValue(value));
}
public void setNull(String propertyName) {
values.put(propertyName, NULL_VALUE);
}
/**
@@ -586,6 +586,21 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
}
}
@Override
public void preCommit() {
// do nothing
}
@Override
public void postCommit() {
// do nothing
}
@Override
public void postRollback(Throwable cause) {
// do nothing
}
/**
* Return true if the transaction is active.
*/
@@ -373,6 +373,9 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
private void firePreCommit() {
withEachCallback(TransactionCallback::preCommit);
if (changeLogHolder != null) {
changeLogHolder.preCommit();
}
}
private void firePostCommit() {
@@ -981,14 +984,23 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
* Batch flush, jdbc commit, trigger registered TransactionCallbacks, notify l2 cache etc.
*/
private void flushCommitAndNotify() throws SQLException {
internalBatchFlush();
firePreCommit();
// only performCommit can throw an exception
preCommit();
performCommit();
postCommit();
}
@Override
public void postCommit() {
firePostCommit();
notifyCommit();
}
@Override
public void preCommit() {
internalBatchFlush();
firePreCommit();
}
/**
* Perform a commit, fire callbacks and notify l2 cache etc.
* <p>
@@ -1132,11 +1144,16 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
} finally {
// these will not throw an exception
firePostRollback();
notifyRollback(cause);
postRollback(cause);
}
}
@Override
public void postRollback(Throwable cause) {
firePostRollback();
notifyRollback(cause);
}
/**
* If the transaction is active then perform rollback.
*/
@@ -13,9 +13,9 @@ public class JtaTransaction extends JdbcTransaction {
private final UserTransaction userTransaction;
private boolean commmitted;
private final boolean newTransaction;
private boolean newTransaction;
private boolean committed;
/**
* Create the JtaTransaction.
@@ -41,7 +41,6 @@ public class JtaTransaction extends JdbcTransaction {
if (connection.getAutoCommit()) {
connection.setAutoCommit(false);
}
} catch (SQLException e) {
throw new PersistenceException(e);
}
@@ -52,7 +51,7 @@ public class JtaTransaction extends JdbcTransaction {
*/
@Override
public void commit() {
if (commmitted) {
if (committed) {
throw new PersistenceException("This transaction has already been committed.");
}
try {
@@ -60,14 +59,14 @@ public class JtaTransaction extends JdbcTransaction {
if (newTransaction) {
userTransaction.commit();
}
notifyCommit();
postCommit();
} finally {
close();
}
} catch (Exception e) {
throw new PersistenceException(e);
}
commmitted = true;
committed = true;
}
@Override
@@ -80,7 +79,7 @@ public class JtaTransaction extends JdbcTransaction {
*/
@Override
public void rollback(Throwable e) {
if (!commmitted) {
if (!committed) {
try {
try {
if (userTransaction != null) {
@@ -90,7 +89,7 @@ public class JtaTransaction extends JdbcTransaction {
userTransaction.setRollbackOnly();
}
}
notifyRollback(e);
postRollback(e);
} finally {
closeConnection();
}
@@ -98,7 +97,6 @@ public class JtaTransaction extends JdbcTransaction {
throw new PersistenceException(ex);
}
}
}
/**
@@ -190,19 +190,17 @@ public class JtaTransactionManager implements ExternalTransactionManager {
@Override
public void beforeCompletion() {
// Future note: for JPA2 locking we will
// have beforeCommit events to fire
transaction.preCommit();
}
@Override
public void afterCompletion(int status) {
switch (status) {
case Status.STATUS_COMMITTED:
if (logger.isDebugEnabled()) {
logger.debug("Jta Txn [" + transaction.getId() + "] committed");
}
transactionManager.notifyOfCommit(transaction);
transaction.postCommit();
// Remove this transaction object as it is completed
transactionManager.scope().clearExternal();
break;
@@ -211,7 +209,7 @@ public class JtaTransactionManager implements ExternalTransactionManager {
if (logger.isDebugEnabled()) {
logger.debug("Jta Txn [" + transaction.getId() + "] rollback");
}
transactionManager.notifyOfRollback(transaction, null);
transaction.postRollback(null);
// Remove this transaction object as it is completed
transactionManager.scope().clearExternal();
break;
@@ -88,6 +88,21 @@ class NoTransaction implements SpiTransaction {
// do nothing
}
@Override
public void preCommit() {
// do nothing
}
@Override
public void postCommit() {
// do nothing
}
@Override
public void postRollback(Throwable cause) {
// do nothing
}
@Override
public String getLogPrefix() {
return null;
@@ -56,20 +56,31 @@ public class TChangeLogHolder {
* Add a bean change to the change set.
*/
public void addBeanChange(BeanChange change) {
changes.addBeanChange(change);
if (++count >= batchSize) {
// we hit the batch size so send what we have knowing
// that the transaction has not completed yet and
// reset the changes and count
owner.sendChangeLog(changes);
changes = new ChangeSet(transactionId, ++batchId);
count = 0;
sendChanges();
}
}
private void sendChanges() {
owner.sendChangeLog(changes);
changes = new ChangeSet(transactionId, ++batchId);
count = 0;
}
/**
* On post commit send the changes we have collected.
* Send the changes held prior to transaction commit.
*/
public void preCommit() {
sendChanges();
}
/**
* On post commit send the changes we have collected. This should be
* only the COMMITTED state and with all changes sent prior to commit.
*/
public void postCommit() {
changes.setTxnState(TxnState.COMMITTED);
@@ -634,7 +634,7 @@ public class TransactionManager implements SpiTransactionManager {
public ScopedTransaction externalBeginTransaction(SpiTransaction transaction, TxScope txScope) {
ScopedTransaction scopedTxn = new ScopedTransaction(scopeManager);
scopedTxn.push(new ScopeTrans(rollbackOnChecked, false, transaction, txScope));
scopeManager.set(scopedTxn);
scopeManager.replace(scopedTxn);
return scopedTxn;
}
@@ -1,6 +1,8 @@
package org.tests.batchinsert;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import io.ebean.DtoQuery2Test;
import io.ebean.Ebean;
import io.ebean.EbeanServer;
import io.ebean.Transaction;
@@ -105,6 +107,41 @@ public class TestBatchInsertFlush extends BaseTestCase {
assertSql(sql.get(0)).contains("select count(*)");
}
@Test
@Transactional(batchSize = 20)
public void transactional_flushOnSqlQuery() {
LoggedSqlCollector.start();
DB.save(new EBasicVer("b1"));
DB.save(new EBasicVer("b2"));
// trigger JDBC batch by default
DB.sqlQuery("select count(*) from e_basicver")
.mapToScalar(Integer.class)
.findOne();
List<String> sql = LoggedSqlCollector.stop();
assertSql(sql.get(0)).contains("insert into e_basicver");
}
@Test
@Transactional(batchSize = 20)
public void transactional_flushOnDtoQuery() {
LoggedSqlCollector.start();
DB.save(new EBasicVer("b1"));
DB.save(new EBasicVer("b2"));
// trigger JDBC batch by default
DB.findDto(DtoQuery2Test.DCust.class, "select id, name from o_customer")
.findList();
List<String> sql = LoggedSqlCollector.stop();
assertSql(sql.get(0)).contains("insert into e_basicver");
}
@Test
@Transactional(batch = PersistBatch.ALL)
public void transactional_flushOnQuery() {
@@ -0,0 +1,78 @@
package org.tests.cache;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.tests.model.cache.M2MCacheChild;
import org.tests.model.cache.M2MCacheMaster;
import io.ebean.BaseTestCase;
import io.ebean.DB;
public class TestM2MCache extends BaseTestCase {
@Test
public void testM2MWithCache() throws Exception {
M2MCacheChild cld = new M2MCacheChild();
cld.setName("test");
cld.setId(1);
DB.save(cld);
M2MCacheMaster cfg = new M2MCacheMaster();
cfg.setId(42);
cfg.getSet1().add(cld);
cfg.getSet2().add(cld);
DB.save(cfg);
// find master and access set1 + set2.
// lazy load something from set1
M2MCacheMaster cfg1 = DB.find(M2MCacheMaster.class, 42);
cfg1.getSet1().size();
cfg1.getSet2().size();
assertThat(cfg1.getSet1().iterator().next().getName()).isEqualTo("test");
// do it again
cfg1 = DB.find(M2MCacheMaster.class, 42);
cfg1.getSet1().size();
cfg1.getSet2().size();
assertThat(cfg1.getSet1().iterator().next().getName()).isEqualTo("test");
// do it again
cfg1 = DB.find(M2MCacheMaster.class, 42);
cfg1.getSet1().size();
cfg1.getSet2().size();
assertThat(cfg1.getSet1().iterator().next().getName()).isEqualTo("test");
}
@Test
public void testM2MWithCacheMinimal() throws Exception {
M2MCacheChild cld = new M2MCacheChild();
cld.setName("test");
cld.setId(2);
DB.save(cld);
M2MCacheMaster cfg = new M2MCacheMaster();
cfg.setId(43);
cfg.getSet1().add(cld);
cfg.getSet2().add(cld);
DB.save(cfg);
DB.find(M2MCacheMaster.class, 43);
M2MCacheMaster cfg1 = DB.find(M2MCacheMaster.class, 43);
cfg1.getSet2().size();
cfg1 = DB.find(M2MCacheMaster.class, 43);
cfg1.getSet1().size();
cfg1.getSet2().size();
assertThat(cfg1.getSet1().iterator().next().getName()).isEqualTo("test");
}
}
@@ -34,4 +34,7 @@ public class COOne {
return children;
}
public void setChildren(List<COOneMany> children) {
this.children = children;
}
}
@@ -0,0 +1,52 @@
package org.tests.cascade;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestDeleteO2MOrphans extends BaseTestCase {
@Test
public void test() {
final long id = setup();
// act
setNewChildren(id);
// assert
COOne check = findById(id);
//FIXME #2127 #2141: assertThat(check.getChildren()).hasSize(2);
assertThat(check.getChildren()).hasSize(4);
DB.delete(check);
}
private COOne findById(long id) {
return DB.find(COOne.class).where().idEq(id).findOne();
}
private void setNewChildren(long id) {
COOne found = findById(id);
found.setChildren(createManies("M3", "M4"));
DB.update(found);
}
private long setup() {
COOne company = new COOne("P0");
company.setChildren(createManies("M1", "M2"));
DB.insert(company);
return company.getId();
}
private List<COOneMany> createManies(String name1, String name2) {
COOneMany employee1 = new COOneMany(name1);
COOneMany employee2 = new COOneMany(name2);
List<COOneMany> employees = new ArrayList<>();
employees.add(employee1);
employees.add(employee2);
return employees;
}
}
@@ -0,0 +1,62 @@
package org.tests.cascade;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
public class TestOnlyKillOrphans extends BaseTestCase {
@Test
public void test() {
final COOne one = setup();
assertThat(one.getChildren()).hasSize(2);
CORoot root = new CORoot("P1", one);
DB.insert(root);
// assert
CORoot check = DB.find(CORoot.class, root.getId());
assertThat(check.getOne().getChildren()).hasSize(2);
DB.delete(check);
}
@Test
public void test2() {
final COOne one = setup();
assertThat(one.getChildren()).hasSize(2);
one.getChildren().add(new COOneMany("_M1"));
DB.save(one);
// assert
COOne check = DB.find(COOne.class, one.getId());
assertThat(check.getChildren().stream().map(it -> it.getName()).sorted().collect(Collectors.toList()))
.isEqualTo(Stream.of("M1", "M2", "_M1").sorted().collect(Collectors.toList()));
DB.delete(check);
}
private COOne setup() {
COOne one = new COOne("P0");
one.setChildren(createManies("M1", "M2"));
DB.insert(one);
return one;
}
private List<COOneMany> createManies(String... names) {
List<COOneMany> manies = new ArrayList<>();
for (String name : names) {
manies.add(new COOneMany(name));
}
return manies;
}
}
@@ -15,11 +15,15 @@ import io.ebean.event.changelog.ChangeLogPrepare;
import io.ebean.event.changelog.ChangeLogRegister;
import io.ebean.event.changelog.ChangeSet;
import io.ebean.event.changelog.ChangeType;
import io.ebean.event.changelog.TxnState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.tests.model.basic.EBasicChangeLog;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@@ -43,6 +47,13 @@ public class TestChangeLog extends BaseTestCase {
server.shutdown();
}
private BeanChange firstChange() {
ChangeSet changeSet0 = changeLogListener.changes.get(0);
BeanChange change = changeSet0.getChanges().get(0);
changeLogListener.changes.clear();
return change;
}
@Test
public void test() {
@@ -50,8 +61,13 @@ public class TestChangeLog extends BaseTestCase {
bean.setName("logBean");
bean.setShortDescription("hello");
server.save(bean);
BeanChange change = changeLogListener.changes.getChanges().get(0);
final List<ChangeSet> allChanges = changeLogListener.changes;
assertThat(allChanges).hasSize(2);
assertThat(allChanges.get(0).getTxnState()).isEqualTo(TxnState.IN_PROGRESS);
assertThat(allChanges.get(1).getTxnState()).isEqualTo(TxnState.COMMITTED);
BeanChange change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.INSERT);
assertThat(change.getData())
.contains("\"name\":\"logBean\"")
@@ -61,8 +77,7 @@ public class TestChangeLog extends BaseTestCase {
bean.setName("ChangedName");
server.save(bean);
change = changeLogListener.changes.getChanges().get(0);
change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.UPDATE);
assertThat(change.getOldData()).contains("\"name\":\"logBean\"");
assertThat(change.getData()).contains("\"name\":\"ChangedName\"");
@@ -70,14 +85,11 @@ public class TestChangeLog extends BaseTestCase {
server.delete(bean);
change = changeLogListener.changes.getChanges().get(0);
change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.DELETE);
assertThat(change.getData()).isNull();
}
@Test
public void testWithNull() {
@@ -85,7 +97,7 @@ public class TestChangeLog extends BaseTestCase {
bean.setName(null);
bean.setShortDescription("hello");
server.save(bean);
BeanChange change = changeLogListener.changes.getChanges().get(0);
BeanChange change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.INSERT);
assertThat(change.getData())
@@ -99,7 +111,7 @@ public class TestChangeLog extends BaseTestCase {
bean.setShortDescription("hello");
server.save(bean);
change = changeLogListener.changes.getChanges().get(0);
change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.UPDATE);
assertThat(change.getOldData())
@@ -113,11 +125,10 @@ public class TestChangeLog extends BaseTestCase {
server.delete(bean);
change = changeLogListener.changes.getChanges().get(0);
change = firstChange();
assertThat(change.getEvent()).isEqualTo(ChangeType.DELETE);
assertThat(change.getData()).isNull();
}
private Database createServer() {
@@ -153,16 +164,15 @@ public class TestChangeLog extends BaseTestCase {
ObjectMapper objectMapper = new ObjectMapper();
ChangeSet changes;
List<ChangeSet> changes = new ArrayList<>();
public TDChangeLogListener() {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@Override
public void log(ChangeSet changes) {
this.changes = changes;
this.changes.add(changes);
try {
String json = objectMapper.writeValueAsString(changes);
ChangeSet changes1 = objectMapper.readValue(json, ChangeSet.class);
@@ -24,7 +24,7 @@ public class TestInsertDuplicateKey extends BaseTestCase {
public void clearDb() {
server().find(Document.class).asDraft().where().contains("title", "UniqueKey").delete();
}
@Test(expected = DuplicateKeyException.class)
public void insert_duplicateKey() {
@@ -89,7 +89,7 @@ public class TestInsertDuplicateKey extends BaseTestCase {
doc2.save();
// flush at this point, fails
Ebean.getDefaultServer().currentTransaction().flushBatch();
Ebean.getDefaultServer().currentTransaction().flush();
} catch (DuplicateKeyException e) {
log.info("duplicate failed but just continue" + e.getMessage());
try {
@@ -1,7 +1,7 @@
package org.tests.json;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.DB;
import io.ebean.annotation.ForPlatform;
import io.ebean.annotation.Platform;
import io.ebean.text.TextException;
@@ -25,7 +25,7 @@ import static org.junit.Assert.assertTrue;
public class TestDbJson_List extends BaseTestCase {
private EBasicJsonList bean = new EBasicJsonList();
private final EBasicJsonList bean = new EBasicJsonList();
private EBasicJsonList found;
@@ -57,9 +57,9 @@ public class TestDbJson_List extends BaseTestCase {
bean.getBeanMap().put("key0", new PlainBean("k0", 90));
bean.getBeanMap().put("key1", new PlainBean("k1", 91));
Ebean.save(bean);
DB.save(bean);
found = Ebean.find(EBasicJsonList.class, bean.getId());
found = DB.find(EBasicJsonList.class, bean.getId());
assertThat(found.getTags()).containsExactly("one", "two");
assertTrue(found.getFlags().contains(42L));
@@ -81,7 +81,7 @@ public class TestDbJson_List extends BaseTestCase {
//@Test//(dependsOnMethods = "insert")
public void json_parse_format() {
String asJson = Ebean.json().toJson(found);
String asJson = DB.json().toJson(found);
assertThat(asJson).contains("\"tags\":[\"one\",\"two\"]");
assertThat(asJson).contains("\"flags\":[42,43,44]");
assertThat(asJson).contains("\"plainBean\":{\"name\":\"plain\"");
@@ -90,7 +90,7 @@ public class TestDbJson_List extends BaseTestCase {
assertThat(asJson).contains("\"beanMap\":{");
assertThat(asJson).contains("\"id\":");
EBasicJsonList fromJson = Ebean.json().toBean(EBasicJsonList.class, asJson);
EBasicJsonList fromJson = DB.json().toBean(EBasicJsonList.class, asJson);
assertEquals(found.getId(), fromJson.getId());
assertEquals(found.getId(), fromJson.getId());
assertEquals(found.getName(), fromJson.getName());
@@ -109,7 +109,7 @@ public class TestDbJson_List extends BaseTestCase {
found.setName("mod");
LoggedSqlCollector.start();
Ebean.save(found);
DB.save(found);
List<String> sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
@@ -122,7 +122,7 @@ public class TestDbJson_List extends BaseTestCase {
found.getTags().add("three");
LoggedSqlCollector.start();
Ebean.save(found);
DB.save(found);
List<String> sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
@@ -135,7 +135,7 @@ public class TestDbJson_List extends BaseTestCase {
found.getFlags().remove(42L);
LoggedSqlCollector.start();
Ebean.save(found);
DB.save(found);
List<String> sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
@@ -150,7 +150,7 @@ public class TestDbJson_List extends BaseTestCase {
found.getBeanMap().remove("key0");
LoggedSqlCollector.start();
Ebean.save(found);
DB.save(found);
List<String> sql = LoggedSqlCollector.stop();
// we don't update the phone numbers (as they are not dirty)
@@ -166,13 +166,13 @@ public class TestDbJson_List extends BaseTestCase {
bean.setTags(null);
bean.setBeanMap(null);
Ebean.save(bean);
DB.save(bean);
EBasicJsonList found = Ebean.find(EBasicJsonList.class, bean.getId());
EBasicJsonList found = DB.find(EBasicJsonList.class, bean.getId());
assertNull(found.getPlainBean());
String asJson = Ebean.json().toJson(found);
String asJson = DB.json().toJson(found);
assertNotNull(asJson);
}
@@ -186,17 +186,16 @@ public class TestDbJson_List extends BaseTestCase {
plainBean.setName("Blubb");
bean.getBeanMap().put("bla", plainBean);
Ebean.save(bean);
DB.save(bean);
// set some invalid JSON content into DB
Ebean.update(EBasicJsonList.class)
.set("beanMap", "blabla")
.where().eq("id", bean.getId())
.update();
DB.sqlUpdate("update ebasic_json_list set bean_map=? where id=?")
.setParameters("blabla", bean.getId())
.execute();
try {
// a normal query fails due to invalid JSON content
Ebean.find(EBasicJsonList.class)
DB.find(EBasicJsonList.class)
.setId(bean.getId())
.findOne();
@@ -208,7 +207,7 @@ public class TestDbJson_List extends BaseTestCase {
assertThat(e.getMessage()).contains("beanMap");
}
bean = Ebean.find(EBasicJsonList.class)
bean = DB.find(EBasicJsonList.class)
.setId(bean.getId())
.setAllowLoadErrors() // allow invalid JSON content
.findOne();
@@ -220,6 +219,6 @@ public class TestDbJson_List extends BaseTestCase {
.isInstanceOf(TextException.class)
.hasMessageContaining("blabla");
Ebean.delete(bean);
DB.delete(bean);
}
}
@@ -1,14 +1,16 @@
package org.tests.json;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.DB;
import io.ebean.Query;
import io.ebean.text.json.EJson;
import io.ebeantest.LoggedSql;
import org.tests.model.json.EBasicJsonMap;
import org.junit.Test;
import org.tests.model.json.EBasicJsonMapDetail;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -29,9 +31,9 @@ public class TestJsonMapBasic extends BaseTestCase {
bean.getDetails().add(new EBasicJsonMapDetail("detail1"));
bean.getDetails().add(new EBasicJsonMapDetail("detail2"));
Ebean.save(bean);
DB.save(bean);
Query<EBasicJsonMap> query1 = Ebean.find(EBasicJsonMap.class)
Query<EBasicJsonMap> query1 = DB.find(EBasicJsonMap.class)
.fetch("details")
.where().startsWith("details.name", "detail")
.query();
@@ -40,7 +42,7 @@ public class TestJsonMapBasic extends BaseTestCase {
assertThat(query1.getGeneratedSql()).contains("select distinct on (t0.id, t1.id) ");
Query<EBasicJsonMap> query2 = Ebean.find(EBasicJsonMap.class)
Query<EBasicJsonMap> query2 = DB.find(EBasicJsonMap.class)
.where().startsWith("details.name", "detail")
.query();
query2.findList();
@@ -60,9 +62,9 @@ public class TestJsonMapBasic extends BaseTestCase {
bean.setName("one");
bean.setContent(content);
Ebean.save(bean);
DB.save(bean);
EBasicJsonMap bean1 = Ebean.find(EBasicJsonMap.class, bean.getId());
EBasicJsonMap bean1 = DB.find(EBasicJsonMap.class, bean.getId());
assertEquals(bean.getId(), bean1.getId());
assertEquals(bean.getName(), bean1.getName());
@@ -70,16 +72,16 @@ public class TestJsonMapBasic extends BaseTestCase {
assertEquals(18L, bean1.getContent().get("docId"));
bean1.setName("just change name");
Ebean.save(bean1);
DB.save(bean1);
// content changes detected - dirty state so included in update
Map<String, Object> content1 = bean1.getContent();
content1.put("additional", "newValue");
content1.put("docId", 99L);
bean1.setName("two");
Ebean.save(bean1);
DB.save(bean1);
EBasicJsonMap bean2 = Ebean.find(EBasicJsonMap.class, bean.getId());
EBasicJsonMap bean2 = DB.find(EBasicJsonMap.class, bean.getId());
// name changed and docId changed
assertEquals("two", bean2.getName());
@@ -89,12 +91,48 @@ public class TestJsonMapBasic extends BaseTestCase {
content1.put("additional", "modValue");
bean1.setName("three");
bean1.setContent(content1);
Ebean.save(bean1);
DB.save(bean1);
EBasicJsonMap bean3 = Ebean.find(EBasicJsonMap.class, bean.getId());
EBasicJsonMap bean3 = DB.find(EBasicJsonMap.class, bean.getId());
assertEquals("three", bean3.getName());
assertEquals(99L, bean3.getContent().get("docId"));
assertEquals("modValue", bean3.getContent().get("additional"));
}
@Test
public void updateQuery_bindingMap() throws IOException {
String s0 = "{\"docId\":22,\"contentId\":\"initialDoc\"}";
Map<String, Object> content = EJson.parseObject(s0);
EBasicJsonMap bean = new EBasicJsonMap();
bean.setName("one");
bean.setContent(content);
DB.save(bean);
String s1 = "{\"docId\":222,\"contentId\":\"updatedDoc222\"}";
Map<String, Object> content1 = EJson.parseObject(s1);
LoggedSql.start();
final int rows = DB.update(EBasicJsonMap.class)
.set("content", content1)
.where().eq("id", bean.getId())
.update();
final List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("update ebasic_json_map set content=? where id = ?");
assertThat(rows).isEqualTo(1);
final EBasicJsonMap found = DB.find(EBasicJsonMap.class, bean.getId());
final Map<String, Object> content2 = found.getContent();
assertThat(content2.get("contentId")).isEqualTo("updatedDoc222");
assertThat(content2.get("docId")).isEqualTo(222L);
DB.delete(found);
}
}
@@ -0,0 +1,69 @@
package org.tests.lifecycle;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.tests.model.basic.EBasicLog;
import org.tests.model.basic.EBasicWithLog;
import io.ebean.BaseTestCase;
import io.ebean.DB;
public class TestLifecycleWithLog extends BaseTestCase {
private List<String> getLogs() {
List<String> ret = DB.find(EBasicLog.class)
.select("name")
.findSingleAttributeList();
DB.find(EBasicLog.class).delete();
return ret;
}
@Test
public void testCUD() {
EBasicWithLog bean = new EBasicWithLog();
bean.setId(1L);
bean.setName("Test1");
DB.save(bean);
assertThat(getLogs()).contains("prePersist", "postPersist");
bean.setName("Test2");
DB.save(bean);
assertThat(getLogs()).contains("preUpdate", "postUpdate");
DB.delete(bean);
assertThat(getLogs()).contains("preSoftDelete", "postSoftDelete");
DB.deletePermanent(bean);
assertThat(getLogs()).contains("preRemove", "postRemove");
}
@Test
public void testCUDBatch() {
EBasicWithLog bean = new EBasicWithLog();
bean.setId(2L);
bean.setName("Test2");
List<EBasicWithLog> beans = Arrays.asList(bean);
DB.saveAll(beans);
assertThat(getLogs()).contains("prePersist", "postPersist");
bean.setName("Test2Modified");
DB.saveAll(beans);
assertThat(getLogs()).contains("preUpdate", "postUpdate");
DB.deleteAll(beans);
assertThat(getLogs()).contains("preSoftDelete", "postSoftDelete");
DB.deleteAllPermanent(beans);
assertThat(getLogs()).contains("preRemove", "postRemove");
}
}
@@ -0,0 +1,32 @@
package org.tests.model.basic;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "e_basic_log")
public class EBasicLog {
@Id
Long id;
String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,124 @@
package org.tests.model.basic;
import io.ebean.DB;
import io.ebean.annotation.PostSoftDelete;
import io.ebean.annotation.PreSoftDelete;
import io.ebean.annotation.SoftDelete;
import javax.annotation.PostConstruct;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Version;
@Entity
@Table(name = "e_basic_withlog")
public class EBasicWithLog {
@Id
Long id;
String name;
@SoftDelete
boolean deleted;
@Version
Long version;
@PrePersist
public void prePersist() {
writeLog("prePersist");
}
@PostPersist
public void postPersist() {
writeLog("postPersist");
}
@PreUpdate
public void preUpdate() {
writeLog("preUpdate");
}
@PostUpdate
public void postUpdate() {
writeLog("postUpdate");
}
@PreRemove
public void preRemove() {
writeLog("preRemove");
}
@PostRemove
public void postRemove() {
writeLog("postRemove");
}
@PostSoftDelete
public void postSoftDelete() {
writeLog("postSoftDelete");
}
@PreSoftDelete
public void preSoftDelete() {
writeLog("preSoftDelete");
}
@PostLoad
public void postLoad() {
writeLog("postLoad");
}
@PostConstruct
public void postConstruct() {
writeLog("postConstruct");
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
private void writeLog(String title) {
EBasicLog log = new EBasicLog();
log.setName(title);
DB.save(log);
}
}
@@ -40,7 +40,7 @@ public class TestInsertBatchThenFlushThenUpdate extends BaseTestCase {
// nothing flushed yet
assertThat(LoggedSqlCollector.start()).isEmpty();
txn.flushBatch();
txn.flush();
List<String> loggedSql1 = LoggedSqlCollector.start();
assertThat(loggedSql1).hasSize(4);
@@ -0,0 +1,33 @@
package org.tests.model.cache;
import javax.persistence.Entity;
import javax.persistence.Id;
import io.ebean.annotation.Cache;
@Entity
@Cache(enableQueryCache = true, enableBeanCache = true)
public class M2MCacheChild {
@Id
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,53 @@
package org.tests.model.cache;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import io.ebean.annotation.Cache;
@Entity
@Cache(enableQueryCache = true, enableBeanCache = true)
public class M2MCacheMaster {
@Id
private Integer id;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "m2mcache_set1")
private Set<M2MCacheChild> set1 = new LinkedHashSet<>();
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "m2mcache_set2")
private Set<M2MCacheChild> set2 = new LinkedHashSet<>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Set<M2MCacheChild> getSet1() {
return set1;
}
public void setSet1(Set<M2MCacheChild> set1) {
this.set1 = set1;
}
public Set<M2MCacheChild> getSet2() {
return set2;
}
public void setSet2(Set<M2MCacheChild> set2) {
this.set2 = set2;
}
}
@@ -1,7 +1,7 @@
package org.tests.model.history;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.DB;
import io.ebean.annotation.IgnorePlatform;
import io.ebean.annotation.Platform;
import org.ebeantest.LoggedSqlCollector;
@@ -16,7 +16,7 @@ public class TestHistoryOneToMany extends BaseTestCase {
@IgnorePlatform(Platform.ORACLE)
@Test
public void test() {
public void test() throws InterruptedException {
HiTOne one = new HiTOne("one");
@@ -37,11 +37,12 @@ public class TestHistoryOneToMany extends BaseTestCase {
one.getTwos().add(_22);
Ebean.save(one);
DB.save(one);
Thread.sleep(20);
LoggedSqlCollector.start();
List<HiTOne> list = Ebean.find(HiTOne.class)
List<HiTOne> list = DB.find(HiTOne.class)
.fetch("twos")
.fetch("twos.threes")
.where().ilike("name", "on%")
@@ -51,15 +52,14 @@ public class TestHistoryOneToMany extends BaseTestCase {
List<String> sql = LoggedSqlCollector.stop();
assertThat(list).hasSize(1);
assertThat(list.get(0).getTwos()).hasSize(2);
assertThat(list.get(0).getTwos().get(0).getThrees()).hasSize(3);
if (isH2()) {
assertThat(sql).hasSize(2);
assertSql(sql.get(0)).contains("from hi_tone_with_history t0 where (t0.sys_period_start <= ? and (t0.sys_period_end is null or t0.sys_period_end > ?)) and lower(t0.name) like ? escape'' limit 10");
assertSql(sql.get(1)).contains("from hi_ttwo_with_history t0 left join hi_tthree_with_history t1 on t1.hi_ttwo_id = t0.id and (t1.sys_period_start <= ? and (t1.sys_period_end is null or t1.sys_period_end > ?)) where (t0.sys_period_start <= ? and (t0.sys_period_end is null or t0.sys_period_end > ?)) and (t0.hi_tone_id) in (?)");
}
assertThat(list).hasSize(1);
assertThat(list.get(0).getTwos()).hasSize(2);
assertThat(list.get(0).getTwos().get(0).getThrees()).hasSize(3);
}
}
@@ -0,0 +1,52 @@
package org.tests.model.lazywithcache;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import io.ebean.annotation.Cache;
/**
* Class with @Cache and lazy load property.
*
* @author Noemi Szemenyei, FOCONIS AG
*
*/
@Entity
@Cache(enableQueryCache = true)
public class ChildWithCache {
@Id
Long id;
String name;
@Basic(fetch = FetchType.LAZY)
String address;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAddress(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
}
@@ -0,0 +1,46 @@
package org.tests.model.lazywithcache;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
/**
* Parent class with ChildWithCache.
*
*/
@Entity
public class ParentA {
@Id
Long id;
@ManyToOne(optional = true)
ChildWithCache child;
String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public ChildWithCache getChild() {
return child;
}
public void setChild(ChildWithCache child) {
this.child = child;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,36 @@
package org.tests.model.lazywithcache;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
/**
* Parent class with ChildWithCache.
*
*/
@Entity
public class ParentB {
@Id
Long id;
@ManyToOne(optional = true)
ChildWithCache child;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public ChildWithCache getChild() {
return child;
}
public void setChild(ChildWithCache child) {
this.child = child;
}
}
@@ -0,0 +1,52 @@
package org.tests.model.lazywithcache;
import org.junit.Test;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test with bean cache and lazy loaded property.
*
* @author Noemi Szemenyei, FOCONIS AG
*
*/
public class TestWithCacheAndLazyLoad extends BaseTestCase{
@Test
public void testGetters() {
ChildWithCache child = new ChildWithCache();
child.setId(1L);
child.setName("Child With Cache");
child.setAddress("Address");
DB.save(child);
ParentA parentA = new ParentA();
parentA.setId(1L);
parentA.setName("Parent A");
parentA.setChild(child);
DB.save(parentA);
ParentB parentB = new ParentB();
parentB.setId(1L);
parentB.setChild(child);
DB.save(parentB);
ParentA tempA = DB.find(ParentA.class, 1L);
tempA.getChild().getName(); // load name
ParentB tempB = DB.find(ParentB.class, 1L);
ChildWithCache temp = tempB.getChild();
// if the next line is commented out, the test passes
temp.getName(); // load name from cache --> ebean_intercept.loadedFromCache = true
assertThat(temp.getAddress()).isEqualTo("Address");
assertThat(temp.getName()).isEqualTo("Child With Cache");
}
}
@@ -3,18 +3,24 @@ package org.tests.query.other;
import io.ebean.BaseTestCase;
import io.ebean.CountDistinctOrder;
import io.ebean.CountedValue;
import io.ebean.DB;
import io.ebean.Ebean;
import io.ebean.Query;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.tests.inherit.ChildA;
import org.tests.inherit.Data;
import org.tests.inherit.EUncle;
import org.tests.lazyforeignkeys.MainEntity;
import org.tests.lazyforeignkeys.MainEntityRelation;
import org.tests.model.basic.Contact;
import org.tests.model.basic.Customer;
import org.tests.model.basic.Order;
import org.tests.model.basic.ResetBasicData;
import org.tests.model.basic.VwCustomer;
import org.tests.o2m.OmBasicParent;
import java.sql.Date;
import java.time.LocalDate;
@@ -24,6 +30,46 @@ import static org.assertj.core.api.Assertions.assertThat;
public class TestQuerySingleAttribute extends BaseTestCase {
@Test
public void findSingleAttributesTwoToMany() {
ResetBasicData.reset();
// Query without ors with equals causing joins, only one Customer with name Rob exists
Query<Customer> query0 = DB.find(Customer.class)
.select("name")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where()
.eq("name", "Rob")
.query();
CountedValue<String> robs0 = (CountedValue<String>) query0.findSingleAttributeList().get(0);
assertThat(robs0.getValue()).isEqualTo("Rob");
assertThat(robs0.getCount()).isEqualTo(1);
// Query with or with equals causing joins
Query<Customer> query = DB.find(Customer.class)
.select("name")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where()
.eq("name", "Rob")
.or()
.eq("orders.status", Order.Status.NEW)
.eq("contacts.firstName", "Fred1")
.endOr()
.query();
CountedValue<String> robs = (CountedValue<String>) query.findSingleAttributeList().get(0);
assertThat(robs.getValue()).isEqualTo("Rob");
// only one Customer named rob exists, but 7 is returned for the amount of Customers named Rob
assertThat(robs.getCount()).isEqualTo(1);
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*) " +
"from (select distinct t0.id, t0.name as attribute_ " +
"from o_customer t0 left join contact u1 on u1.customer_id = t0.id left join o_order u2 on u2.kcustomer_id = t0.id " +
"where t0.name = ? and (u2.status = ? or u1.first_name = ?)) r1 " +
"group by r1.attribute_ " +
"order by count(*) desc, r1.attribute_");
}
@Test
public void exampleUsage() {
@@ -102,6 +148,93 @@ public class TestQuerySingleAttribute extends BaseTestCase {
assertThat(name).isNotNull();
}
@Test
public void findSingleAttributeList_with_join_column() {
ResetBasicData.reset();
Query<MainEntityRelation> query = Ebean.find(MainEntityRelation.class)
.fetch("entity1", "attr1")
.setDistinct(true)
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
List<CountedValue<String>> attr1list = query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select distinct t0.id, t0.id1, t1.attr1 as attribute_ from main_entity_relation t0 left join main_entity t1 on t1.id = t0.id1) r1"
+ " group by r1.attribute_"
+ " order by count(*) desc, r1.attribute_"); // sub-query select clause includes t0.id1
assertThat(attr1list).isNotNull();
assertThat(attr1list).hasSize(2);
assertThat(attr1list.get(0).getValue()).isEqualTo("a1");
assertThat(attr1list.get(0).getCount()).isEqualTo(2l);
assertThat(attr1list.get(1).getValue()).isEqualTo("a2");
assertThat(attr1list.get(1).getCount()).isEqualTo(1l);
}
@Test
public void findSingleAttributesVariousSelection1() {
Query<MainEntityRelation> query = Ebean.find(MainEntityRelation.class)
.fetch("entity1", "attr1")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select t0.id1, t1.attr1 as attribute_ from main_entity_relation t0 left join main_entity t1 on t1.id = t0.id1) r1"
+ " group by r1.attribute_"
+ " order by count(*) desc, r1.attribute_"); // sub-query select clause includes t0.id1
}
@Test
public void findSingleAttributesVariousSelection2() {
Query<MainEntityRelation> query = Ebean.find(MainEntityRelation.class)
.select("attr1")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select t0.attr1 as attribute_ from main_entity_relation t0) r1"
+ " group by r1.attribute_"
+ " order by count(*) desc, r1.attribute_");
}
@Test
public void findSingleAttributesVariousSelection3() {
Query<MainEntityRelation> query = Ebean.find(MainEntityRelation.class)
.select("id")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select t0.id as attribute_ from main_entity_relation t0) r1"
+ " group by r1.attribute_"
+ " order by count(*) desc, r1.attribute_");
}
@Test
public void findSingleAttributesVariousSelection4() {
Query<MainEntityRelation> query = Ebean.find(MainEntityRelation.class)
.fetch("entity1", "id")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select t0.id1, t1.id as attribute_ from main_entity_relation t0 left join main_entity t1 on t1.id = t0.id1) r1"
+ " group by r1.attribute_"
+ " order by count(*) desc, r1.attribute_"); // sub-query select clause includes t0.id1,
}
@Test
public void findSingleAttributesVariousSelection5() {
Query<OmBasicParent> query = Ebean.find(OmBasicParent.class)
.fetch("children", "name")
.setCountDistinct(CountDistinctOrder.COUNT_DESC_ATTR_ASC)
.where().query();
query.findSingleAttributeList();
assertThat(sqlOf(query)).contains("select r1.attribute_, count(*)"
+ " from (select t1.id, t1.name as attribute_ from om_basic_parent t0 left join om_basic_child t1 on t1.parent_id = t0.id) r1 "
+ "group by r1.attribute_ order by count(*) desc, r1.attribute_"); // sub-query select clause includes t1.id,
}
@Test
public void findSingleAttribute_with_aggregate() {
@@ -624,4 +757,43 @@ public class TestQuerySingleAttribute extends BaseTestCase {
System.out.println(" count:" + entry.getCount()+" orderStatus:" + entry.getValue() );
}
}
@Before
public void setup() {
MainEntity e1 = new MainEntity();
e1.setId("1");
e1.setAttr1("a1");
DB.save(e1);
MainEntity e2 = new MainEntity();
e2.setId("2");
e2.setAttr1("a2");
DB.save(e2);
MainEntity e3 = new MainEntity();
e3.setId("3");
e3.setAttr1("a1");
DB.save(e3);
MainEntityRelation rel = new MainEntityRelation();
rel.setEntity1(e1);
rel.setEntity2(e1);
DB.save(rel);
rel = new MainEntityRelation();
rel.setEntity1(e2);
rel.setEntity2(e2);
DB.save(rel);
rel = new MainEntityRelation();
rel.setEntity1(e3);
rel.setEntity2(e3);
DB.save(rel);
}
@After
public void cleanup() {
Ebean.find(MainEntityRelation.class).delete();
Ebean.find(MainEntity.class).delete();
}
}
@@ -137,6 +137,27 @@ public class TestSoftDeleteBasic extends BaseTestCase {
query.delete();
}
@Test
public void testFindChild_joinParent() {
EBasicSoftDelete bean = new EBasicSoftDelete();
bean.setName("softDelParent_withChild");
bean.addChild("child1", 10);
DB.save(bean);
Query<EBasicSDChild> query = DB.find(EBasicSDChild.class)
.where()
.eq("owner.name", "softDelParent_withChild")
.query();
List<EBasicSDChild> list = query.findList();
assertSql(query).contains("join ebasic_soft_delete t1 on t1.id = t0.owner_id and t1.deleted =");
assertThat(list).hasSize(1);
// Cleanup created entity
DB.deletePermanent(bean);
}
@Test
public void testFindSoftDeletedList() {
EBasicSoftDelete bean = new EBasicSoftDelete();
@@ -11,7 +11,7 @@ import org.tests.model.basic.ESimple;
/**
* This test tests a strange bug in the 6.2.0. sqlserver JDBC driver.
* (Version 6.1.7.jre8-preview works)
*
* <p>
* https://github.com/Microsoft/mssql-jdbc/pull/374
*
* @author Roland Praml, FOCONIS AG
@@ -29,7 +29,7 @@ public class TestSqlServerBatch extends BaseTestCase {
for (int i = 0; i < 10; i++) {
ESimple model = new ESimple();
model.setName("baz "+i);
model.setName("baz " + i);
Ebean.save(model);
}
@@ -56,13 +56,13 @@ public class TestSqlServerBatch extends BaseTestCase {
txn.setGetGeneratedKeys(false);
// explicitly flush the JDBC batch buffer
txn.flushBatch();
txn.flush();
for (int i = 0; i < 10; i++) {
ESimple model = new ESimple();
model.setName(i % 2 == 0 ? null:"foobar");
Ebean.save(model);
}
for (int i = 0; i < 10; i++) {
ESimple model = new ESimple();
model.setName(i % 2 == 0 ? null : "foobar");
Ebean.save(model);
}
// do not commit
} finally {
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean ddl generation</name>
@@ -23,20 +23,20 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>12.2.0</version>
<version>12.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
@@ -77,7 +77,7 @@
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>12.5.0</version>
<version>12.6.6</version>
<executions>
<execution>
<id>test</id>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean external mapping api</name>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<!-- <parent>-->
<!-- <groupId>org.avaje</groupId>-->
@@ -14,7 +14,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-parent-12.6.2</tag>
<tag>ebean-parent-12.6.6</tag>
</scm>
<name>ebean external mapping xml</name>
@@ -33,7 +33,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-externalmapping-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
@@ -59,14 +59,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean postgis</name>
@@ -24,7 +24,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
@@ -75,7 +75,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<!-- <parent>-->
<!-- <groupId>org.avaje</groupId>-->
@@ -14,7 +14,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-parent-12.6.2</tag>
<tag>ebean-parent-12.6.6</tag>
</scm>
<name>ebean querybean</name>
@@ -27,7 +27,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
@@ -67,14 +67,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
+92
View File
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.6</version>
</parent>
<artifactId>ebean-redis</artifactId>
<name>ebean redis</name>
<description>Ebean Redis L2 Cache</description>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>12.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.avaje.composite</groupId>
<artifactId>junit</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.avaje.composite</groupId>
<artifactId>logback</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.18</version>
<extensions>true</extensions>
<configuration>
<tiles>
<tile>io.ebean.tile:enhancement:12.6.1</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,161 @@
package io.ebean.redis;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.meta.MetricVisitor;
import io.ebeaninternal.server.cache.DefaultServerCache;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DuelCache implements ServerCache, NearCacheInvalidate {
private final DefaultServerCache near;
private final RedisCache remote;
private final NearCacheNotify cacheNotify;
private final String cacheKey;
public DuelCache(DefaultServerCache near, RedisCache remote, String cacheKey, NearCacheNotify cacheNotify) {
this.near = near;
this.remote = remote;
this.cacheKey = cacheKey;
this.cacheNotify = cacheNotify;
}
@Override
public void visit(MetricVisitor visitor) {
near.visit(visitor);
remote.visit(visitor);
}
@Override
public void invalidateKeys(Set<Object> keySet) {
near.removeAll(keySet);
}
@Override
public void invalidateKey(Object id) {
near.remove(id);
}
@Override
public void invalidateClear() {
near.clear();
}
@Override
public Map<Object, Object> getAll(Set<Object> keys) {
Map<Object, Object> resultMap = near.getAll(keys);
Set<Object> localKeys = resultMap.keySet();
Set<Object> remainingKeys = new HashSet<>();
for (Object key : keys) {
if (!localKeys.contains(key)) {
remainingKeys.add(key);
}
}
if (!remainingKeys.isEmpty()) {
// fetch missing ones from remote cache and merge results
Map<Object, Object> remoteMap = remote.getAll(remainingKeys);
if (!remoteMap.isEmpty()) {
near.putAll(remoteMap);
resultMap.putAll(remoteMap);
}
}
return resultMap;
}
@Override
public Object get(Object id) {
Object val = near.get(id);
if (val != null) {
return val;
}
Object remoteVal = remote.get(id);
if (remoteVal != null) {
near.put(id, remoteVal);
}
return remoteVal;
}
@Override
public void putAll(Map<Object, Object> keyValues) {
near.putAll(keyValues);
remote.putAll(keyValues);
cacheNotify.invalidateKeys(cacheKey, keyValues.keySet());
}
@Override
public void put(Object id, Object value) {
near.put(id, value);
remote.put(id, value);
cacheNotify.invalidateKey(cacheKey, id);
}
@Override
public void removeAll(Set<Object> keys) {
near.removeAll(keys);
remote.removeAll(keys);
cacheNotify.invalidateKeys(cacheKey, keys);
}
@Override
public void remove(Object id) {
near.remove(id);
remote.remove(id);
cacheNotify.invalidateKey(cacheKey, id);
}
@Override
public void clear() {
near.clear();
remote.clear();
cacheNotify.invalidateClear(cacheKey);
}
/**
* Return the near cache hit count.
*/
public long getNearHitCount() {
return near.getHitCount();
}
/**
* Return the near cache miss count.
*/
public long getNearMissCount() {
return near.getMissCount();
}
/**
* Return the redis cache hit count.
*/
public long getRemoteHitCount() {
return remote.getHitCount();
}
/**
* Return the redis cache miss count.
*/
public long getRemoteMissCount() {
return remote.getMissCount();
}
@Override
public int size() {
return 0;
}
@Override
public int getHitRatio() {
return 0;
}
@Override
public ServerCacheStatistics getStatistics(boolean reset) {
return null;
}
}
@@ -0,0 +1,49 @@
package io.ebean.redis;
import java.security.SecureRandom;
import java.util.Base64;
/**
* Provides a modified base64 encoded UUID and shorter 12 character random unique value.
* <p>
* <h3>newId()</h3>
* <p>
* It produces a 22 character string that is a base64 encoded UUID with the +
* and / characters replaced with - and _ so as to be URL safe without requiring
* encoding.
* </p>
* <h3>newShortId()</h3>
* <p>
* It produces a 12 character string that base64 encoded random number (72 bit).
* </p>
* <p>
* Note that this now internally uses java.util.Base64 to encode the values.
* </p>
*/
public class ModId {
private static final SecureRandom shortIdSecureRandom = new SecureRandom();
private static final Base64.Encoder urlEncoder = Base64.getUrlEncoder();
/**
* Return a 12 character string using a 72 bit randomly generated ID encoded
* in modified base64.
* <p>
* A UUID is 128 bits and this is 72 bits so quite a bit smaller but still
* very random with one in 4.7 * 10^21 chance of a collision.
* </p>
*/
public static String id() {
// Random 72 bits
byte[] randomBytes = new byte[9];
shortIdSecureRandom.nextBytes(randomBytes);
return encode64(randomBytes);
}
private static String encode64(byte[] bytes) {
return urlEncoder.encodeToString(bytes);
}
}
@@ -0,0 +1,24 @@
package io.ebean.redis;
import java.util.Set;
/**
* Near cache invalidation.
*/
public interface NearCacheInvalidate {
/**
* Invalidate from near cache the given keys.
*/
void invalidateKeys(Set<Object> keySet);
/**
* Invalidate from near cache the given key.
*/
void invalidateKey(Object id);
/**
* Clear the near cache.
*/
void invalidateClear();
}
@@ -0,0 +1,24 @@
package io.ebean.redis;
import java.util.Set;
/**
* Notify other cluster members to invalidate parts of their near cache.
*/
public interface NearCacheNotify {
/**
* Invalidate the given keys.
*/
void invalidateKeys(String cacheKey, Set<Object> keySet);
/**
* Invalidate a single key.
*/
void invalidateKey(String cacheKey, Object id);
/**
* Clear a near cache.
*/
void invalidateClear(String cacheKey);
}
@@ -0,0 +1,268 @@
package io.ebean.redis;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.CountMetric;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.TimedMetric;
import io.ebean.redis.encode.Encode;
import io.ebean.redis.encode.EncodePrefixKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import redis.clients.jedis.util.SafeEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
class RedisCache implements ServerCache {
private static final Logger log = LoggerFactory.getLogger(RedisCache.class);
private static final String CURSOR_0 = "0";
private static final byte[] CURSOR_0_BYTES = SafeEncoder.encode(CURSOR_0);
private final JedisPool jedisPool;
private final String cacheKey;
private final Encode keyEncode;
private final Encode valueEncode;
private final TimedMetric metricGet;
private final TimedMetric metricGetAll;
private final TimedMetric metricPut;
private final TimedMetric metricPutAll;
private final TimedMetric metricRemove;
private final TimedMetric metricRemoveAll;
private final TimedMetric metricClear;
private final CountMetric hitCount;
private final CountMetric missCount;
RedisCache(JedisPool jedisPool, ServerCacheConfig config, Encode valueEncode) {
this.jedisPool = jedisPool;
this.cacheKey = config.getCacheKey();
this.keyEncode = new EncodePrefixKey(config.getCacheKey());
this.valueEncode = valueEncode;
String pre = "l2r.";
String shortName = config.getShortName();
MetricFactory factory = MetricFactory.get();
hitCount = factory.createCountMetric(pre + shortName + ".hit");
missCount = factory.createCountMetric(pre + shortName + ".miss");
metricGet = factory.createTimedMetric(pre + shortName + ".get");
metricGetAll = factory.createTimedMetric(pre + shortName + ".getMany");
metricPut = factory.createTimedMetric(pre + shortName + ".put");
metricPutAll = factory.createTimedMetric(pre + shortName + ".putMany");
metricRemove = factory.createTimedMetric(pre + shortName + ".remove");
metricRemoveAll = factory.createTimedMetric(pre + shortName + ".removeMany");
metricClear = factory.createTimedMetric(pre + shortName + ".clear");
}
@Override
public void visit(MetricVisitor visitor) {
hitCount.visit(visitor);
missCount.visit(visitor);
metricGet.visit(visitor);
metricGetAll.visit(visitor);
metricPut.visit(visitor);
metricPutAll.visit(visitor);
metricRemove.visit(visitor);
metricRemoveAll.visit(visitor);
metricClear.visit(visitor);
}
private byte[] key(Object id) {
return keyEncode.encode(id);
}
private byte[] value(Object data) {
if (data == null) {
return null;
}
return valueEncode.encode(data);
}
private Object valueDecode(byte[] data) {
try {
if (data == null) {
return null;
}
return valueEncode.decode(data);
} catch (Exception e) {
log.error("Error decoding data, treated as cache miss", e);
return null;
}
}
@Override
public Map<Object, Object> getAll(Set<Object> keys) {
long start = System.nanoTime();
Map<Object, Object> map = new LinkedHashMap<>();
List<Object> keyList = new ArrayList<>(keys);
try (Jedis resource = jedisPool.getResource()) {
List<byte[]> valsAsBytes = resource.mget(keysAsBytes(keyList));
for (int i = 0; i < keyList.size(); i++) {
Object val = valueDecode(valsAsBytes.get(i));
if (val != null) {
map.put(keyList.get(i), val);
}
}
int hits = map.size();
int miss = keys.size() - hits;
if (hits > 0) {
hitCount.add(hits);
}
if (miss > 0) {
missCount.add(miss);
}
metricGetAll.addSinceNanos(start);
return map;
}
}
@Override
public Object get(Object id) {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
Object val = valueDecode(resource.get(key(id)));
if (val != null) {
hitCount.increment();
} else {
missCount.increment();
}
metricGet.addSinceNanos(start);
return val;
}
}
@Override
public void put(Object id, Object value) {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.set(key(id), value(value));
metricPut.addSinceNanos(start);
}
}
@Override
public void putAll(Map<Object, Object> keyValues) {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
byte[][] raw = new byte[keyValues.size() * 2][];
int pos = 0;
for (Map.Entry<Object, Object> entry : keyValues.entrySet()) {
raw[pos++] = key(entry.getKey());
raw[pos++] = value(entry.getValue());
}
resource.mset(raw);
metricPutAll.addSinceNanos(start);
}
}
@Override
public void remove(Object id) {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.del(key(id));
metricRemove.addSinceNanos(start);
}
}
@Override
public void removeAll(Set<Object> keys) {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.del(keysAsBytes(keys));
metricRemoveAll.addSinceNanos(start);
}
}
private byte[][] keysAsBytes(Collection<Object> keys) {
byte[][] raw = new byte[keys.size()][];
int pos = 0;
for (Object id : keys) {
raw[pos++] = key(id);
}
return raw;
}
@Override
public void clear() {
long start = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
ScanParams params = new ScanParams();
params.match(cacheKey + ":*");
String next;
byte[] nextCursor = CURSOR_0_BYTES;
do {
ScanResult<byte[]> scanResult = resource.scan(nextCursor, params);
List<byte[]> keys = scanResult.getResult();
nextCursor = scanResult.getCursorAsBytes();
if (!keys.isEmpty()) {
byte[][] raw = new byte[keys.size()][];
for (int i = 0; i < keys.size(); i++) {
raw[i] = keys.get(i);
}
resource.del(raw);
}
next = SafeEncoder.encode(nextCursor);
} while (!next.equals("0"));
metricClear.addSinceNanos(start);
}
}
/**
* Return the count of get hits.
*/
public long getHitCount() {
return hitCount.get(false);
}
/**
* Return the count of get misses.
*/
public long getMissCount() {
return missCount.get(false);
}
@Override
public int size() {
return 0;
}
@Override
public int getHitRatio() {
return 0;
}
@Override
public ServerCacheStatistics getStatistics(boolean reset) {
ServerCacheStatistics cacheStats = new ServerCacheStatistics();
cacheStats.setCacheName(cacheKey);
cacheStats.setHitCount(hitCount.get(reset));
cacheStats.setMissCount(missCount.get(reset));
cacheStats.setPutCount(metricPut.collect(reset).getCount());
cacheStats.setRemoveCount(metricRemove.collect(reset).getCount());
cacheStats.setClearCount(metricClear.collect(reset).getCount());
return cacheStats;
}
}
@@ -0,0 +1,525 @@
package io.ebean.redis;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheNotification;
import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.DatabaseConfig;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.TimedMetric;
import io.ebean.redis.encode.EncodeBeanData;
import io.ebean.redis.encode.EncodeManyIdsData;
import io.ebean.redis.encode.EncodeSerializable;
import io.ebean.redis.topic.DaemonTopic;
import io.ebean.redis.topic.DaemonTopicRunner;
import io.ebeaninternal.server.cache.DefaultServerCache;
import io.ebeaninternal.server.cache.DefaultServerCacheConfig;
import io.ebeaninternal.server.cache.DefaultServerQueryCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.util.SafeEncoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static java.util.Arrays.asList;
class RedisCacheFactory implements ServerCacheFactory {
private static final Logger log = LoggerFactory.getLogger(RedisCacheFactory.class);
private static final Logger queryLogger = LoggerFactory.getLogger("io.ebean.cache.QUERY");
private static final Logger logger = LoggerFactory.getLogger("io.ebean.cache.CACHE");
private static final Logger tableModLogger = LoggerFactory.getLogger("io.ebean.cache.TABLEMODS");
private static final int MSG_NEARCACHE_CLEAR = 1;
private static final int MSG_NEARCACHE_KEYS = 2;
private static final int MSG_NEARCACHE_KEY = 3;
/**
* Channel for standard L2 cache messages.
*/
private static final String CHANNEL_L2 = "ebean.l2cache";
/**
* Channel specifically for near cache invalidation messages.
*/
private static final String CHANNEL_NEAR = "ebean.l2near";
private static final byte[] CHANNEL_L2_BYTES = SafeEncoder.encode(CHANNEL_L2);
private static final byte[] CHANNEL_NEAR_BYTES = SafeEncoder.encode(CHANNEL_NEAR);
private final ConcurrentHashMap<String, RQueryCache> queryCaches = new ConcurrentHashMap<>();
private final Map<String, NearCacheInvalidate> nearCacheMap = new ConcurrentHashMap<>();
private final EncodeManyIdsData encodeManyIdsData = new EncodeManyIdsData();
private final EncodeBeanData encodeBeanData = new EncodeBeanData();
private final EncodeSerializable encodeSerializable = new EncodeSerializable();
private final BackgroundExecutor executor;
private final JedisPool jedisPool;
private final DaemonTopicRunner daemonTopicRunner;
private final NearCacheNotify nearCacheNotify;
private final TimedMetric metricOutNearCache;
private final TimedMetric metricOutTableMod;
private final TimedMetric metricOutQueryCache;
private final TimedMetric metricInNearCache;
private final TimedMetric metricInTableMod;
private final TimedMetric metricInQueryCache;
private final String serverId = ModId.id();
private ServerCacheNotify listener;
RedisCacheFactory(DatabaseConfig config, BackgroundExecutor executor) {
this.executor = executor;
this.nearCacheNotify = new DNearCacheNotify();
MetricFactory factory = MetricFactory.get();
this.metricOutTableMod = factory.createTimedMetric("l2a.outTableMod");
this.metricOutQueryCache = factory.createTimedMetric("l2a.outQueryCache");
this.metricOutNearCache = factory.createTimedMetric("l2a.outNearKeys");
this.metricInTableMod = factory.createTimedMetric("l2a.inTableMod");
this.metricInQueryCache = factory.createTimedMetric("l2a.inQueryCache");
this.metricInNearCache = factory.createTimedMetric("l2a.inNearKeys");
if (config.isDisableL2Cache()) {
this.jedisPool = null;
this.daemonTopicRunner = null;
} else {
this.jedisPool = getJedisPool(config);
this.daemonTopicRunner = new DaemonTopicRunner(jedisPool, new CacheDaemonTopic());
daemonTopicRunner.run();
}
}
/**
* Return the JedisPool to use (only 1 at this stage).
*/
private JedisPool getJedisPool(DatabaseConfig config) {
JedisPool jedisPool = config.getServiceObject(JedisPool.class);
if (jedisPool != null) {
return jedisPool;
}
RedisConfig redisConfig = config.getServiceObject(RedisConfig.class);
if (redisConfig == null) {
redisConfig = new RedisConfig();
}
redisConfig.loadProperties(config.getProperties());
log.info("using l2cache redis host {}:{}", redisConfig.getServer(), redisConfig.getPort());
return redisConfig.createPool();
}
@Override
public void visit(MetricVisitor visitor) {
metricOutQueryCache.visit(visitor);
metricOutTableMod.visit(visitor);
metricOutNearCache.visit(visitor);
metricInTableMod.visit(visitor);
metricInQueryCache.visit(visitor);
metricInNearCache.visit(visitor);
}
@Override
public ServerCache createCache(ServerCacheConfig config) {
if (config.isQueryCache()) {
return createQueryCache(config);
}
return createNormalCache(config);
}
private ServerCache createNormalCache(ServerCacheConfig config) {
RedisCache redisCache = createRedisCache(config);
boolean nearCache = config.getCacheOptions().isNearCache();
if (!nearCache) {
return redisCache;
}
String cacheKey = config.getCacheKey();
DefaultServerCache near = new DefaultServerCache(new DefaultServerCacheConfig(config));
near.periodicTrim(executor);
DuelCache duelCache = new DuelCache(near, redisCache, cacheKey, nearCacheNotify);
nearCacheMap.put(cacheKey, duelCache);
return duelCache;
}
private RedisCache createRedisCache(ServerCacheConfig config) {
switch (config.getType()) {
case NATURAL_KEY:
return new RedisCache(jedisPool, config, encodeSerializable);
case BEAN:
return new RedisCache(jedisPool, config, encodeBeanData);
case COLLECTION_IDS:
return new RedisCache(jedisPool, config, encodeManyIdsData);
default:
throw new IllegalArgumentException("Unexpected cache type? " + config.getType());
}
}
private ServerCache createQueryCache(ServerCacheConfig config) {
synchronized (this) {
RQueryCache cache = queryCaches.get(config.getCacheKey());
if (cache == null) {
logger.debug("create query cache [{}]", config.getCacheKey());
cache = new RQueryCache(new DefaultServerCacheConfig(config));
cache.periodicTrim(executor);
queryCaches.put(config.getCacheKey(), cache);
}
return cache;
}
}
@Override
public ServerCacheNotify createCacheNotify(ServerCacheNotify listener) {
this.listener = listener;
return new RServerCacheNotify();
}
private void sendQueryCacheInvalidation(String name) {
long nanos = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.publish(CHANNEL_L2, serverId + ":queryCache:" + name);
} finally {
metricOutQueryCache.addSinceNanos(nanos);
}
}
private void sendTableMod(String formattedMsg) {
long nanos = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.publish(CHANNEL_L2, serverId + ":tableMod:" + formattedMsg);
} finally {
metricOutTableMod.addSinceNanos(nanos);
}
}
/**
* Query cache implementation using Redis channel for message notifications.
*/
private class RQueryCache extends DefaultServerQueryCache {
RQueryCache(DefaultServerCacheConfig config) {
super(config);
}
@Override
public void clear() {
super.clear();
sendQueryCacheInvalidation(name);
}
/**
* Process the invalidation message coming from the cluster.
*/
private void invalidate() {
queryLogger.debug(" CLEAR {}(*) - cluster invalidate", name);
super.clear();
}
}
/**
* Publish table modifications using Redis channel (to other cluster members)
*/
private class RServerCacheNotify implements ServerCacheNotify {
@Override
public void notify(ServerCacheNotification tableModifications) {
Set<String> dependentTables = tableModifications.getDependentTables();
if (dependentTables != null && !dependentTables.isEmpty()) {
StringBuilder msg = new StringBuilder(50);
for (String table : dependentTables) {
msg.append(table).append(",");
}
String formattedMsg = msg.toString();
if (tableModLogger.isDebugEnabled()) {
tableModLogger.debug("Publish TableMods - {}", formattedMsg);
}
sendTableMod(formattedMsg);
}
}
}
/**
* Clear the query cache if we have it.
*/
private void queryCacheInvalidate(String key) {
long nanos = System.nanoTime();
try {
RQueryCache queryCache = queryCaches.get(key);
if (queryCache != null) {
queryCache.invalidate();
}
} finally {
metricInQueryCache.addSinceNanos(nanos);
}
}
/**
* Process a remote dependent table modify event.
*/
private void processTableNotify(String rawMessage) {
long nanos = System.nanoTime();
try {
if (logger.isDebugEnabled()) {
logger.debug("processTableNotify {}", rawMessage);
}
Set<String> tables = new HashSet<>(asList(rawMessage.split(",")));
listener.notify(new ServerCacheNotification(tables));
} finally {
metricInTableMod.addSinceNanos(nanos);
}
}
/**
* Near cache notification using a specific Redis channel (CHANNEL_NEAR).
*/
private class DNearCacheNotify implements NearCacheNotify {
@Override
public void invalidateKeys(String cacheKey, Set<Object> keySet) {
try {
sendMessage(messageInvalidateKeys(cacheKey, keySet));
} catch (IOException e) {
logger.error("failed to transmit invalidateKeys() message", e);
}
}
@Override
public void invalidateKey(String cacheKey, Object id) {
try {
sendMessage(messageInvalidateKey(cacheKey, id));
} catch (IOException e) {
logger.error("failed to transmit invalidateKeys() message", e);
}
}
@Override
public void invalidateClear(String cacheKey) {
try {
sendMessage(messageInvalidateClear(cacheKey));
} catch (IOException e) {
logger.error("failed to transmit invalidateKeys() message", e);
}
}
private void sendMessage(byte[] message) {
long nanos = System.nanoTime();
try (Jedis resource = jedisPool.getResource()) {
resource.publish(CHANNEL_NEAR_BYTES, message);
} finally {
metricOutNearCache.addSinceNanos(nanos);
}
}
private byte[] messageInvalidateKeys(String cacheKey, Set<Object> keySet) throws IOException {
ByteArrayOutputStream ba = new ByteArrayOutputStream(100);
ObjectOutputStream os = new ObjectOutputStream(ba);
os.writeUTF(serverId);
os.writeInt(MSG_NEARCACHE_KEYS);
os.writeUTF(cacheKey);
os.writeInt(keySet.size());
for (Object key : keySet) {
os.writeObject(key);
}
os.flush();
os.close();
return ba.toByteArray();
}
private byte[] messageInvalidateKey(String cacheKey, Object id) throws IOException {
ByteArrayOutputStream ba = new ByteArrayOutputStream(100);
ObjectOutputStream os = new ObjectOutputStream(ba);
os.writeUTF(serverId);
os.writeInt(MSG_NEARCACHE_KEY);
os.writeUTF(cacheKey);
os.writeObject(id);
os.flush();
os.close();
return ba.toByteArray();
}
private byte[] messageInvalidateClear(String cacheKey) throws IOException {
ByteArrayOutputStream ba = new ByteArrayOutputStream(100);
ObjectOutputStream os = new ObjectOutputStream(ba);
os.writeUTF(serverId);
os.writeInt(MSG_NEARCACHE_CLEAR);
os.writeUTF(cacheKey);
os.flush();
os.close();
return ba.toByteArray();
}
}
/**
* Redis channel listener that supports reconnection etc.
*/
private class CacheDaemonTopic implements DaemonTopic {
@Override
public void subscribe(Jedis jedis) {
jedis.subscribe(new ChannelSubscriber(), CHANNEL_L2_BYTES, CHANNEL_NEAR_BYTES);
}
@Override
public void notifyConnected() {
logger.info("Established connection to Redis");
}
/**
* Handles updates to the features (via redis topic notifications).
*/
private class ChannelSubscriber extends BinaryJedisPubSub {
@Override
public void onMessage(byte[] channel, byte[] message) {
String channelName = SafeEncoder.encode(channel);
if (channelName.equals(CHANNEL_L2)) {
processL2Message(SafeEncoder.encode(message));
} else {
processNearCacheMessage(message);
}
}
private void processNearCacheMessage(byte[] message) {
long nanos = System.nanoTime();
int msgType = 0;
String cacheKey = null;
try {
ObjectInputStream oi = new ObjectInputStream(new ByteArrayInputStream(message));
String sourceServerId = oi.readUTF();
if (sourceServerId.equals(serverId)) {
// ignore this message as we are the server that sent it
return;
}
msgType = oi.readInt();
cacheKey = oi.readUTF();
if (logger.isDebugEnabled()) {
logger.debug("processNearCacheMessage serverId:{} type:{} cacheKey:{}", sourceServerId, msgType, cacheKey);
}
switch (msgType) {
case MSG_NEARCACHE_CLEAR:
nearCacheInvalidateClear(cacheKey);
break;
case MSG_NEARCACHE_KEY:
Object key = oi.readObject();
nearCacheInvalidateKey(cacheKey, key);
break;
case MSG_NEARCACHE_KEYS:
int count = oi.readInt();
Set<Object> keys = new LinkedHashSet<>();
for (int i = 0; i < count; i++) {
keys.add(oi.readObject());
}
nearCacheInvalidateKeys(cacheKey, keys);
break;
default:
throw new IllegalStateException("Unexpected message type ? " + msgType);
}
} catch (IOException | ClassNotFoundException e) {
logger.error("failed to decode near cache message [" + SafeEncoder.encode(message) + "] for cache:" + cacheKey, e);
if (cacheKey != null) {
nearCacheInvalidateClear(cacheKey);
}
} finally {
if (msgType != 0) {
metricInNearCache.addSinceNanos(nanos);
}
}
}
private void processL2Message(String message) {
try {
String[] split = message.split(":");
if (serverId.equals(split[0])) {
// ignore this message as we are the server that sent it
return;
}
switch (split[1]) {
case "tableMod":
processTableNotify(split[2]);
break;
case "queryCache":
queryCacheInvalidate(split[2]);
break;
default:
logger.error("Unknown L2 message type[{}] on redis channel - message[{}] ", split[0], message);
}
} catch (Exception e) {
logger.error("Error handling L2 message[" + message + "]", e);
}
}
}
}
/**
* Invalidate key for a local near cache.
*/
private void nearCacheInvalidateKey(String cacheKey, Object key) {
NearCacheInvalidate invalidate = nearCacheMap.get(cacheKey);
if (invalidate == null) {
warnNearCacheNotFound(cacheKey);
} else {
invalidate.invalidateKey(key);
}
}
/**
* Invalidate keys for a local near cache.
*/
private void nearCacheInvalidateKeys(String cacheKey, Set<Object> keys) {
NearCacheInvalidate invalidate = nearCacheMap.get(cacheKey);
if (invalidate == null) {
warnNearCacheNotFound(cacheKey);
} else {
invalidate.invalidateKeys(keys);
}
}
/**
* Invalidate clear for a local near cache.
*/
private void nearCacheInvalidateClear(String cacheKey) {
NearCacheInvalidate invalidate = nearCacheMap.get(cacheKey);
if (invalidate == null) {
warnNearCacheNotFound(cacheKey);
} else {
invalidate.invalidateClear();
}
}
private void warnNearCacheNotFound(String cacheKey) {
logger.warn("No near cache found for cacheKey [" + cacheKey + "] yet - probably on startup");
}
}
@@ -0,0 +1,17 @@
package io.ebean.redis;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.DatabaseConfig;
public class RedisCachePlugin implements ServerCachePlugin {
/**
* Create the ServerCacheFactory implementation.
*/
@Override
public ServerCacheFactory create(DatabaseConfig config, BackgroundExecutor executor) {
return new RedisCacheFactory(config, executor);
}
}
@@ -0,0 +1,179 @@
package io.ebean.redis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Properties;
/**
* Deployment configuration for redis.
*/
public class RedisConfig {
private String server = "localhost";
private int port = 6379;
private int maxTotal = 200;
private int maxIdle = 200;
private int minIdle = 1;
private long maxWaitMillis = -1L;
private boolean blockWhenExhausted = true;
private int timeout = 2000;
private String username;
private String password;
private boolean ssl;
/**
* Return a new JedisPool based on the configuration.
*/
public JedisPool createPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxWaitMillis(maxWaitMillis);
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
return new JedisPool(poolConfig, server, port, timeout, username, password, ssl);
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public long getMaxWaitMillis() {
return maxWaitMillis;
}
public void setMaxWaitMillis(long maxWaitMillis) {
this.maxWaitMillis = maxWaitMillis;
}
public boolean isBlockWhenExhausted() {
return blockWhenExhausted;
}
public void setBlockWhenExhausted(boolean blockWhenExhausted) {
this.blockWhenExhausted = blockWhenExhausted;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isSsl() {
return ssl;
}
public void setSsl(boolean ssl) {
this.ssl = ssl;
}
public void loadProperties(Properties properties) {
Reader reader = new Reader(properties);
this.server = reader.get("ebean.redis.server", server);
this.port = reader.getInt("ebean.redis.port", port);
this.ssl = reader.getBool("ebean.redis.ssl", ssl);
this.minIdle = reader.getInt("ebean.redis.minIdle", minIdle);
this.maxIdle = reader.getInt("ebean.redis.maxIdle", maxIdle);
this.maxTotal = reader.getInt("ebean.redis.maxTotal", maxTotal);
this.maxWaitMillis = reader.getLong("ebean.redis.maxWaitMillis", maxWaitMillis);
this.timeout = reader.getInt("ebean.redis.timeout", timeout);
this.username = reader.get("ebean.redis.username", username);
this.password = reader.get("ebean.redis.password", password);
this.blockWhenExhausted = reader.getBool("ebean.redis.blockWhenExhausted", blockWhenExhausted);
}
private static class Reader {
private final Properties properties;
Reader(Properties properties) {
this.properties = (properties != null) ? properties : new Properties();
}
String get(String key, String defaultVal) {
return System.getProperty(key, properties.getProperty(key, defaultVal));
}
int getInt(String key, int defaultVal) {
final String val = get(key, null);
return val != null ? Integer.parseInt(val.trim()) : defaultVal;
}
long getLong(String key, long defaultVal) {
final String val = get(key, null);
return val != null ? Integer.parseInt(val.trim()) : defaultVal;
}
boolean getBool(String key, boolean defaultVal) {
final String val = get(key, null);
return val != null ? Boolean.parseBoolean(val.trim()) : defaultVal;
}
}
}
@@ -0,0 +1,8 @@
package io.ebean.redis.encode;
public interface Encode {
byte[] encode(Object value);
Object decode(byte[] data);
}
@@ -0,0 +1,47 @@
package io.ebean.redis.encode;
import io.ebeaninternal.server.cache.CachedBeanData;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class EncodeBeanData implements Encode {
@Override
public byte[] encode(Object value) {
try {
CachedBeanData data = (CachedBeanData) value;
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
data.writeExternal(oos);
oos.flush();
oos.close();
return os.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
@Override
public Object decode(byte[] data) {
try {
ByteArrayInputStream is = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(is);
CachedBeanData read = new CachedBeanData();
read.readExternal(ois);
return read;
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
}
@@ -0,0 +1,47 @@
package io.ebean.redis.encode;
import io.ebeaninternal.server.cache.CachedManyIds;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class EncodeManyIdsData implements Encode {
@Override
public byte[] encode(Object value) {
try {
CachedManyIds data = (CachedManyIds) value;
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
data.writeExternal(oos);
oos.flush();
oos.close();
return os.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
@Override
public Object decode(byte[] data) {
try {
ByteArrayInputStream is = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(is);
CachedManyIds read = new CachedManyIds();
read.readExternal(ois);
return read;
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
}
@@ -0,0 +1,41 @@
package io.ebean.redis.encode;
import io.ebean.cache.TenantAwareKey;
import java.nio.charset.StandardCharsets;
public class EncodePrefixKey implements Encode {
private final String prefix;
public EncodePrefixKey(String cacheKey) {
this.prefix = cacheKey + ":";
}
@Override
public byte[] encode(Object value) {
try {
if (!(value instanceof String) && !(value instanceof TenantAwareKey.CacheKey)) {
throw new IllegalStateException("Expecting String keys but got type:" + value.getClass());
}
String key = prefix + value.toString();
return key.getBytes(StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
@Override
public Object decode(byte[] data) {
try {
String key = new String(data, StandardCharsets.UTF_8);
int pos = key.indexOf(':');
return key.substring(pos);
} catch (Exception e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
}
@@ -0,0 +1,37 @@
package io.ebean.redis.encode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class EncodeSerializable implements Encode {
@Override
public byte[] encode(Object value) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream());
oos.writeObject(value);
oos.flush();
oos.close();
return new ByteArrayOutputStream().toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
@Override
public Object decode(byte[] data) {
try {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return ois.readObject();
}
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException("Failed to decode cache data", e);
}
}
}
@@ -0,0 +1,22 @@
package io.ebean.redis.encode;
import java.io.Serializable;
public class PrefixKey implements Serializable {
private final String prefix;
private final Object key;
PrefixKey(String prefix, Object key) {
this.prefix = prefix;
this.key = key;
}
public String getPrefix() {
return prefix;
}
public Object getKey() {
return key;
}
}
@@ -0,0 +1,21 @@
package io.ebean.redis.topic;
import redis.clients.jedis.Jedis;
/**
* Topic subscriber that has re-connect notification.
*/
public interface DaemonTopic {
/**
* Subscribe to the topic/channel (blocking).
*
* @param jedis The redis connection to subscribe (and block on).
*/
void subscribe(Jedis jedis);
/**
* Notify that the topic subscription has been connected (or reconnected).
*/
void notifyConnected();
}
@@ -0,0 +1,95 @@
package io.ebean.redis.topic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisException;
import java.util.Timer;
import java.util.TimerTask;
/**
* Subscribe to redis topic listening for changes.
* <p>
* Handles reconnection to redis when the connection is lost and
* notification of when reconnection takes place.
* </p>
*/
public class DaemonTopicRunner {
private static final Logger log = LoggerFactory.getLogger(DaemonTopicRunner.class);
private static final long reconnectWaitMillis = 1000;
private final JedisPool jedisPool;
private final DaemonTopic daemonTopic;
public DaemonTopicRunner(JedisPool jedisPool, DaemonTopic daemonTopic) {
this.jedisPool = jedisPool;
this.daemonTopic = daemonTopic;
}
public void run() {
Thread t = new Thread(() -> attemptConnections(), "redis-sub");
t.start();
}
private void attemptConnections() {
Timer reloadTimer = new Timer("redis-sub-notify");
ReloadNotifyTask notifyTask = null;
int attempts = 1;
while (true) {
if (notifyTask != null) {
// we didn't successfully re-connect to redis
notifyTask.cancel();
}
notifyTask = new ReloadNotifyTask();
reloadTimer.schedule(notifyTask, reconnectWaitMillis + 500);
attempts++;
try {
subscribe();
} catch (JedisException e) {
log.debug("... redis subscribe connection attempt:{} failed:{}", attempts, e.getMessage());
try {
// wait a little before retrying
Thread.sleep(reconnectWaitMillis);
} catch (InterruptedException e1) {
log.warn("Interrupted redis re-connection wait", e1);
}
}
}
}
/**
* Subscribe and block when successful.
*/
private void subscribe() {
Jedis jedis = jedisPool.getResource();
jedis.echo("hi");
try {
daemonTopic.subscribe(jedis);
} catch (Exception e) {
log.error("Lost connection to topic, starting re-connection loop", e);
attemptConnections();
} finally {
try {
jedis.close();
} catch (Exception e) {
log.warn("Error closing probably broken Redis connection", e);
}
}
}
private class ReloadNotifyTask extends TimerTask {
@Override
public void run() {
daemonTopic.notifyConnected();
}
}
}
@@ -0,0 +1 @@
io.ebean.redis.RedisCachePlugin
@@ -0,0 +1,46 @@
package io.ebean.redis;
import org.junit.Test;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
public class RedisConfigTest {
@Test
public void loadProperties() {
Properties p = new Properties();
p.setProperty("ebean.redis.server", "test-server");
p.setProperty("ebean.redis.port", "99");
p.setProperty("ebean.redis.maxIdle", "5");
p.setProperty("ebean.redis.maxTotal", "6");
p.setProperty("ebean.redis.minIdle", "7");
p.setProperty("ebean.redis.maxWaitMillis", "8");
p.setProperty("ebean.redis.username", "un");
p.setProperty("ebean.redis.password", "pw");
p.setProperty("ebean.redis.ssl", "true");
RedisConfig config = new RedisConfig();
config.loadProperties(p);
assertThat(config.getServer()).isEqualTo("test-server");
assertThat(config.getPort()).isEqualTo(99);
assertThat(config.getMaxIdle()).isEqualTo(5);
assertThat(config.getMaxTotal()).isEqualTo(6);
assertThat(config.getMinIdle()).isEqualTo(7);
assertThat(config.getMaxWaitMillis()).isEqualTo(8);
assertThat(config.getUsername()).isEqualTo("un");
assertThat(config.getPassword()).isEqualTo("pw");
assertThat(config.isSsl()).isEqualTo(true);
}
@Test
public void test_defaultValues() {
RedisConfig config = new RedisConfig();
assertThat(config.getUsername()).isNull();
assertThat(config.getPassword()).isNull();
assertThat(config.isSsl()).isEqualTo(false);
}
}
@@ -0,0 +1,58 @@
package org.domain;
import io.ebean.Model;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import java.time.Instant;
@MappedSuperclass
public class EBase extends Model {
@Id
protected long id;
@Version
protected long version;
@WhenCreated
protected Instant whenCreated;
@WhenModified
protected Instant whenModified;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
public Instant getWhenCreated() {
return whenCreated;
}
public void setWhenCreated(Instant whenCreated) {
this.whenCreated = whenCreated;
}
public Instant getWhenModified() {
return whenModified;
}
public void setWhenModified(Instant whenModified) {
this.whenModified = whenModified;
}
}
@@ -0,0 +1,69 @@
package org.domain;
import io.ebean.annotation.Cache;
import io.ebean.annotation.Index;
import javax.persistence.Entity;
import java.time.LocalDate;
@Cache(enableQueryCache = true, nearCache = true, naturalKey = "name")
@Entity
public class Person extends EBase {
public enum Status {
NEW,
ACTIVE,
INACTIVE
}
@Index(unique = true)
String name;
Status status;
LocalDate localDate;
String notes;
public Person(String name) {
this.name = name;
this.status = Status.NEW;
}
public String toString() {
return "[id:" + id + " name:" + name + "date:" + localDate + ']';
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public LocalDate getLocalDate() {
return localDate;
}
public void setLocalDate(LocalDate localDate) {
this.localDate = localDate;
}
}
@@ -0,0 +1,123 @@
package org.integration;
import io.ebean.redis.DuelCache;
import org.domain.Person;
import io.ebean.DB;
import io.ebean.Database;
import io.ebean.DatabaseFactory;
import io.ebean.config.DatabaseConfig;
import org.domain.query.QPerson;
import org.junit.Test;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
public class ClusterTest {
private Database createOther(DataSource dataSource) {
DatabaseConfig config = new DatabaseConfig();
config.setDataSource(dataSource);
config.loadFromProperties();
config.setDefaultServer(false);
config.setName("other");
config.setDdlGenerate(false);
config.setDdlRun(false);
return DatabaseFactory.create(config);
}
@Test
public void testBothNear() {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.getPluginApi().getDataSource());
new QPerson()
.name.eq("Someone")
.delete();
Person foo = new Person("Someone");
foo.save();
DB.getServerCacheManager().clearAll();
other.getMetaInfoManager().resetAllMetrics();
Person fooA = DB.find(Person.class, foo.getId());
Person fooB = other.find(Person.class, foo.getId());
fooA = DB.find(Person.class, foo.getId());
fooB = other.find(Person.class, foo.getId());
fooA = DB.find(Person.class, foo.getId());
fooB = other.find(Person.class, foo.getId());
DuelCache dualCache = (DuelCache) other.getServerCacheManager().getBeanCache(Person.class);
assertCounts(dualCache, 2, 1, 1, 0);
}
@Test
public void test() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.getPluginApi().getDataSource());
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
foo.save();
}
other.getServerCacheManager().clearAll();
other.getMetaInfoManager().resetAllMetrics();
DuelCache dualCache = (DuelCache) other.getServerCacheManager().getBeanCache(Person.class);
Person foo0 = other.find(Person.class, 1);
assertCounts(dualCache, 0, 1, 0, 1);
other.find(Person.class, 1);
assertCounts(dualCache, 1, 1, 0, 1);
other.find(Person.class, 1);
assertCounts(dualCache, 2, 1, 0, 1);
other.find(Person.class, 1);
assertCounts(dualCache, 3, 1, 0, 1);
other.find(Person.class, 2);
assertCounts(dualCache, 3, 2, 0, 2);
foo0.setName("name2");
foo0.save();
allowAsyncMessaging();
Person foo3 = other.find(Person.class, 1);
assertThat(foo3.getName()).isEqualTo("name2");
assertCounts(dualCache, 3, 3, 1, 2);
foo0.setName("name3");
foo0.save();
allowAsyncMessaging();
foo3 = other.find(Person.class, 1);
assertThat(foo3.getName()).isEqualTo("name3");
assertCounts(dualCache, 3, 4, 2, 2);
}
private void assertCounts(DuelCache dualCache, int nearHits, int nearMiss, int remoteHit, int remoteMiss) {
assertThat(dualCache.getNearHitCount()).isEqualTo(nearHits);
assertThat(dualCache.getNearMissCount()).isEqualTo(nearMiss);
assertThat(dualCache.getRemoteHitCount()).isEqualTo(remoteHit);
assertThat(dualCache.getRemoteMissCount()).isEqualTo(remoteMiss);
}
private void allowAsyncMessaging() throws InterruptedException {
Thread.sleep(20);
}
}
@@ -0,0 +1,107 @@
package org.integration;
import io.ebean.DB;
import org.domain.Person;
import org.domain.query.QPerson;
import org.junit.Test;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class IntegrationTest {
@Test
public void test() throws InterruptedException {
insertSomePeople();
Person fiona = findByName("Fiona");
fiona.setName("Fortuna");
fiona.setLocalDate(LocalDate.now());
fiona.update();
Thread.sleep(100);
Person one = findById(1);
assertThat(one).isNotNull();
for (int i = 1; i < 4; i++) {
System.out.println("hit " + findById(i));
}
List<Person> one2 = nameStartsWith("fo");
assertThat(one2).hasSize(1);
one2 = nameStartsWith("j");
assertThat(one2).hasSize(2);
one2 = nameStartsWith("j");
assertThat(one2).hasSize(2);
List<Person> byNames = findByNames("Jack", "Rob");
assertThat(byNames).hasSize(2);
byNames = findByNames("Jack", "Rob", "Moby");
assertThat(byNames).hasSize(3);
fiona.setName("fo2");
fiona.setLocalDate(LocalDate.now());
fiona.update();
byNames = findByNames("Jack", "Rob", "Moby");
assertThat(byNames).hasSize(3);
Thread.sleep(200);
one2 = nameStartsWith("fo%");
System.out.println("one2 " + one2);
one2 = nameStartsWith("f0%");
System.out.println("one2 " + one2);
DB.getServerCacheManager().clear(Person.class);
System.out.println("done");
}
private List<Person> insertSomePeople() {
List<Person> people = new ArrayList<>();
for (String name : new String[]{"Jack", "John", "Rob", "Moby", "Fiona"}) {
people.add(new Person(name));
}
DB.saveAll(people);
return people;
}
private Person findByName(String name) {
return new QPerson()
.name.eq(name)
.findOne();
}
private List<Person> findByNames(String... names) {
return new QPerson()
.name.in(names)
.setUseCache(true)
.findList();
}
private Person findById(int id) {
return new QPerson()
.id.eq(id)
.findOne();
}
private List<Person> nameStartsWith(String pattern) {
return new QPerson()
.name.istartsWith(pattern)
.setUseQueryCache(true)
.findList();
}
}
@@ -0,0 +1,9 @@
ebean:
dumpMetricsOnShutdown: true
dumpMetricsOptions: sql,hash,loc
test:
redis: latest
# shutdown: stop # stop | remove
platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate
dbName: myapp
@@ -0,0 +1,29 @@
<configuration scan="true" scanPeriod="10 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>TRACE</level>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="STDOUT"/>
</root>
<!--<logger name="com.avaje.ebeaninternal.server.cluster" level="TRACE"/>-->
<!-- <logger name="io.ebean" level="DEBUG"/>-->
<logger name="io.ebean.SQL" level="TRACE"/>
<logger name="io.ebean.TXN" level="TRACE"/>
<logger name="io.ebean.SUM" level="TRACE"/>
<logger name="io.ebean.cache" level="TRACE"/>
<logger name="io.ebean.cache.QUERY" level="TRACE"/>
<logger name="io.ebean.cache.BEAN" level="TRACE"/>
<logger name="io.ebean.cache.COLL" level="TRACE"/>
<logger name="io.ebean.cache.NATKEY" level="TRACE"/>
</configuration>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<!-- <parent>-->
<!-- <groupId>org.avaje</groupId>-->
@@ -14,7 +14,7 @@
<scm>
<developerConnection>scm:git:git@github.com:ebean-orm/ebean.git</developerConnection>
<tag>ebean-parent-12.6.2</tag>
<tag>ebean-parent-12.6.6</tag>
</scm>
<name>ebean test</name>
@@ -43,14 +43,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
@@ -91,7 +91,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.12</version>
<version>42.2.18</version>
<scope>test</scope>
</dependency>
@@ -1,7 +1,6 @@
package io.ebean.test;
import io.ebean.DB;
import io.ebean.migration.util.IOUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -0,0 +1,62 @@
package io.ebean.test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
/**
* Utilities for IO.
*/
class IOUtils {
/**
* Reads the entire contents of the specified input stream and return them as UTF-8 string.
*/
static String readUtf8(InputStream in) throws IOException {
return bytesToUtf8(read(in));
}
/**
* Reads the entire contents of the specified input stream and returns them as a byte array.
*/
private static byte[] read(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
pump(in, buffer);
return buffer.toByteArray();
}
/**
* Returns the UTF-8 string corresponding to the specified bytes.
*/
private static String bytesToUtf8(byte[] data) {
return new String(data, StandardCharsets.UTF_8);
}
/**
* Reads data from the specified input stream and copies it to the specified
* output stream, until the input stream is at EOF. Both streams are then
* closed.
*/
private static void pump(InputStream in, OutputStream out) throws IOException {
if (in == null) throw new IOException("Input stream is null");
if (out == null) throw new IOException("Output stream is null");
try {
try {
byte[] buffer = new byte[4096];
for (; ; ) {
int bytes = in.read(buffer);
if (bytes < 0) {
break;
}
out.write(buffer, 0, bytes);
}
} finally {
in.close();
}
} finally {
out.close();
}
}
}
@@ -1,22 +1,18 @@
datasource:
db:
username: sa
password:
password: sa
databaseUrl: jdbc:h2:mem:tests
pg:
username: sa
password:
password: sa
databaseUrl: jdbc:h2:mem:tests
ebean:
# migration:
# run: false
#
# ddl:
# generate: true
# run: true
## createOnly: true
ddl:
generate: true
run: true
docstore:
url: http://127.0.0.1:9200
@@ -26,57 +22,9 @@ ebean:
# useDocker: true
elasticVersion: 5.6
# create: true
test:
redis: latest
platform: mariadb #, postgres, mysql, mariadb, oracle, sqlserver, hana
# useDocker: false
# dockerMode: dropCreate
ddlMode: dropCreate # none | dropCreate | create | migrations
dbName: junk
postgres:
version: 9.6
extensions: pgcryto, hstore
username: asd
password: test
url: asd
driver: asd
docker:
postgres:
# port: 6432
version: 9.6
extensions: pgcryto, hstore
username: ${test_db}
password: test
databaseUrl: jdbc:postgresql://localhost:6432/${test_db}
databaseDriver: org.postgresql.Driver
mysql:
version: 5.6
username: ${test_db}
password: test
databaseUrl: jdbc:mysql://localhost:4306/${test_db}
databaseDriver: com.mysql.jdbc.Driver
sqlserver:
version: 2017-CE
username: ${test_db}
password: SqlS3rv#r
databaseUrl: jdbc:sqlserver://localhost:1433;databaseName=${test_db}
databaseDriver: com.microsoft.sqlserver.jdbc.SQLServerDriver
oracle:
username: ${test_db}
password: test
databaseUrl: jdbc:oracle:thin:@127.0.0.1:1521:XE
databaseDriver: oracle.jdbc.driver.OracleDriver
hana:
username: ${test_db}
password: HXEHana1
databaseUrl: jdbc:sap://localhost:39017/?databaseName=HXE
databaseDriver: com.sap.db.jdbc.Driver
@@ -1,21 +0,0 @@
postgres.version=9.6
postgres.dbName=test_db
postgres.dbUser=test_user
postgres.dbPassword=test
postgres.dbExtensions=hstore,pgcrypto
sqlserver.version=2017-CU2
#sqlserver.port=1433
#sqlserver.dbName=test_db
#sqlserver.dbUser=test_user
#sqlserver.dbPassword=SqlS3rv#r
hana.version=2.00.033.00.20180925.2
hana.port=39117
hana.instanceNumber=91
hana.passwordsUrl=file:///hana/mounts/passwords.json
hana.mountsDirectory=/data/dockermounts
# agree to the SAP license (https://www.sap.com/docs/download/cmp/2016/06/sap-hana-express-dev-agmt-and-exhibit.pdf)
hana.agreeToSapLicense=false
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>12.6.2</version>
<version>12.6.6</version>
</parent>
<name>ebean composite</name>
@@ -22,34 +22,34 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<!-- In future ebean-ddl-generator would only be dependency of ebean-test -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
<!-- In future this is optional and not included by default -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-autotune</artifactId>
<version>12.6.2</version>
<version>12.6.6</version>
</dependency>
</dependencies>

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