diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 1b5a1952d3..d96245c4cb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -773,4 +773,10 @@ public boolean isInlineSqlUpdateLimit() { public int forwardOnlyFetchSize() { return queryEngine.forwardOnlyFetchSize(); } + + public void clearContext() { + if (!transaction.isAutoPersistUpdates()) { + beanDescriptor.contextClear(transaction.persistenceContext()); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestOrmUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestOrmUpdate.java index 03aa874f8d..9e93a064f5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestOrmUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestOrmUpdate.java @@ -77,10 +77,13 @@ public void setBindLog(String bindLog) { */ @Override public void postExecute() { + OrmUpdateType ormUpdateType = ormUpdate.ormUpdateType(); + if (OrmUpdateType.INSERT != ormUpdateType && !transaction.isAutoPersistUpdates()) { + beanDescriptor.contextClear(transaction.persistenceContext()); + } if (startNanos > 0) { persistExecute.collectOrmUpdate(label, startNanos); } - OrmUpdateType ormUpdateType = ormUpdate.ormUpdateType(); String tableName = ormUpdate.baseTable(); if (transaction.isLogSummary()) { transaction.logSummary("{0} table[{1}] rows[{2}] bind[{3}]", ormUpdateType, tableName, rowCount, bindLog); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java index e4f0c023f9..a5453420f7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java @@ -3,10 +3,13 @@ import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.persist.BatchControl; import io.ebeaninternal.server.persist.PersistExecute; import io.ebeaninternal.server.persist.TrimLogSql; +import java.util.List; + /** * Persist request specifically for CallableSql. */ @@ -156,6 +159,14 @@ public void logSqlBatchBind() { */ @Override public void postExecute() { + if (sqlType != SqlType.SQL_INSERT && !transaction.isAutoPersistUpdates()) { + List> descriptors = server.descriptors(tableName); + if (descriptors != null) { + for (BeanDescriptor descriptor : descriptors) { + descriptor.contextClear(transaction.persistenceContext()); + } + } + } if (startNanos > 0) { persistExecute.collectSqlUpdate(label, startNanos); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 081ae5d77d..dbb803fc9b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -2056,6 +2056,13 @@ public void contextClear(PersistenceContext pc, Object idValue) { pc.clear(rootBeanType, idValue); } + /** + * Clear a bean from the persistence context. + */ + public void contextClear(PersistenceContext pc) { + pc.clear(rootBeanType); + } + /** * Delete a bean from the persistence context (such that we don't fetch it in the same transaction). */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 240ab41a26..c4d179e6dc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -399,7 +399,7 @@ public void cacheNotify(TransactionEventTable.TableIUD tableIUD, CacheChangeSet * Return the BeanDescriptors mapped to the table. */ public List> descriptors(String tableName) { - return tableToDescMap.get(tableName.toLowerCase()); + return tableName == null ? Collections.emptyList() : tableToDescMap.get(tableName.toLowerCase()); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 969d7ca252..a0dab920a9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -74,6 +74,9 @@ private int executeUpdate(OrmQueryRequest request, CQueryUpdate query) { if (request.logSql()) { request.logSql("{0}; --bind({1}) --micros({2}) --rows({3})", query.generatedSql(), query.bindLog(), query.micros(), rows); } + if (rows > 0) { + request.clearContext(); + } return rows; } catch (SQLException e) { throw translate(request, query.bindLog(), query.generatedSql(), e); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java index 85081238b9..add07241a7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java @@ -61,6 +61,7 @@ public String generatedSql() { /** * Execute the update or delete statement returning the row count. */ + @SuppressWarnings("resource") public int execute() throws SQLException { long startNano = System.nanoTime(); try { @@ -109,6 +110,7 @@ private void close() { pstmt = null; } + @SuppressWarnings("resource") @Override public void profile() { transaction() diff --git a/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java b/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java index bfe12fd8f6..eca48dd08e 100644 --- a/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java +++ b/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java @@ -8,24 +8,22 @@ import static io.ebean.PersistenceContextScope.QUERY; import static io.ebean.PersistenceContextScope.TRANSACTION; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.*; -public class TestPersistenceContextQueryScope extends BaseTestCase { +class TestPersistenceContextQueryScope extends BaseTestCase { @Test - public void test() { + void test() { EBasicVer bean = new EBasicVer("first"); DB.save(bean); - //DB.cacheManager().setCaching(EBasicVer.class, true); - try (Transaction txn = DB.beginTransaction()) { EBasicVer bean1 = DB.find(EBasicVer.class, bean.getId()); // do an update of the name in the DB + // With #3295 this now automatically clears the persistence context for BasicVer.class int rowCount = DB.sqlUpdate("update e_basicver set name=? where id=?") .setParameter("second") .setParameter(bean.getId()) @@ -33,22 +31,129 @@ public void test() { assertEquals(1, rowCount); - // fetch the bean again... but doesn't hit DB as it - // is in the PersistenceContext which is transaction scoped + // Prior to #3295 this returns the same bean via transaction scoped Persistence Context + // With #3295 this is now a fresh bean + EBasicVer bean2 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .findOne(); + + // QUERY scope hits the DB (doesn't use the existing transactions persistence context) + EBasicVer bean3 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(QUERY) + .findOne(); + + // TRANSACTION scope ... same as bean2 and does not hit the DB + EBasicVer bean5 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(TRANSACTION) + .findOne(); + + assertEquals("first", bean.getName()); + assertEquals("first", bean1.getName()); + assertEquals("second", bean2.getName()); + assertEquals("second", bean3.getName()); + assertEquals("second", bean5.getName()); + assertNotSame(bean1, bean2); + assertNotSame(bean1, bean5); + assertSame(bean2, bean5); + assertNotSame(bean3, bean5); + + DB.delete(bean3); + + txn.commit(); + } + } + + @Test + void ormUpdateQuery_expect_clearsContext() { + + EBasicVer bean = new EBasicVer("first"); + DB.save(bean); + + try (Transaction txn = DB.beginTransaction()) { + EBasicVer bean1 = DB.find(EBasicVer.class, bean.getId()); + + // do an update of the name in the DB + // With #3295 this now automatically clears the persistence context for BasicVer.class + int rowCount = DB.update(EBasicVer.class) + .set("name", "second") + .where().idEq(bean.getId()) + .update(); + assertEquals(1, rowCount); + + // Prior to #3295 this returns the same bean via transaction scoped Persistence Context + // With #3295 this is now a fresh bean + EBasicVer bean2 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .findOne(); + + // QUERY scope hits the DB (doesn't use the existing transactions persistence context) + EBasicVer bean3 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(QUERY) + .findOne(); + + // TRANSACTION scope ... same as bean2 and does not hit the DB + EBasicVer bean5 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(TRANSACTION) + .findOne(); + + assertEquals("first", bean.getName()); + assertEquals("first", bean1.getName()); + assertEquals("second", bean2.getName()); + assertEquals("second", bean3.getName()); + assertEquals("second", bean5.getName()); + assertNotSame(bean1, bean2); + assertNotSame(bean1, bean5); + assertSame(bean2, bean5); + assertNotSame(bean3, bean5); + + DB.delete(bean3); + txn.commit(); + } + } + + @Test + void ormUpdate_expect_clearsContext() { + + EBasicVer bean = new EBasicVer("first"); + DB.save(bean); + + try (Transaction txn = DB.beginTransaction()) { + EBasicVer bean1 = DB.find(EBasicVer.class, bean.getId()); + + // do an update of the name in the DB + // With #3295 this now automatically clears the persistence context for BasicVer.class + int rowCount = DB.createUpdate(EBasicVer.class, "update ebasicver set name = ? where id = ?") + .setParameter(1, "second") + .setParameter(2, bean.getId()) + .execute(); + assertEquals(1, rowCount); + + // fetch the bean again... + // Prior to #3295 this returns the same bean via transaction scoped Persistence Context + // With #3295 this is now a fresh bean EBasicVer bean2 = DB.find(EBasicVer.class) .setId(bean.getId()) .setUseCache(false) // ignore L2 cache .findOne(); // QUERY scope hits the DB (doesn't use the existing transactions persistence context) - // ... also explicitly not use bean cache EBasicVer bean3 = DB.find(EBasicVer.class) .setId(bean.getId()) .setUseCache(false) // ignore L2 cache .setPersistenceContextScope(QUERY) .findOne(); - // TRANsACTION scope ... same as bean2 and does not hit the DB + // TRANSACTION scope ... same as bean2 and does not hit the DB EBasicVer bean5 = DB.find(EBasicVer.class) .setId(bean.getId()) .setUseCache(false) // ignore L2 cache @@ -57,12 +162,69 @@ public void test() { assertEquals("first", bean.getName()); assertEquals("first", bean1.getName()); - assertEquals("first", bean2.getName()); - assertEquals("first", bean5.getName()); - assertSame(bean1, bean2); - assertSame(bean1, bean5); + assertEquals("second", bean2.getName()); + assertEquals("second", bean3.getName()); + assertEquals("second", bean5.getName()); + assertNotSame(bean1, bean2); + assertNotSame(bean1, bean5); + assertSame(bean2, bean5); + assertNotSame(bean3, bean5); + DB.delete(bean3); + txn.commit(); + } + } + + + @Test + void ormUpdateQueryWithAutoPersist_expect_clearsContext() { + + EBasicVer bean = new EBasicVer("first"); + DB.save(bean); + + try (Transaction txn = DB.beginTransaction()) { + txn.setAutoPersistUpdates(true); + EBasicVer bean1 = DB.find(EBasicVer.class, bean.getId()); + + // do an update of the name in the DB + // With #3295 this now automatically clears the persistence context for BasicVer.class + int rowCount = DB.update(EBasicVer.class) + .set("name", "second") + .where().idEq(bean.getId()) + .update(); + assertEquals(1, rowCount); + + // Prior to #3295 this returns the same bean via transaction scoped Persistence Context + // With #3295 this is now a fresh bean + EBasicVer bean2 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .findOne(); + + // QUERY scope hits the DB (doesn't use the existing transactions persistence context) + EBasicVer bean3 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(QUERY) + .findOne(); + + // TRANSACTION scope ... same as bean2 and does not hit the DB + EBasicVer bean5 = DB.find(EBasicVer.class) + .setId(bean.getId()) + .setUseCache(false) // ignore L2 cache + .setPersistenceContextScope(TRANSACTION) + .findOne(); + + assertEquals("first", bean.getName()); + assertEquals("first", bean1.getName()); + assertEquals("first", bean2.getName()); // This is still first assertEquals("second", bean3.getName()); + assertEquals("first", bean5.getName()); // This is still first + assertSame(bean1, bean2); // Now the same + assertSame(bean1, bean5); // Now the same + assertSame(bean2, bean5); + assertNotSame(bean3, bean5); + DB.delete(bean3); txn.commit();