HHH-7093 - ValidityAuditStrategy behaviour depending on row value constractor feature

This commit is contained in:
Lukasz Antoniak 2012-05-23 21:32:47 +02:00
parent 7ed87233a9
commit af554fe59b
5 changed files with 160 additions and 30 deletions

View File

@ -131,6 +131,10 @@ public class Oracle9iDialect extends Oracle8iDialect {
return true;
}
public boolean supportsRowValueConstructorSyntax() {
return true;
}
public boolean supportsTupleDistinctCounts() {
return false;
}

View File

@ -459,4 +459,7 @@ public class PostgreSQL81Dialect extends Dialect {
return " for share";
}
public boolean supportsRowValueConstructorSyntax() {
return true;
}
}

View File

@ -500,7 +500,7 @@ public final class AuditMetadataGenerator {
addJoins(pc, propertyMapper, auditingData, pc.getEntityName(), xmlMappingData, true);
// Storing the generated configuration
EntityConfiguration entityCfg = new EntityConfiguration(auditEntityName,pc.getClassName(), idMapper,
EntityConfiguration entityCfg = new EntityConfiguration(auditEntityName, pc.getClassName(), idMapper,
propertyMapper, parentEntityName);
entitiesConfigurations.put(pc.getEntityName(), entityCfg);
}

View File

@ -7,10 +7,13 @@ import java.util.Map;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.RevisionType;
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;
@ -18,6 +21,7 @@ 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.property.Getter;
import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants.MIDDLE_ENTITY_ALIAS;
@ -44,6 +48,7 @@ import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants
*
* @author Stephanie Pau
* @author Adam Warski (adam at warski dot org)
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class ValidityAuditStrategy implements AuditStrategy {
@ -58,28 +63,54 @@ public class ValidityAuditStrategy implements AuditStrategy {
public void perform(Session session, String entityName, AuditConfiguration auditCfg, Serializable id, Object data,
Object revision) {
AuditEntitiesConfiguration audEntCfg = auditCfg.getAuditEntCfg();
String auditedEntityName = audEntCfg.getAuditEntityName(entityName);
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();
// Update the end date of the previous row if this operation is expected to have a previous row
if (getRevisionType(auditCfg, data) != RevisionType.ADD) {
/*
Constructing a query:
select e from audited_ent e where e.end_rev is null and e.id = :id
*/
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());
QueryBuilder qb = new QueryBuilder(auditedEntityName, MIDDLE_ENTITY_ALIAS);
@SuppressWarnings({"unchecked"})
List<Object> l = qb.toQuery(session).setLockOptions(LockOptions.UPGRADE).list();
// e.id = :id
IdMapper idMapper = auditCfg.getEntCfg().get(entityName).getIdMapper();
idMapper.addIdEqualsToQuery(qb.getRootParameters(), id, auditCfg.getAuditEntCfg().getOriginalIdPropName(), true);
updateLastRevision(session, auditCfg, l, id, auditedEntityName, revision);
} else {
// Save the audit data
session.save(auditedEntityName, data);
sessionCacheCleaner.scheduleAuditDataRemoval(session, data);
addEndRevisionNullRestriction(auditCfg, qb);
@SuppressWarnings({"unchecked"})
List<Object> l = qb.toQuery(session).setLockOptions(LockOptions.UPGRADE).list();
updateLastRevision(session, auditCfg, l, id, auditedEntityName, revision);
// 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
@ -87,10 +118,22 @@ public class ValidityAuditStrategy implements AuditStrategy {
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();
}
@SuppressWarnings({"unchecked"})
public void performCollectionChange(Session session, AuditConfiguration auditCfg,
PersistentCollectionChangeData persistentCollectionChangeData, Object revision) {
final QueryBuilder qb = new QueryBuilder(persistentCollectionChangeData.getEntityName(), MIDDLE_ENTITY_ALIAS);
// Adding a parameter for each id component, except the rev number
@ -104,7 +147,7 @@ public class ValidityAuditStrategy implements AuditStrategy {
}
}
addEndRevisionNullRestriction(auditCfg, qb);
addEndRevisionNullRestriction(auditCfg, qb.getRootParameters());
final List<Object> l = qb.toQuery(session).setLockOptions(LockOptions.UPGRADE).list();
@ -120,9 +163,8 @@ public class ValidityAuditStrategy implements AuditStrategy {
sessionCacheCleaner.scheduleAuditDataRemoval(session, persistentCollectionChangeData.getData());
}
private void addEndRevisionNullRestriction(AuditConfiguration auditCfg, QueryBuilder qb) {
// e.end_rev is null
qb.getRootParameters().addWhere(auditCfg.getAuditEntCfg().getRevisionEndFieldName(), true, "is", "null", false);
private void addEndRevisionNullRestriction(AuditConfiguration auditCfg, Parameters rootParameters) {
rootParameters.addWhere(auditCfg.getAuditEntCfg().getRevisionEndFieldName(), true, "is", "null", false);
}
public void addEntityAtRevisionRestriction(GlobalConfiguration globalCfg, QueryBuilder rootQueryBuilder,
@ -173,16 +215,9 @@ public class ValidityAuditStrategy implements AuditStrategy {
if (auditCfg.getAuditEntCfg().isRevisionEndTimestampEnabled()) {
// Determine the value of the revision property annotated with @RevisionTimestamp
Date revisionEndTimestamp;
String revEndTimestampFieldName = auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName();
Object revEndTimestampObj = this.revisionTimestampGetter.get(revision);
// convert to a java.util.Date
if (revEndTimestampObj instanceof Date) {
revisionEndTimestamp = (Date) revEndTimestampObj;
} else {
revisionEndTimestamp = new Date((Long) revEndTimestampObj);
}
Date revisionEndTimestamp = convertRevEndTimestampToDate(revEndTimestampObj);
// Setting the end revision timestamp
((Map<String, Object>) previousData).put(revEndTimestampFieldName, revisionEndTimestamp);
@ -195,5 +230,13 @@ public class ValidityAuditStrategy implements AuditStrategy {
throw new RuntimeException("Cannot find previous revision for entity " + auditedEntityName + " and id " + id);
}
}
private Date convertRevEndTimestampToDate(Object revEndTimestampObj) {
// convert to a java.util.Date
if (revEndTimestampObj instanceof Date) {
return (Date) revEndTimestampObj;
}
return new Date((Long) revEndTimestampObj);
}
}

View File

@ -0,0 +1,80 @@
package org.hibernate.envers.tools.query;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.envers.tools.MutableInteger;
import org.hibernate.envers.tools.Pair;
import org.hibernate.envers.tools.StringTools;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class UpdateBuilder {
private final String entityName;
private final String alias;
private final MutableInteger paramCounter;
private final Parameters rootParameters;
private final Map<String, Object> updates;
public UpdateBuilder(String entityName, String alias) {
this(entityName, alias, new MutableInteger());
}
private UpdateBuilder(String entityName, String alias, MutableInteger paramCounter) {
this.entityName = entityName;
this.alias = alias;
this.paramCounter = paramCounter;
rootParameters = new Parameters(alias, "and", paramCounter);
updates = new HashMap<String, Object>();
}
public Parameters getRootParameters() {
return rootParameters;
}
public void updateValue(String propertyName, Object value) {
updates.put(propertyName, value);
}
public void build(StringBuilder sb, Map<String, Object> updateParamValues) {
sb.append("update ").append(entityName).append(" ").append(alias);
sb.append(" set ");
int i = 1;
for (String property : updates.keySet()) {
final String paramName = generateParameterName();
sb.append(alias).append(".").append(property).append(" = ").append(":").append(paramName);
updateParamValues.put(paramName, updates.get(property));
if (i < updates.size()) {
sb.append(", ");
}
++i;
}
if (!rootParameters.isEmpty()) {
sb.append(" where ");
rootParameters.build(sb, updateParamValues);
}
}
private String generateParameterName() {
return "_u" + paramCounter.getAndIncrease();
}
public Query toQuery(Session session) {
StringBuilder querySb = new StringBuilder();
Map<String, Object> queryParamValues = new HashMap<String, Object>();
build(querySb, queryParamValues);
Query query = session.createQuery(querySb.toString());
for (Map.Entry<String, Object> paramValue : queryParamValues.entrySet()) {
query.setParameter(paramValue.getKey(), paramValue.getValue());
}
return query;
}
}