Permalink
Browse files

HHH-7295 - ParentAuditing Test in Envers Testsuite fails across diffe…

…rent databases with different error messages because Hibernate does not set LockMode correctly
  • Loading branch information...
1 parent ed266c5 commit 835f4452ebc4ee08fdf014e698994c2c31a92aeb @sebersole sebersole committed May 29, 2012
Showing with 155 additions and 63 deletions.
  1. +155 −63 hibernate-envers/src/main/java/org/hibernate/envers/strategy/ValidityAuditStrategy.java
@@ -1,9 +1,16 @@
package org.hibernate.envers.strategy;
import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
import org.hibernate.LockOptions;
import org.hibernate.Session;
@@ -13,16 +20,23 @@
import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.envers.configuration.AuditEntitiesConfiguration;
import org.hibernate.envers.configuration.GlobalConfiguration;
-import org.hibernate.envers.entities.EntityConfiguration;
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.envers.tools.query.UpdateBuilder;
+import org.hibernate.event.service.spi.EventListenerRegistry;
+import org.hibernate.event.spi.AutoFlushEvent;
+import org.hibernate.event.spi.AutoFlushEventListener;
+import org.hibernate.event.spi.EventSource;
+import org.hibernate.event.spi.EventType;
+import org.hibernate.jdbc.ReturningWork;
+import org.hibernate.persister.entity.EntityPersister;
+import org.hibernate.persister.entity.Queryable;
import org.hibernate.property.Getter;
+import org.hibernate.sql.Update;
+import org.hibernate.type.Type;
import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants.MIDDLE_ENTITY_ALIAS;
import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants.REVISION_PARAMETER;
@@ -51,8 +65,9 @@
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class ValidityAuditStrategy implements AuditStrategy {
+ private static final Logger log = Logger.getLogger( ValidityAuditStrategy.class );
- /** getter for the revision entity field annotated with @RevisionTimestamp */
+ /** getter for the revision entity field annotated with @RevisionTimestamp */
private Getter revisionTimestampGetter = null;
private final SessionCacheCleaner sessionCacheCleaner;
@@ -61,75 +76,152 @@ public ValidityAuditStrategy() {
sessionCacheCleaner = new SessionCacheCleaner();
}
- public void perform(Session session, String entityName, AuditConfiguration auditCfg, Serializable id, Object data,
- Object revision) {
+ public void perform(
+ final Session session,
+ String entityName,
+ final AuditConfiguration auditCfg,
+ final Serializable id,
+ Object data,
+ final Object revision) {
+
final AuditEntitiesConfiguration audEntitiesCfg = auditCfg.getAuditEntCfg();
- final String auditedEntityName = audEntitiesCfg.getAuditEntityName(entityName);
- final Dialect dialect = ((SessionImplementor)session).getFactory().getDialect();
- final EntityConfiguration auditEntityCfg = auditCfg.getEntCfg().get(entityName);
- final IdMapper idMapper = auditEntityCfg.getIdMapper();
+ final String auditedEntityName = audEntitiesCfg.getAuditEntityName( entityName );
+ final String revisionInfoEntityName = auditCfg.getAuditEntCfg().getRevisionInfoEntityName();
+ final SessionImplementor sessionImplementor = (SessionImplementor) session;
+ final Dialect dialect = sessionImplementor.getFactory().getDialect();
// Update the end date of the previous row if this operation is expected to have a previous row
if (getRevisionType(auditCfg, data) != RevisionType.ADD) {
- if (shallSelectAndUpdate(dialect, auditEntityCfg)) {
- // Constructing a query:
- // select e from audited_ent e where e.end_rev is null and e.id = :id
- QueryBuilder qb = new QueryBuilder(auditedEntityName, MIDDLE_ENTITY_ALIAS);
- // e.id = :id
- idMapper.addIdEqualsToQuery(qb.getRootParameters(), id, auditCfg.getAuditEntCfg().getOriginalIdPropName(), true);
- // e.end_rev is null
- addEndRevisionNullRestriction(auditCfg, qb.getRootParameters());
-
- @SuppressWarnings({"unchecked"})
- List<Object> l = qb.toQuery(session).setLockOptions(LockOptions.UPGRADE).list();
-
- updateLastRevision(session, auditCfg, l, id, auditedEntityName, revision);
- } else {
- // Save the audit data
- session.save(auditedEntityName, data);
- sessionCacheCleaner.scheduleAuditDataRemoval(session, data);
-
- // Workaround for HHH-3298 and FooBarTest#supportsLockingNullableSideOfJoin(Dialect).
- // Constructing a statement:
- // update e from audit_ent e where e.end_rev is null and e.id = :id and e.rev <> :rev
- final UpdateBuilder ub = new UpdateBuilder(auditedEntityName, MIDDLE_ENTITY_ALIAS);
- final Number revisionNumber = auditCfg.getRevisionInfoNumberReader().getRevisionNumber(revision);
- ub.updateValue(auditCfg.getAuditEntCfg().getRevisionEndFieldName(), revision);
- if (auditCfg.getAuditEntCfg().isRevisionEndTimestampEnabled()) {
- Object revEndTimestampObj = revisionTimestampGetter.get(revision);
- Date revisionEndTimestamp = convertRevEndTimestampToDate(revEndTimestampObj);
- ub.updateValue(auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName(), revisionEndTimestamp);
- }
- // e.id = :id
- idMapper.addIdEqualsToQuery(ub.getRootParameters(), id, auditCfg.getAuditEntCfg().getOriginalIdPropName(), true);
- // e.end_rev is null
- addEndRevisionNullRestriction(auditCfg, ub.getRootParameters());
- // e.rev <> :rev
- ub.getRootParameters().addWhereWithParam(auditCfg.getAuditEntCfg().getRevisionNumberPath(), true, "<>", revisionNumber);
- if (ub.toQuery(session).executeUpdate() != 1) {
- throw new RuntimeException("Cannot update previous revision for entity " + auditedEntityName + " and id " + id);
- }
- return;
- }
- }
+ // Save the audit data
+ session.save(auditedEntityName, data);
+ sessionCacheCleaner.scheduleAuditDataRemoval(session, data);
+
+ final Queryable productionEntityQueryable = (Queryable) sessionImplementor.getFactory().getEntityPersister( entityName );
+ final Queryable auditedEntityQueryable = (Queryable) sessionImplementor.getFactory().getEntityPersister( auditedEntityName );
+ final Queryable revisionInfoEntityQueryable = (Queryable) sessionImplementor.getFactory().getEntityPersister( revisionInfoEntityName );
+
+ // first we need to flush the session in order to have the new audit data inserted
+ // todo: expose org.hibernate.internal.SessionImpl.autoFlushIfRequired via SessionImplementor
+ // for now, we duplicate some of that logic here
+ autoFlushIfRequired( sessionImplementor, auditedEntityQueryable, revisionInfoEntityQueryable );
+
+ final Type revisionInfoIdType = sessionImplementor.getFactory()
+ .getEntityPersister( revisionInfoEntityName )
+ .getIdentifierType();
+ final String revEndColumnName = auditedEntityQueryable.toColumns( auditCfg.getAuditEntCfg().getRevisionEndFieldName() )[0];
+
+ final boolean isRevisionEndTimestampEnabled = auditCfg.getAuditEntCfg().isRevisionEndTimestampEnabled();
+
+ // update audit_ent set REVEND = ? [, REVEND_TSTMP = ?] where (prod_ent_id) = ? and REV <> ? and REVEND is null
+ final Update update = new Update( dialect ).setTableName( auditedEntityQueryable.getTableName() );
+ // set REVEND = ?
+ update.addColumn( revEndColumnName );
+ // set [, REVEND_TSTMP = ?]
+ if ( isRevisionEndTimestampEnabled ) {
+ update.addColumn(
+ auditedEntityQueryable.toColumns(
+ auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName()
+ )[0]
+ );
+ }
+
+ // where (prod_ent_id) = ?
+ update.addPrimaryKeyColumns( productionEntityQueryable.getIdentifierColumnNames() );
+ // where REV <> ?
+ update.addWhereColumn(
+ auditedEntityQueryable.toColumns(
+ auditCfg.getAuditEntCfg().getRevisionNumberPath()
+ )[0],
+ "<> ?"
+ );
+ // where REVEND is null
+ update.addWhereColumn( revEndColumnName, " is null" );
+
+ // Now lets execute the sql...
+ final String updateSql = update.toStatementString();
+
+ int rowCount = session.doReturningWork(
+ new ReturningWork<Integer>() {
+ @Override
+ public Integer execute(Connection connection) throws SQLException {
+ PreparedStatement preparedStatement = connection.prepareStatement( updateSql );
+
+ try {
+ int index = 1;
+
+ // set REVEND = ?
+ final Number revisionNumber = auditCfg.getRevisionInfoNumberReader().getRevisionNumber( revision );
+ revisionInfoIdType.nullSafeSet( preparedStatement, revisionNumber, index, sessionImplementor );
+ index += revisionInfoIdType.getColumnSpan( sessionImplementor.getFactory() );
+
+ // set [, REVEND_TSTMP = ?]
+ if ( isRevisionEndTimestampEnabled ) {
+ final Object revEndTimestampObj = revisionTimestampGetter.get( revision );
+ final Date revisionEndTimestamp = convertRevEndTimestampToDate( revEndTimestampObj );
+ final Type revEndTsType = auditedEntityQueryable.getPropertyType(
+ auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName()
+ );
+ revEndTsType.nullSafeSet( preparedStatement, revisionEndTimestamp, index, sessionImplementor );
+ index += revEndTsType.getColumnSpan( sessionImplementor.getFactory() );
+ }
+
+ // where (prod_ent_id) = ?
+ final Type idType = productionEntityQueryable.getIdentifierType();
+ idType.nullSafeSet( preparedStatement, id, index, sessionImplementor );
+ index += idType.getColumnSpan( sessionImplementor.getFactory() );
+
+ // where REV <> ?
+ final Type revType = auditedEntityQueryable.getPropertyType(
+ auditCfg.getAuditEntCfg().getRevisionNumberPath()
+ );
+ revType.nullSafeSet( preparedStatement, revisionNumber, index, sessionImplementor );
+
+ // where REVEND is null
+ // nothing to bind....
+
+ return preparedStatement.executeUpdate();
+ }
+ finally {
+ try {
+ preparedStatement.close();
+ }
+ catch (SQLException e) {
+ log.debug( "Could not release prepared statement : " + e.getMessage() );
+ }
+ }
+ }
+ }
+ );
+
+ if ( rowCount != 1 ) {
+ throw new RuntimeException(
+ "Cannot update previous revision for entity " + auditedEntityName + " and id " + id
+ );
+ }
+ return;
+ }
// Save the audit data
session.save(auditedEntityName, data);
sessionCacheCleaner.scheduleAuditDataRemoval(session, data);
}
- protected boolean shallSelectAndUpdate(Dialect dialect, EntityConfiguration auditEntityCfg) {
- // Hibernate fails to execute multi-table bulk operations if dialect does not support "row value constructor" feature.
- // In case of inheritance, secondary and join table mappings SQL query looks like:
- // update ParentEntity_AUD set REVEND=? where (id, REV) IN (select id, REV from HT_ChildEntity_AUD)
- // because Hibernate utilizes temporary tables.
- // See: http://in.relation.to/Bloggers/MultitableBulkOperations, https://community.jboss.org/wiki/TemporaryTableUse.
- // TODO: This might be improved to return false only if Hibernate is supposed to produce query with row value
- // constructor and the actual dialect does not support required feature. However, Hibernate decides to use temporary
- // tables while translating HQL to SQL query (QueryTranslatorImpl#buildAppropriateStatementExecutor(HqlSqlWalker)),
- // and it is difficult to predict here.
- return !dialect.supportsRowValueConstructorSyntax();
- }
+ private void autoFlushIfRequired(
+ SessionImplementor sessionImplementor,
+ Queryable auditedEntityQueryable,
+ Queryable revisionInfoEntityQueryable) {
+ final Set<String> querySpaces = new HashSet<String>();
+ querySpaces.add( auditedEntityQueryable.getTableName() );
+ querySpaces.add( revisionInfoEntityQueryable.getTableName() );
+ final AutoFlushEvent event = new AutoFlushEvent( querySpaces, (EventSource) sessionImplementor );
+ final Iterable<AutoFlushEventListener> listeners = sessionImplementor.getFactory().getServiceRegistry()
+ .getService( EventListenerRegistry.class )
+ .getEventListenerGroup( EventType.AUTO_FLUSH )
+ .listeners();
+ for ( AutoFlushEventListener listener : listeners ) {
+ listener.onAutoFlush( event );
+ }
+ }
@SuppressWarnings({"unchecked"})
public void performCollectionChange(Session session, AuditConfiguration auditCfg,

0 comments on commit 835f445

Please sign in to comment.