diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/revisioninfo/DefaultRevisionInfoGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/revisioninfo/DefaultRevisionInfoGenerator.java index e9aa7bb9bc..be774ff2f9 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/revisioninfo/DefaultRevisionInfoGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/revisioninfo/DefaultRevisionInfoGenerator.java @@ -29,6 +29,7 @@ import org.hibernate.envers.EntityTrackingRevisionListener; import org.hibernate.envers.RevisionListener; import org.hibernate.envers.RevisionType; import org.hibernate.envers.entities.PropertyData; +import org.hibernate.envers.synchronization.SessionCacheCleaner; import org.hibernate.envers.tools.reflection.ReflectionTools; import org.hibernate.property.Setter; @@ -45,6 +46,7 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { private final Setter revisionTimestampSetter; private final boolean timestampAsDate; private final Class revisionInfoClass; + private final SessionCacheCleaner sessionCacheCleaner; public DefaultRevisionInfoGenerator(String revisionInfoEntityName, Class revisionInfoClass, Class listenerClass, @@ -69,10 +71,13 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { // Default listener - none listener = null; } + + sessionCacheCleaner = new SessionCacheCleaner(); } public void saveRevisionData(Session session, Object revisionData) { session.save(revisionInfoEntityName, revisionData); + sessionCacheCleaner.scheduleAuditDataRemoval(session, revisionData); } public Object generate() { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/DefaultAuditStrategy.java b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/DefaultAuditStrategy.java index 14d3ae2328..4783962acf 100755 --- a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/DefaultAuditStrategy.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/DefaultAuditStrategy.java @@ -1,13 +1,17 @@ -package org.hibernate.envers.strategy; -import java.io.Serializable; -import org.hibernate.Session; -import org.hibernate.envers.configuration.AuditConfiguration; -import org.hibernate.envers.configuration.GlobalConfiguration; -import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData; -import org.hibernate.envers.entities.mapper.relation.MiddleComponentData; -import org.hibernate.envers.entities.mapper.relation.MiddleIdData; -import org.hibernate.envers.tools.query.Parameters; -import org.hibernate.envers.tools.query.QueryBuilder; +package org.hibernate.envers.strategy; +import java.io.Serializable; +import org.hibernate.Session; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.envers.configuration.AuditConfiguration; +import org.hibernate.envers.configuration.GlobalConfiguration; +import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData; +import org.hibernate.envers.entities.mapper.relation.MiddleComponentData; +import org.hibernate.envers.entities.mapper.relation.MiddleIdData; +import org.hibernate.envers.synchronization.SessionCacheCleaner; +import org.hibernate.envers.tools.query.Parameters; +import org.hibernate.envers.tools.query.QueryBuilder; +import org.hibernate.event.spi.EventSource; /** * Default strategy is to simply persist the audit data. @@ -16,14 +20,22 @@ import org.hibernate.envers.tools.query.QueryBuilder; * @author Stephanie Pau */ public class DefaultAuditStrategy implements AuditStrategy { + private final SessionCacheCleaner sessionCacheCleaner; + + public DefaultAuditStrategy() { + sessionCacheCleaner = new SessionCacheCleaner(); + } + public void perform(Session session, String entityName, AuditConfiguration auditCfg, Serializable id, Object data, Object revision) { session.save(auditCfg.getAuditEntCfg().getAuditEntityName(entityName), data); + sessionCacheCleaner.scheduleAuditDataRemoval(session, data); } public void performCollectionChange(Session session, AuditConfiguration auditCfg, PersistentCollectionChangeData persistentCollectionChangeData, Object revision) { session.save(persistentCollectionChangeData.getEntityName(), persistentCollectionChangeData.getData()); + sessionCacheCleaner.scheduleAuditDataRemoval(session, persistentCollectionChangeData.getData()); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/ValidityAuditStrategy.java b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/ValidityAuditStrategy.java index d6b8ea4537..afa399567b 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/ValidityAuditStrategy.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/ValidityAuditStrategy.java @@ -12,6 +12,7 @@ import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData; import org.hibernate.envers.entities.mapper.id.IdMapper; import org.hibernate.envers.entities.mapper.relation.MiddleComponentData; import org.hibernate.envers.entities.mapper.relation.MiddleIdData; +import org.hibernate.envers.synchronization.SessionCacheCleaner; import org.hibernate.envers.tools.query.Parameters; import org.hibernate.envers.tools.query.QueryBuilder; import org.hibernate.property.Getter; @@ -43,6 +44,12 @@ public class ValidityAuditStrategy implements AuditStrategy { /** getter for the revision entity field annotated with @RevisionTimestamp */ private Getter revisionTimestampGetter = null; + private final SessionCacheCleaner sessionCacheCleaner; + + public ValidityAuditStrategy() { + sessionCacheCleaner = new SessionCacheCleaner(); + } + public void perform(Session session, String entityName, AuditConfiguration auditCfg, Serializable id, Object data, Object revision) { AuditEntitiesConfiguration audEntCfg = auditCfg.getAuditEntCfg(); @@ -71,6 +78,7 @@ public class ValidityAuditStrategy implements AuditStrategy { // Save the audit data session.save(auditedEntityName, data); + sessionCacheCleaner.scheduleAuditDataRemoval(session, data); } @SuppressWarnings({"unchecked"}) @@ -103,6 +111,7 @@ public class ValidityAuditStrategy implements AuditStrategy { // Save the audit data session.save(persistentCollectionChangeData.getEntityName(), persistentCollectionChangeData.getData()); + sessionCacheCleaner.scheduleAuditDataRemoval(session, persistentCollectionChangeData.getData()); } private void addEndRevisionNulLRestriction(AuditConfiguration auditCfg, QueryBuilder qb) { @@ -175,7 +184,7 @@ public class ValidityAuditStrategy implements AuditStrategy { // Saving the previous version session.save(auditedEntityName, previousData); - + sessionCacheCleaner.scheduleAuditDataRemoval(session, previousData); } else { throw new RuntimeException("Cannot find previous revision for entity " + auditedEntityName + " and id " + id); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/synchronization/SessionCacheCleaner.java b/hibernate-envers/src/main/java/org/hibernate/envers/synchronization/SessionCacheCleaner.java new file mode 100644 index 0000000000..177f022acb --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/synchronization/SessionCacheCleaner.java @@ -0,0 +1,27 @@ +package org.hibernate.envers.synchronization; + +import org.hibernate.Session; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.event.spi.EventSource; + +/** + * Class responsible for evicting audit data entries that have been stored in the session level cache. + * This operation increases Envers performance in case of massive entity updates without clearing persistence context. + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class SessionCacheCleaner { + /** + * Schedules audit data removal from session level cache after transaction completion. The operation is performed + * regardless of commit success. + * @param session Active Hibernate session. + * @param data Audit data that shall be evicted (e.g. revision data or entity snapshot) + */ + public void scheduleAuditDataRemoval(final Session session, final Object data) { + ((EventSource) session).getActionQueue().registerProcess(new AfterTransactionCompletionProcess() { + public void doAfterTransactionCompletion(boolean success, SessionImplementor session) { + ((Session) session).evict(data); + } + }); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/performance/EvictAuditDataAfterCommitTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/performance/EvictAuditDataAfterCommitTest.java new file mode 100644 index 0000000000..97639f9567 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/performance/EvictAuditDataAfterCommitTest.java @@ -0,0 +1,104 @@ +package org.hibernate.envers.test.performance; + +import org.hibernate.MappingException; +import org.hibernate.Session; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.envers.DefaultRevisionEntity; +import org.hibernate.envers.test.AbstractSessionTest; +import org.hibernate.envers.test.entities.StrTestEntity; +import org.hibernate.envers.test.entities.onetomany.SetRefEdEntity; +import org.hibernate.envers.test.entities.onetomany.SetRefIngEntity; +import org.hibernate.testing.TestForIssue; +import org.junit.Assert; +import org.junit.Test; + +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class EvictAuditDataAfterCommitTest extends AbstractSessionTest { + @Override + protected void initMappings() throws MappingException, URISyntaxException { + config.addAnnotatedClass(StrTestEntity.class); + config.addAnnotatedClass(SetRefEdEntity.class); + config.addAnnotatedClass(SetRefIngEntity.class); + } + + @Test + @TestForIssue(jiraKey = "HHH-6614") + public void testSessionCacheClear() { + getSession().getTransaction().begin(); + StrTestEntity ste = new StrTestEntity("data"); + getSession().persist(ste); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), "org.hibernate.envers.test.entities.StrTestEntity_AUD"); + } + + @Test + @TestForIssue(jiraKey = "HHH-6614") + public void testSessionCacheCollectionClear() { + final String[] auditEntityNames = new String[] {"org.hibernate.envers.test.entities.onetomany.SetRefEdEntity_AUD", + "org.hibernate.envers.test.entities.onetomany.SetRefIngEntity_AUD"}; + + SetRefEdEntity ed1 = new SetRefEdEntity(1, "data_ed_1"); + SetRefEdEntity ed2 = new SetRefEdEntity(2, "data_ed_2"); + SetRefIngEntity ing1 = new SetRefIngEntity(3, "data_ing_1"); + SetRefIngEntity ing2 = new SetRefIngEntity(4, "data_ing_2"); + + getSession().getTransaction().begin(); + getSession().persist(ed1); + getSession().persist(ed2); + getSession().persist(ing1); + getSession().persist(ing2); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), auditEntityNames); + + getSession().getTransaction().begin(); + ed1 = (SetRefEdEntity) getSession().load(SetRefEdEntity.class, ed1.getId()); + ing1.setReference(ed1); + ing2.setReference(ed1); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), auditEntityNames); + + getSession().getTransaction().begin(); + ed2 = (SetRefEdEntity) getSession().load(SetRefEdEntity.class, ed2.getId()); + Set reffering = new HashSet(); + reffering.add(ing1); + reffering.add(ing2); + ed2.setReffering(reffering); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), auditEntityNames); + + getSession().getTransaction().begin(); + ed2 = (SetRefEdEntity) getSession().load(SetRefEdEntity.class, ed2.getId()); + ed2.getReffering().remove(ing1); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), auditEntityNames); + + getSession().getTransaction().begin(); + ed2 = (SetRefEdEntity) getSession().load(SetRefEdEntity.class, ed2.getId()); + ed2.getReffering().iterator().next().setData("mod_data_ing_2"); + getSession().getTransaction().commit(); + checkEmptyAuditSessionCache(getSession(), auditEntityNames); + } + + private void checkEmptyAuditSessionCache(Session session, String ... auditEntityNames) { + List entityNames = Arrays.asList(auditEntityNames); + PersistenceContext persistenceContext = ((SessionImplementor) session).getPersistenceContext(); + for (Object entry : persistenceContext.getEntityEntries().values()) { + EntityEntry entityEntry = (EntityEntry) entry; + if (entityNames.contains(entityEntry.getEntityName())) { + assert false : "Audit data shall not be stored in the session level cache. This causes performance issues."; + } + Assert.assertFalse("Revision entity shall not be stored in the session level cache. This causes performance issues.", + DefaultRevisionEntity.class.getName().equals(entityEntry.getEntityName())); + } + } +} \ No newline at end of file