HHH-8280 - Identifiers reuse option

This commit is contained in:
Lukasz Antoniak 2013-08-01 13:13:31 +02:00
parent 80873328d1
commit dd1bec53fb
6 changed files with 111 additions and 71 deletions

View File

@ -326,6 +326,19 @@
Name of column used for storing ordinal of the change in sets of embeddable elements. Name of column used for storing ordinal of the change in sets of embeddable elements.
</entry> </entry>
</row> </row>
<row>
<entry>
<property>org.hibernate.envers.allow_identifier_reuse</property>
</entry>
<entry>
false
</entry>
<entry>
Guarantees proper validity audit strategy behavior when application reuses identifiers
of deleted entities. Exactly one row with <literal>null</literal> end date exists
for each identifier.
</entry>
</row>
</tbody> </tbody>
</tgroup> </tgroup>
</table> </table>

View File

@ -125,4 +125,10 @@ public interface EnversSettings {
* Name of column used for storing ordinal of the change in sets of embeddable elements. Defaults to {@literal SETORDINAL}. * Name of column used for storing ordinal of the change in sets of embeddable elements. Defaults to {@literal SETORDINAL}.
*/ */
public static final String EMBEDDABLE_SET_ORDINAL_FIELD_NAME = "org.hibernate.envers.embeddable_set_ordinal_field_name"; public static final String EMBEDDABLE_SET_ORDINAL_FIELD_NAME = "org.hibernate.envers.embeddable_set_ordinal_field_name";
/**
* Guarantees proper validity audit strategy behavior when application reuses identifiers of deleted entities.
* Exactly one row with {@code null} end date exists for each identifier.
*/
public static final String ALLOW_IDENTIFIER_REUSE = "org.hibernate.envers.allow_identifier_reuse";
} }

View File

