HHH-7807 - Cascade delete revisions

Conflicts:
	documentation/src/main/docbook/devguide/en-US/Envers.xml
	hibernate-envers/src/main/java/org/hibernate/envers/configuration/GlobalConfiguration.java
	hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/AuditMetadataGenerator.java
This commit is contained in:
Lukasz Antoniak 2013-08-05 23:42:05 -04:00 committed by Brett Meyer
parent 5255c5074f
commit 456dfd83f9
9 changed files with 182 additions and 0 deletions

View File

@ -326,6 +326,18 @@
Name of column used for storing ordinal of the change in sets of embeddable elements.
</entry>
</row>
<row>
<entry>
<property>org.hibernate.envers.cascade_delete_revision</property>
</entry>
<entry>
false
</entry>
<entry>
While deleting revision entry, remove data of associated audited entities.
Requires database support for cascade row removal.
</entry>
</row>
<row>
<entry>
<property>org.hibernate.envers.allow_identifier_reuse</property>

View File

@ -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;

View File

@ -889,6 +889,14 @@ arbitrary number of queries, and import declarations of arbitrary classes.
<xs:attribute name="foreign-key" type="xs:string"/>
<xs:attribute name="lazy" type="lazy-attribute"/>
<xs:attribute name="name" use="required" type="xs:string"/>
<xs:attribute name="on-delete" default="noaction">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="cascade"/>
<xs:enumeration value="noaction"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<!-- A property embedded in a composite identifier or map index (always not-null). -->

View File

@ -75,6 +75,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;
@ -107,6 +110,9 @@ public class GlobalConfiguration {
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
);
@ -192,6 +198,10 @@ public class GlobalConfiguration {
return useRevisionEntityWithNativeId;
}
public boolean isCascadeDeleteRevision() {
return cascadeDeleteRevision;
}
public boolean isAllowIdentifierReuse() {
return allowIdentifierReuse;
}

View File

@ -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() );

View File

@ -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<ListOwningEntity>() );
owning.setReferences( new ArrayList<ListOwnedEntity>() );
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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();