diff --git a/documentation/src/main/docbook/devguide/en-US/Envers.xml b/documentation/src/main/docbook/devguide/en-US/Envers.xml index fe981b5223..86651fb8f8 100644 --- a/documentation/src/main/docbook/devguide/en-US/Envers.xml +++ b/documentation/src/main/docbook/devguide/en-US/Envers.xml @@ -326,6 +326,18 @@ Name of column used for storing ordinal of the change in sets of embeddable elements. + + + org.hibernate.envers.cascade_delete_revision + + + false + + + While deleting revision entry, remove data of associated audited entities. + Requires database support for cascade row removal. + + org.hibernate.envers.allow_identifier_reuse diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/HbmBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/HbmBinder.java index 0cea5956ae..64166cd818 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/HbmBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/HbmBinder.java @@ -2322,6 +2322,7 @@ public final class HbmBinder { if ( propertyRef != null ) { mappings.addUniquePropertyReference( toOne.getReferencedEntityName(), propertyRef ); } + toOne.setCascadeDeleteEnabled( "cascade".equals( subnode.attributeValue( "on-delete" ) ) ); } else if ( value instanceof Collection ) { Collection coll = (Collection) value; diff --git a/hibernate-core/src/main/resources/org/hibernate/hibernate-mapping-4.0.xsd b/hibernate-core/src/main/resources/org/hibernate/hibernate-mapping-4.0.xsd index 530bff1d22..ae67440487 100644 --- a/hibernate-core/src/main/resources/org/hibernate/hibernate-mapping-4.0.xsd +++ b/hibernate-core/src/main/resources/org/hibernate/hibernate-mapping-4.0.xsd @@ -889,6 +889,14 @@ arbitrary number of queries, and import declarations of arbitrary classes. + + + + + + + + diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/GlobalConfiguration.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/GlobalConfiguration.java index 1bcf57eb30..61516f9b71 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/GlobalConfiguration.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/GlobalConfiguration.java @@ -74,6 +74,9 @@ public class GlobalConfiguration { // Use revision entity with native id generator private final boolean useRevisionEntityWithNativeId; + + // While deleting revision entry, remove data of associated audited entities + private final boolean cascadeDeleteRevision; // Support reused identifiers of previously deleted entities private final boolean allowIdentifierReuse; @@ -106,6 +109,9 @@ public class GlobalConfiguration { trackEntitiesChangedInRevision = ConfigurationHelper.getBoolean( EnversSettings.TRACK_ENTITIES_CHANGED_IN_REVISION, properties, false ); + + cascadeDeleteRevision = ConfigurationHelper.getBoolean( + "org.hibernate.envers.cascade_delete_revision", properties, false ); useRevisionEntityWithNativeId = ConfigurationHelper.getBoolean( EnversSettings.USE_REVISION_ENTITY_WITH_NATIVE_ID, properties, true @@ -191,6 +197,10 @@ public class GlobalConfiguration { public boolean isUseRevisionEntityWithNativeId() { return useRevisionEntityWithNativeId; } + + public boolean isCascadeDeleteRevision() { + return cascadeDeleteRevision; + } public boolean isAllowIdentifierReuse() { return allowIdentifierReuse; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java index a117e85c96..926a81ac26 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java @@ -139,6 +139,9 @@ public final class AuditMetadataGenerator { private Element cloneAndSetupRevisionInfoRelationMapping() { final Element revMapping = (Element) revisionInfoRelationMapping.clone(); revMapping.addAttribute( "name", verEntCfg.getRevisionFieldName() ); + if ( globalCfg.isCascadeDeleteRevision() ) { + revMapping.addAttribute( "on-delete", "cascade" ); + } MetadataTools.addOrModifyColumn( revMapping, verEntCfg.getRevisionFieldName() ); diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/AbstractRevisionEntityRemovalTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/AbstractRevisionEntityRemovalTest.java new file mode 100644 index 0000000000..2b8b5b48a0 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/AbstractRevisionEntityRemovalTest.java @@ -0,0 +1,108 @@ +package org.hibernate.envers.test.integration.reventity.removal; + +import java.util.ArrayList; +import java.util.Map; +import javax.persistence.EntityManager; + +import org.junit.Assert; +import org.junit.Test; + +import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase; +import org.hibernate.envers.test.Priority; +import org.hibernate.envers.test.entities.StrTestEntity; +import org.hibernate.envers.test.entities.manytomany.ListOwnedEntity; +import org.hibernate.envers.test.entities.manytomany.ListOwningEntity; +import org.hibernate.testing.DialectChecks; +import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.TestForIssue; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +@TestForIssue( jiraKey = "HHH-7807" ) +@RequiresDialectFeature(DialectChecks.SupportsCascadeDeleteCheck.class) +public abstract class AbstractRevisionEntityRemovalTest extends BaseEnversJPAFunctionalTestCase { + @Override + protected void addConfigOptions(Map options) { + options.put( "org.hibernate.envers.cascade_delete_revision", "true" ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + StrTestEntity.class, ListOwnedEntity.class, ListOwningEntity.class, + getRevisionEntityClass() + }; + } + + @Test + @Priority(10) + public void initData() { + EntityManager em = getEntityManager(); + + // Revision 1 - simple entity + em.getTransaction().begin(); + em.persist( new StrTestEntity( "data" ) ); + em.getTransaction().commit(); + + // Revision 2 - many-to-many relation + em.getTransaction().begin(); + ListOwnedEntity owned = new ListOwnedEntity( 1, "data" ); + ListOwningEntity owning = new ListOwningEntity( 1, "data" ); + owned.setReferencing( new ArrayList() ); + owning.setReferences( new ArrayList() ); + owned.getReferencing().add( owning ); + owning.getReferences().add( owned ); + em.persist( owned ); + em.persist( owning ); + em.getTransaction().commit(); + + em.getTransaction().begin(); + Assert.assertEquals( 1, countRecords( em, "STR_TEST_AUD" ) ); + Assert.assertEquals( 1, countRecords( em, "ListOwned_AUD" ) ); + Assert.assertEquals( 1, countRecords( em, "ListOwning_AUD" ) ); + Assert.assertEquals( 1, countRecords( em, "ListOwning_ListOwned_AUD" ) ); + em.getTransaction().commit(); + + em.close(); + } + + @Test + @Priority(9) + public void testRemoveExistingRevisions() { + EntityManager em = getEntityManager(); + removeRevision( em, 1 ); + removeRevision( em, 2 ); + em.close(); + } + + @Test + @Priority(8) + public void testEmptyAuditTables() { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + + Assert.assertEquals( 0, countRecords( em, "STR_TEST_AUD" ) ); + Assert.assertEquals( 0, countRecords( em, "ListOwned_AUD" ) ); + Assert.assertEquals( 0, countRecords( em, "ListOwning_AUD" ) ); + Assert.assertEquals( 0, countRecords( em, "ListOwning_ListOwned_AUD" ) ); + + em.getTransaction().commit(); + em.close(); + } + + private int countRecords(EntityManager em, String tableName) { + return ( (Number) em.createNativeQuery( "SELECT COUNT(*) FROM " + tableName ).getSingleResult() ).intValue(); + } + + private void removeRevision(EntityManager em, Number number) { + em.getTransaction().begin(); + Object entity = em.find( getRevisionEntityClass(), number ); + Assert.assertNotNull( entity ); + em.remove( entity ); + em.getTransaction().commit(); + Assert.assertNull( em.find( getRevisionEntityClass(), number ) ); + } + + protected abstract Class getRevisionEntityClass(); +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveDefaultRevisionEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveDefaultRevisionEntity.java new file mode 100644 index 0000000000..af2cf50ad6 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveDefaultRevisionEntity.java @@ -0,0 +1,13 @@ +package org.hibernate.envers.test.integration.reventity.removal; + +import org.hibernate.envers.enhanced.SequenceIdRevisionEntity; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class RemoveDefaultRevisionEntity extends AbstractRevisionEntityRemovalTest { + @Override + protected Class getRevisionEntityClass() { + return SequenceIdRevisionEntity.class; + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveTrackingRevisionEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveTrackingRevisionEntity.java new file mode 100644 index 0000000000..05f4a1bf9a --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/removal/RemoveTrackingRevisionEntity.java @@ -0,0 +1,21 @@ +package org.hibernate.envers.test.integration.reventity.removal; + +import java.util.Map; + +import org.hibernate.envers.enhanced.SequenceIdTrackingModifiedEntitiesRevisionEntity; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class RemoveTrackingRevisionEntity extends AbstractRevisionEntityRemovalTest { + @Override + public void addConfigOptions(Map configuration) { + super.addConfigOptions( configuration ); + configuration.put("org.hibernate.envers.track_entities_changed_in_revision", "true"); + } + + @Override + protected Class getRevisionEntityClass() { + return SequenceIdTrackingModifiedEntitiesRevisionEntity.class; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java index f9e29799d8..3616d96d18 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java @@ -80,6 +80,12 @@ abstract public class DialectChecks { } } + public static class SupportsCascadeDeleteCheck implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsCascadeDelete(); + } + } + public static class SupportsCircularCascadeDeleteCheck implements DialectCheck { public boolean isMatch(Dialect dialect) { return dialect.supportsCircularCascadeDeleteConstraints();