@ -75,6 +75,9 @@ public class GlobalConfiguration {
// Use revision entity with native id generator // Use revision entity with native id generator
private final boolean useRevisionEntityWithNativeId; private final boolean useRevisionEntityWithNativeId;
// Support reused identifiers of previously deleted entities
private final boolean allowIdentifierReuse;
/* /*
Which operator to use in correlated subqueries (when we want a property to be equal to the result of Which operator to use in correlated subqueries (when we want a property to be equal to the result of
a correlated subquery, for example: e.p <operator> (select max(e2.p) where e2.p2 = e.p2 ...). a correlated subquery, for example: e.p <operator> (select max(e2.p) where e2.p2 = e.p2 ...).
@ -131,6 +134,10 @@ public class GlobalConfiguration {
else { else {
revisionListenerClass = null; revisionListenerClass = null;
} }
allowIdentifierReuse = ConfigurationHelper.getBoolean(
EnversSettings.ALLOW_IDENTIFIER_REUSE, properties, false
);
} }
public boolean isGenerateRevisionsForCollections() { public boolean isGenerateRevisionsForCollections() {
@ -184,4 +191,8 @@ public class GlobalConfiguration {
public boolean isUseRevisionEntityWithNativeId() { public boolean isUseRevisionEntityWithNativeId() {
return useRevisionEntityWithNativeId; return useRevisionEntityWithNativeId;
} }
public boolean isAllowIdentifierReuse() {
return allowIdentifierReuse;
}
} }

View File

@ -100,10 +100,13 @@ public class ValidityAuditStrategy implements AuditStrategy {
// Update the end date of the previous row. // Update the end date of the previous row.
// //
// The statement will no-op if an entity with a given identifier has been // When application reuses identifiers of previously removed entities:
// The UPDATE statement will no-op if an entity with a given identifier has been
// inserted for the first time. But in case a deleted primary key value was // inserted for the first time. But in case a deleted primary key value was
// reused, this guarantees correct strategy behavior: exactly one row with // reused, this guarantees correct strategy behavior: exactly one row with
// a null end date exists for each identifier. // null end date exists for each identifier.
final boolean reuseEntityIdentifier = auditCfg.getGlobalCfg().isAllowIdentifierReuse();
if ( reuseEntityIdentifier || getRevisionType( auditCfg, data ) != RevisionType.ADD ) {
final Queryable productionEntityQueryable = getQueryable( entityName, sessionImplementor ); final Queryable productionEntityQueryable = getQueryable( entityName, sessionImplementor );
final Queryable rootProductionEntityQueryable = getQueryable( final Queryable rootProductionEntityQueryable = getQueryable(
productionEntityQueryable.getRootEntityName(), productionEntityQueryable.getRootEntityName(),
@ -242,12 +245,13 @@ public class ValidityAuditStrategy implements AuditStrategy {
} }
); );
if ( rowCount != 1 && getRevisionType(auditCfg, data) != RevisionType.ADD ) { if ( rowCount != 1 && ( !reuseEntityIdentifier || ( getRevisionType( auditCfg, data ) != RevisionType.ADD ) ) ) {
throw new RuntimeException( throw new RuntimeException(
"Cannot update previous revision for entity " + auditedEntityName + " and id " + id "Cannot update previous revision for entity " + auditedEntityName + " and id " + id
); );
} }
} }
}
private Queryable getQueryable(String entityName, SessionImplementor sessionImplementor) { private Queryable getQueryable(String entityName, SessionImplementor sessionImplementor) {
return (Queryable) sessionImplementor.getFactory().getEntityPersister( entityName ); return (Queryable) sessionImplementor.getFactory().getEntityPersister( entityName );

View File

@ -0,0 +1,73 @@
package org.hibernate.envers.test.integration.strategy;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import java.util.Arrays;
import java.util.Map;
import org.hibernate.envers.configuration.EnversSettings;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.entities.IntNoAutoIdTestEntity;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Tests that reusing identifiers doesn't cause auditing misbehavior.
*
* @author adar
*/
@TestForIssue(jiraKey = "HHH-8280")
public class IdentifierReuseTest extends BaseEnversJPAFunctionalTestCase {
@Override
protected void addConfigOptions(Map options) {
options.put( EnversSettings.ALLOW_IDENTIFIER_REUSE, "true" );
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { IntNoAutoIdTestEntity.class };
}
@Test
public void testIdentifierReuse() {
final Integer reusedId = 1;
EntityManager entityManager = getEntityManager();
saveUpdateAndRemoveEntity( entityManager, reusedId );
saveUpdateAndRemoveEntity( entityManager, reusedId );
entityManager.close();
assertEquals(
Arrays.asList( 1, 2, 3, 4, 5, 6 ),
getAuditReader().getRevisions( IntNoAutoIdTestEntity.class, reusedId )
);
}
private void saveUpdateAndRemoveEntity(EntityManager entityManager, Integer id) {
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
IntNoAutoIdTestEntity entity = new IntNoAutoIdTestEntity( 0, id );
entityManager.persist( entity );
assertEquals( id, entity.getId() );
transaction.commit();
transaction.begin();
entity = entityManager.find( IntNoAutoIdTestEntity.class, id );
entity.setNumVal( 1 );
entity = entityManager.merge( entity );
assertEquals( id, entity.getId() );
transaction.commit();
transaction.begin();
entity = entityManager.find( IntNoAutoIdTestEntity.class, id );
assertNotNull( entity );
entityManager.remove( entity );
transaction.commit();
}
}

View File

@ -1,67 +0,0 @@
package org.hibernate.envers.test.integration.strategy;
import static org.junit.Assert.*;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import org.hibernate.envers.configuration.EnversSettings;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.entities.IntNoAutoIdTestEntity;
import org.hibernate.testing.TestForIssue;
import org.junit.Test;
/**
* Tests that reusing identifiers doesn't cause {@link ValidityAuditstrategy}
* to misbehave.
*
* @author adar
*
*/
@TestForIssue(jiraKey = "HHH-8280")
public class ValidityAuditStrategyIdentifierReuseTest extends
BaseEnversJPAFunctionalTestCase {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
protected void addConfigOptions(Map options) {
options.put(EnversSettings.AUDIT_STRATEGY,
"org.hibernate.envers.strategy.ValidityAuditStrategy");
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { IntNoAutoIdTestEntity.class };
}
private void saveRemoveEntity(EntityManager em, Integer id) {
EntityTransaction et = em.getTransaction();
et.begin();
IntNoAutoIdTestEntity e = new IntNoAutoIdTestEntity(0, id);
em.persist(e);
assertEquals(id, e.getId());
et.commit();
et.begin();
e = em.find(IntNoAutoIdTestEntity.class, id);
assertNotNull(e);
em.remove(e);
et.commit();
}
@Test
public void testValidityAuditStrategyDoesntThrowWhenIdentifierIsReused() {
final Integer reusedID = 1;
EntityManager em = getEntityManager();
try {
saveRemoveEntity(em, reusedID);
saveRemoveEntity(em, reusedID);
} finally {
em.close();
}
}
}