From 66234249c63edc4733922c80224e5613d13b53fe Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 9 Jan 2024 07:44:27 +1300 Subject: [PATCH] #3295 Clear PersistenceContext on execution of bulk updates or deletes Using SqlUpdate or an ORM Update query clear the appropriate part of the PersistenceContext. The effect is that ORM queries executed after a bulk update will effectively load a fresh copy of the data from the database and will not reuse an instance from the persistence context if the bean in question had already been loaded. --- .../server/core/OrmQueryRequest.java | 1 + .../server/core/PersistRequestOrmUpdate.java | 5 +- .../server/core/PersistRequestUpdateSql.java | 11 ++ .../server/deploy/BeanDescriptor.java | 7 + .../server/deploy/BeanDescriptorManager.java | 2 +- .../TestPersistenceContextQueryScope.java | 125 ++++++++++++++++-- 6 files changed, 136 insertions(+), 15 deletions(-) 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 3dfca446b..830b825f5 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 @@ -338,6 +338,7 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery private int notifyCache(int rows, boolean update) { if (rows > 0) { + beanDescriptor.contextClear(transaction.persistenceContext()); beanDescriptor.cacheUpdateQuery(update, transaction); } return rows; 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 03aa874f8..4c6771cbb 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 final class PersistRequestOrmUpdate extends PersistRequest { */ @Override public void postExecute() { + OrmUpdateType ormUpdateType = ormUpdate.ormUpdateType(); + if (OrmUpdateType.INSERT != ormUpdateType) { + 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 e4f0c023f..dc6d0b5de 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 @@ package io.ebeaninternal.server.core; 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 final class PersistRequestUpdateSql extends PersistRequest { */ @Override public void postExecute() { + if (sqlType != SqlType.SQL_INSERT) { + 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 081ae5d77..dbb803fc9 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 class BeanDescriptor implements BeanType, STreeType, SpiBeanType { 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 240ab41a2..c4d179e6d 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 final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy * 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-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java b/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java index 56988bc4b..f82f855d1 100644 --- a/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java +++ b/ebean-test/src/test/java/org/tests/persistencecontext/TestPersistenceContextQueryScope.java @@ -1,5 +1,6 @@ package org.tests.persistencecontext; +import io.ebean.Transaction; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import org.junit.jupiter.api.Test; @@ -7,22 +8,20 @@ import org.tests.model.basic.EBasicVer; 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); - DB.beginTransaction(); - try { + try (Transaction txn = DB.beginTransaction()) { EBasicVer bean1 = DB.find(EBasicVer.class, bean.getId()); // do an update of the name in the DB @@ -57,18 +56,118 @@ public class TestPersistenceContextQueryScope extends BaseTestCase { 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", bean5.getName()); + assertNotSame(bean1, bean2); + assertNotSame(bean1, bean5); assertEquals("second", bean3.getName()); DB.delete(bean3); - DB.commitTransaction(); + txn.commit(); + } + } - } finally { - DB.endTransaction(); + @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 + int rowCount = DB.update(EBasicVer.class) + .set("name", "second") + .where().idEq(bean.getId()) + .update(); + assertEquals(1, rowCount); + + // fetch the bean again... but doesn't hit DB as it + // is in the PersistenceContext which is transaction scoped + 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 + 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); + + 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 + 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... but doesn't hit DB as it + // is in the PersistenceContext which is transaction scoped + 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 + 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); + + DB.delete(bean3); + txn.commit(); } } }