diff --git a/envers/pom.xml b/envers/pom.xml index a3b996d788..1cf776af19 100644 --- a/envers/pom.xml +++ b/envers/pom.xml @@ -95,7 +95,7 @@ org.testng testng - 5.8 + 5.10 jdk15 test diff --git a/envers/src/main/java/org/hibernate/envers/AuditMappedBy.java b/envers/src/main/java/org/hibernate/envers/AuditMappedBy.java new file mode 100644 index 0000000000..109e6f82e1 --- /dev/null +++ b/envers/src/main/java/org/hibernate/envers/AuditMappedBy.java @@ -0,0 +1,37 @@ +package org.hibernate.envers; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + *

+ * Annotation to specify a "fake" bi-directional relation. Such a relation uses {@code @OneToMany} + + * {@code @JoinColumn} on the one side, and {@code @ManyToOne} + {@code @Column(insertable=false, updatable=false)} on + * the many side. Then, Envers won't use a join table to audit this relation, but will store changes as in a normal + * bi-directional relation. + *

+ * + *

+ * This annotation is experimental and may change in future releases. + *

+ * + * @author Adam Warski (adam at warski dot org) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface AuditMappedBy { + /** + * @return Name of the property in the related entity which maps back to this entity. The property should be + * mapped with {@code @ManyToOne} and {@code @Column(insertable=false, updatable=false)}. + */ + String mappedBy(); + + /** + * @return Name of the property in the related entity which maps to the position column. Should be specified only + * for indexed collection, when @{@link org.hibernate.annotations.IndexColumn} is used on the collection. + * The property should be mapped with {@code @Column(insertable=false, updatable=false)}. + */ + String positionMappedBy() default ""; +} diff --git a/envers/src/main/java/org/hibernate/envers/configuration/ClassesAuditingData.java b/envers/src/main/java/org/hibernate/envers/configuration/ClassesAuditingData.java new file mode 100644 index 0000000000..696b24c7fc --- /dev/null +++ b/envers/src/main/java/org/hibernate/envers/configuration/ClassesAuditingData.java @@ -0,0 +1,43 @@ +package org.hibernate.envers.configuration; + +import org.hibernate.envers.configuration.metadata.reader.ClassAuditingData; +import org.hibernate.mapping.PersistentClass; + +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.util.LinkedHashMap; + +/** + * A helper class holding auditing meta-data for all persistent classes. + * @author Adam Warski (adam at warski dot org) + */ +public class ClassesAuditingData { + private final Map entityNameToAuditingData = new HashMap(); + private final Map persistentClassToAuditingData = new LinkedHashMap(); + + /** + * Stores information about auditing meta-data for the given class. + * @param pc Persistent class. + * @param cad Auditing meta-data for the given class. + */ + public void addClassAuditingData(PersistentClass pc, ClassAuditingData cad) { + entityNameToAuditingData.put(pc.getEntityName(), cad); + persistentClassToAuditingData.put(pc, cad); + } + + /** + * @return A collection of all auditing meta-data for persistent classes. + */ + public Collection> getAllClassAuditedData() { + return persistentClassToAuditingData.entrySet(); + } + + /** + * @param entityName Name of the entity. + * @return Auditing meta-data for the given entity. + */ + public ClassAuditingData getClassAuditingData(String entityName) { + return entityNameToAuditingData.get(entityName); + } +} diff --git a/envers/src/main/java/org/hibernate/envers/configuration/EntitiesConfigurator.java b/envers/src/main/java/org/hibernate/envers/configuration/EntitiesConfigurator.java index e72d136df4..82f0235f77 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/EntitiesConfigurator.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/EntitiesConfigurator.java @@ -60,30 +60,36 @@ public class EntitiesConfigurator { Document revisionInfoXmlMapping, Element revisionInfoRelationMapping) { // Creating a name register to capture all audit entity names created. AuditEntityNameRegister auditEntityNameRegister = new AuditEntityNameRegister(); - - AuditMetadataGenerator auditMetaGen = new AuditMetadataGenerator(cfg, globalCfg, verEntCfg, - revisionInfoRelationMapping, auditEntityNameRegister); DOMWriter writer = new DOMWriter(); // Sorting the persistent class topologically - superclass always before subclass Iterator classes = GraphTopologicalSort.sort(new PersistentClassGraphDefiner(cfg)).iterator(); - Map pcDatas = - new HashMap(); + ClassesAuditingData classesAuditingData = new ClassesAuditingData(); Map xmlMappings = new HashMap(); - // First pass + // Reading metadata from annotations while (classes.hasNext()) { PersistentClass pc = classes.next(); + // Collecting information from annotations on the persistent class pc AnnotationsMetadataReader annotationsMetadataReader = new AnnotationsMetadataReader(globalCfg, reflectionManager, pc); ClassAuditingData auditData = annotationsMetadataReader.getAuditData(); + classesAuditingData.addClassAuditingData(pc, auditData); + } + + AuditMetadataGenerator auditMetaGen = new AuditMetadataGenerator(cfg, globalCfg, verEntCfg, + revisionInfoRelationMapping, auditEntityNameRegister, classesAuditingData); + + // First pass + for (Map.Entry pcDatasEntry : classesAuditingData.getAllClassAuditedData()) { + PersistentClass pc = pcDatasEntry.getKey(); + ClassAuditingData auditData = pcDatasEntry.getValue(); + EntityXmlMappingData xmlMappingData = new EntityXmlMappingData(); if (auditData.isAudited()) { - pcDatas.put(pc, auditData); - if (!StringTools.isEmpty(auditData.getAuditTable().value())) { verEntCfg.addCustomAuditTableName(pc.getEntityName(), auditData.getAuditTable().value()); } @@ -97,31 +103,29 @@ public class EntitiesConfigurator { } // Second pass - for (Map.Entry pcDatasEntry : pcDatas.entrySet()) { + for (Map.Entry pcDatasEntry : classesAuditingData.getAllClassAuditedData()) { EntityXmlMappingData xmlMappingData = xmlMappings.get(pcDatasEntry.getKey()); - auditMetaGen.generateSecondPass(pcDatasEntry.getKey(), pcDatasEntry.getValue(), xmlMappingData); + if (pcDatasEntry.getValue().isAudited()) { + auditMetaGen.generateSecondPass(pcDatasEntry.getKey(), pcDatasEntry.getValue(), xmlMappingData); + try { + cfg.addDocument(writer.write(xmlMappingData.getMainXmlMapping())); + //writeDocument(xmlMappingData.getMainXmlMapping()); - try { - cfg.addDocument(writer.write(xmlMappingData.getMainXmlMapping())); - // TODO - //writeDocument(xmlMappingData.getMainXmlMapping()); - - for (Document additionalMapping : xmlMappingData.getAdditionalXmlMappings()) { - cfg.addDocument(writer.write(additionalMapping)); - // TODO - //writeDocument(additionalMapping); + for (Document additionalMapping : xmlMappingData.getAdditionalXmlMappings()) { + cfg.addDocument(writer.write(additionalMapping)); + //writeDocument(additionalMapping); + } + } catch (DocumentException e) { + throw new MappingException(e); } - } catch (DocumentException e) { - throw new MappingException(e); } } // Only if there are any versioned classes - if (pcDatas.size() > 0) { + if (classesAuditingData.getAllClassAuditedData().size() > 0) { try { if (revisionInfoXmlMapping != null) { - // TODO //writeDocument(revisionInfoXmlMapping); cfg.addDocument(writer.write(revisionInfoXmlMapping)); } @@ -134,7 +138,7 @@ public class EntitiesConfigurator { auditMetaGen.getNotAuditedEntitiesConfigurations()); } - // todo + @SuppressWarnings({"UnusedDeclaration"}) private void writeDocument(Document e) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); Writer w = new PrintWriter(baos); diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/AuditMetadataGenerator.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/AuditMetadataGenerator.java index 3bf59cec3b..16e74fc071 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/AuditMetadataGenerator.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/AuditMetadataGenerator.java @@ -30,6 +30,7 @@ import java.util.Map; import org.dom4j.Element; import org.hibernate.envers.configuration.GlobalConfiguration; import org.hibernate.envers.configuration.AuditEntitiesConfiguration; +import org.hibernate.envers.configuration.ClassesAuditingData; import org.hibernate.envers.configuration.metadata.reader.ClassAuditingData; import org.hibernate.envers.configuration.metadata.reader.PropertyAuditingData; import org.hibernate.envers.entities.EntityConfiguration; @@ -62,15 +63,22 @@ public final class AuditMetadataGenerator { private final AuditEntitiesConfiguration verEntCfg; private final Element revisionInfoRelationMapping; + /* + * Generators for different kinds of property values/types. + */ private final BasicMetadataGenerator basicMetadataGenerator; private final ComponentMetadataGenerator componentMetadataGenerator; private final IdMetadataGenerator idMetadataGenerator; private final ToOneRelationMetadataGenerator toOneRelationMetadataGenerator; + /* + * Here information about already generated mappings will be accumulated. + */ private final Map entitiesConfigurations; private final Map notAuditedEntitiesConfigurations; private final AuditEntityNameRegister auditEntityNameRegister; + private final ClassesAuditingData classesAuditingData; // Map entity name -> (join descriptor -> element describing the "versioned" join) private final Map> entitiesJoins; @@ -78,7 +86,8 @@ public final class AuditMetadataGenerator { public AuditMetadataGenerator(Configuration cfg, GlobalConfiguration globalCfg, AuditEntitiesConfiguration verEntCfg, Element revisionInfoRelationMapping, - AuditEntityNameRegister auditEntityNameRegister) { + AuditEntityNameRegister auditEntityNameRegister, + ClassesAuditingData classesAuditingData) { this.cfg = cfg; this.globalCfg = globalCfg; this.verEntCfg = verEntCfg; @@ -89,7 +98,8 @@ public final class AuditMetadataGenerator { this.idMetadataGenerator = new IdMetadataGenerator(this); this.toOneRelationMetadataGenerator = new ToOneRelationMetadataGenerator(this); - this.auditEntityNameRegister = auditEntityNameRegister; + this.auditEntityNameRegister = auditEntityNameRegister; + this.classesAuditingData = classesAuditingData; entitiesConfigurations = new HashMap(); notAuditedEntitiesConfigurations = new HashMap(); @@ -448,6 +458,10 @@ public final class AuditMetadataGenerator { return entitiesConfigurations; } + public ClassesAuditingData getClassesAuditingData() { + return classesAuditingData; + } + // Getters for generators and configuration BasicMetadataGenerator getBasicMetadataGenerator() { diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/CollectionMetadataGenerator.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/CollectionMetadataGenerator.java index dd440b1e60..01e8bcce21 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/CollectionMetadataGenerator.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/CollectionMetadataGenerator.java @@ -40,13 +40,11 @@ import org.hibernate.envers.RelationTargetAuditMode; import org.hibernate.envers.configuration.metadata.reader.PropertyAuditingData; import org.hibernate.envers.entities.EntityConfiguration; import org.hibernate.envers.entities.IdMappingData; +import org.hibernate.envers.entities.PropertyData; import org.hibernate.envers.entities.mapper.CompositeMapperBuilder; -import org.hibernate.envers.entities.mapper.relation.BasicCollectionMapper; -import org.hibernate.envers.entities.mapper.relation.CommonCollectionMapperData; -import org.hibernate.envers.entities.mapper.relation.ListCollectionMapper; -import org.hibernate.envers.entities.mapper.relation.MapCollectionMapper; -import org.hibernate.envers.entities.mapper.relation.MiddleComponentData; -import org.hibernate.envers.entities.mapper.relation.MiddleIdData; +import org.hibernate.envers.entities.mapper.PropertyMapper; +import org.hibernate.envers.entities.mapper.id.IdMapper; +import org.hibernate.envers.entities.mapper.relation.*; import org.hibernate.envers.entities.mapper.relation.component.MiddleDummyComponentMapper; import org.hibernate.envers.entities.mapper.relation.component.MiddleMapKeyIdComponentMapper; import org.hibernate.envers.entities.mapper.relation.component.MiddleMapKeyPropertyComponentMapper; @@ -61,6 +59,7 @@ import org.hibernate.envers.entities.mapper.relation.query.OneAuditEntityQueryGe import org.hibernate.envers.entities.mapper.relation.query.RelationQueryGenerator; import org.hibernate.envers.tools.StringTools; import org.hibernate.envers.tools.Tools; +import org.hibernate.envers.tools.MappingTools; import org.hibernate.MappingException; import org.hibernate.mapping.Collection; @@ -79,12 +78,16 @@ import org.hibernate.type.SetType; import org.hibernate.type.SortedMapType; import org.hibernate.type.SortedSetType; import org.hibernate.type.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Generates metadata for a collection-valued property. * @author Adam Warski (adam at warski dot org) */ public final class CollectionMetadataGenerator { + private static final Logger log = LoggerFactory.getLogger(CollectionMetadataGenerator.class); + private final AuditMetadataGenerator mainGenerator; private final String propertyName; private final Collection propertyValue; @@ -145,10 +148,13 @@ public final class CollectionMetadataGenerator { void addCollection() { Type type = propertyValue.getType(); - if ((type instanceof BagType || type instanceof SetType || type instanceof MapType || type instanceof ListType) && - (propertyValue.getElement() instanceof OneToMany) && (propertyValue.isInverse())) { + boolean oneToManyAttachedType = type instanceof BagType || type instanceof SetType || type instanceof MapType || type instanceof ListType; + boolean inverseOneToMany = (propertyValue.getElement() instanceof OneToMany) && (propertyValue.isInverse()); + boolean fakeOneToManyBidirectional = (propertyValue.getElement() instanceof OneToMany) && (propertyAuditingData.getAuditMappedBy() != null); + + if (oneToManyAttachedType && (inverseOneToMany || fakeOneToManyBidirectional)) { // A one-to-many relation mapped using @ManyToOne and @OneToMany(mappedBy="...") - addOneToManyAttached(); + addOneToManyAttached(fakeOneToManyBidirectional); } else { // All other kinds of relations require a middle (join) table. addWithMiddleTable(); @@ -161,7 +167,10 @@ public final class CollectionMetadataGenerator { } @SuppressWarnings({"unchecked"}) - private void addOneToManyAttached() { + private void addOneToManyAttached(boolean fakeOneToManyBidirectional) { + log.debug("Adding audit mapping for property " + referencingEntityName + "." + propertyName + + ": one-to-many collection, using a join column on the referenced entity."); + String mappedBy = getMappedBy(propertyValue); IdMappingData referencedIdMapping = mainGenerator.getReferencedIdMappingData(referencingEntityName, @@ -200,9 +209,29 @@ public final class CollectionMetadataGenerator { // Checking the type of the collection and adding an appropriate mapper. addMapper(commonCollectionMapperData, elementComponentData, indexComponentData); + PropertyMapper fakeBidirectionalRelationMapper; + if (fakeOneToManyBidirectional) { + // In case of a fake many-to-one bidirectional relation, we have to generate a mapper which maps + // the mapped-by property name to the id of the related entity (which is the owner of the collection). + String auditMappedBy = propertyAuditingData.getAuditMappedBy(); + + // Creating a prefixed relation mapper. + IdMapper relMapper = referencingIdMapping.getIdMapper().prefixMappedProperties( + MappingTools.createToOneRelationPrefix(auditMappedBy)); + + fakeBidirectionalRelationMapper = new ToOneIdMapper( + relMapper, + // The mapper will only be used to map from entity to map, so no need to provide other details + // when constructing the PropertyData. + new PropertyData(auditMappedBy, null, null, null), + referencedEntityName); + } else { + fakeBidirectionalRelationMapper = null; + } + // Storing information about this relation. referencingEntityConfiguration.addToManyNotOwningRelation(propertyName, mappedBy, - referencedEntityName, referencingIdData.getPrefixedMapper()); + referencedEntityName, referencingIdData.getPrefixedMapper(), fakeBidirectionalRelationMapper); } /** @@ -237,6 +266,9 @@ public final class CollectionMetadataGenerator { @SuppressWarnings({"unchecked"}) private void addWithMiddleTable() { + log.debug("Adding audit mapping for property " + referencingEntityName + "." + propertyName + + ": collection with a join table."); + // Generating the name of the middle table String auditMiddleTableName; String auditMiddleEntityName; @@ -249,6 +281,8 @@ public final class CollectionMetadataGenerator { auditMiddleEntityName = mainGenerator.getVerEntCfg().getAuditEntityName(middleTableName); } + log.debug("Using join table name: " + auditMiddleTableName); + // Generating the XML mapping for the middle entity, only if the relation isn't inverse. // If the relation is inverse, will be later checked by comparing middleEntityXml with null. Element middleEntityXml; @@ -413,8 +447,8 @@ public final class CollectionMetadataGenerator { } else { // Last but one parameter: collection components are always insertable boolean mapped = mainGenerator.getBasicMetadataGenerator().addBasic(xmlMapping, - new PropertyAuditingData(prefix, "field", ModificationStore.FULL, RelationTargetAuditMode.AUDITED), value, null, - true, true); + new PropertyAuditingData(prefix, "field", ModificationStore.FULL, RelationTargetAuditMode.AUDITED, null, null), + value, null, true, true); if (mapped) { // Simple values are always stored in the first item of the array returned by the query generator. @@ -499,10 +533,34 @@ public final class CollectionMetadataGenerator { return middleEntityXmlId; } + private String getMappedByCommon(PersistentClass referencedClass) { + // If there's an @AuditMappedBy specified, returning it directly. + String auditMappedBy = propertyAuditingData.getAuditMappedBy(); + if (auditMappedBy != null) { + // Checking that the property exists. + try { + referencedClass.getProperty(auditMappedBy); + } catch (MappingException me) { + throw new MappingException("@AuditMappedBy points to a property that can be read: " + + referencedClass.getEntityName() + "." + auditMappedBy, me); + } + + return auditMappedBy; + } + + return null; + } + @SuppressWarnings({"unchecked"}) private String getMappedBy(Collection collectionValue) { - Iterator assocClassProps = - ((OneToMany) collectionValue.getElement()).getAssociatedClass().getPropertyIterator(); + PersistentClass referencedClass = ((OneToMany) collectionValue.getElement()).getAssociatedClass(); + + String mappedByCommon = getMappedByCommon(referencedClass); + if (mappedByCommon != null) { + return mappedByCommon; + } + + Iterator assocClassProps = referencedClass.getPropertyIterator(); while (assocClassProps.hasNext()) { Property property = assocClassProps.next(); @@ -519,6 +577,11 @@ public final class CollectionMetadataGenerator { @SuppressWarnings({"unchecked"}) private String getMappedBy(Table collectionTable, PersistentClass referencedClass) { + String mappedByCommon = getMappedByCommon(referencedClass); + if (mappedByCommon != null) { + return mappedByCommon; + } + Iterator properties = referencedClass.getPropertyIterator(); while (properties.hasNext()) { Property property = properties.next(); diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java index eeaf032315..b905a7e128 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java @@ -139,6 +139,6 @@ public final class IdMetadataGenerator { private PropertyAuditingData getIdPersistentPropertyAuditingData(Property property) { return new PropertyAuditingData(property.getName(), property.getPropertyAccessorName(), - ModificationStore.FULL, RelationTargetAuditMode.AUDITED); + ModificationStore.FULL, RelationTargetAuditMode.AUDITED, null, null); } } diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/ToOneRelationMetadataGenerator.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/ToOneRelationMetadataGenerator.java index 3144d060fb..43272072c0 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/ToOneRelationMetadataGenerator.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/ToOneRelationMetadataGenerator.java @@ -32,17 +32,23 @@ import org.hibernate.envers.entities.mapper.id.IdMapper; import org.hibernate.envers.entities.mapper.relation.OneToOneNotOwningMapper; import org.hibernate.envers.entities.mapper.relation.ToOneIdMapper; import org.hibernate.envers.configuration.metadata.reader.PropertyAuditingData; +import org.hibernate.envers.configuration.metadata.reader.ClassAuditingData; +import org.hibernate.envers.tools.MappingTools; import org.hibernate.MappingException; import org.hibernate.mapping.OneToOne; import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Generates metadata for to-one relations (reference-valued properties). * @author Adam Warski (adam at warski dot org) */ public final class ToOneRelationMetadataGenerator { + private static final Logger log = LoggerFactory.getLogger(ToOneRelationMetadataGenerator.class); + private final AuditMetadataGenerator mainGenerator; ToOneRelationMetadataGenerator(AuditMetadataGenerator auditMetadataGenerator) { @@ -57,7 +63,7 @@ public final class ToOneRelationMetadataGenerator { IdMappingData idMapping = mainGenerator.getReferencedIdMappingData(entityName, referencedEntityName, propertyAuditingData, true); - String lastPropertyPrefix = propertyAuditingData.getName() + "_"; + String lastPropertyPrefix = MappingTools.createToOneRelationPrefix(propertyAuditingData.getName()); // Generating the id mapper for the relation IdMapper relMapper = idMapping.getIdMapper().prefixMappedProperties(lastPropertyPrefix); @@ -66,6 +72,30 @@ public final class ToOneRelationMetadataGenerator { mainGenerator.getEntitiesConfigurations().get(entityName).addToOneRelation( propertyAuditingData.getName(), referencedEntityName, relMapper); + // If the property isn't insertable, checking if this is not a "fake" bidirectional many-to-one relationship, + // that is, when the one side owns the relation (and is a collection), and the many side is non insertable. + // When that's the case and the user specified to store this relation without a middle table (using + // @AuditMappedBy), we have to make the property insertable for the purposes of Envers. In case of changes to + // the entity that didn't involve the relation, it's value will then be stored properly. In case of changes + // to the entity that did involve the relation, it's the responsibility of the collection side to store the + // proper data. + if (!insertable) { + ClassAuditingData referencedAuditingData = mainGenerator.getClassesAuditingData().getClassAuditingData(referencedEntityName); + + // Looking through the properties of the referenced entity to find the right property. + for (String referencedPropertyName : referencedAuditingData.getPropertyNames()) { + String auditMappedBy = referencedAuditingData.getPropertyAuditingData(referencedPropertyName).getAuditMappedBy(); + if (propertyAuditingData.getName().equals(auditMappedBy)) { + log.debug("Non-insertable property " + entityName + "." + propertyAuditingData.getName() + + " will be made insertable because a matching @AuditMappedBy was found in the " + + referencedEntityName + " entity."); + + insertable = true; + break; + } + } + } + // Adding an element to the mapping corresponding to the references entity id's Element properties = (Element) idMapping.getXmlRelationMapping().clone(); properties.addAttribute("name", propertyAuditingData.getName()); @@ -97,7 +127,7 @@ public final class ToOneRelationMetadataGenerator { throw new MappingException("An audited relation to a non-audited entity " + entityName + "!"); } - String lastPropertyPrefix = owningReferencePropertyName + "_"; + String lastPropertyPrefix = MappingTools.createToOneRelationPrefix(owningReferencePropertyName); String referencedEntityName = propertyValue.getReferencedEntityName(); // Generating the id mapper for the relation diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/AuditedPropertiesReader.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/AuditedPropertiesReader.java index a2c5186e03..2f6cb515bb 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/AuditedPropertiesReader.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/AuditedPropertiesReader.java @@ -14,12 +14,7 @@ import javax.persistence.Version; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.annotations.common.reflection.ReflectionManager; -import org.hibernate.envers.AuditJoinTable; -import org.hibernate.envers.AuditOverride; -import org.hibernate.envers.AuditOverrides; -import org.hibernate.envers.Audited; -import org.hibernate.envers.ModificationStore; -import org.hibernate.envers.NotAudited; +import org.hibernate.envers.*; import org.hibernate.envers.configuration.GlobalConfiguration; import org.hibernate.envers.tools.MappingTools; import org.hibernate.mapping.Component; @@ -177,10 +172,21 @@ public class AuditedPropertiesReader { return false; // not audited due to AuditOverride annotation } addPropertyMapKey(property, propertyData); + setPropertyAuditMappedBy(property, propertyData); return true; } + private void setPropertyAuditMappedBy(XProperty property, PropertyAuditingData propertyData) { + AuditMappedBy auditMappedBy = property.getAnnotation(AuditMappedBy.class); + if (auditMappedBy != null) { + propertyData.setAuditMappedBy(auditMappedBy.mappedBy()); + if (!"".equals(auditMappedBy.positionMappedBy())) { + propertyData.setPositionMappedBy(auditMappedBy.positionMappedBy()); + } + } + } + private void addPropertyMapKey(XProperty property, PropertyAuditingData propertyData) { MapKey mapKey = property.getAnnotation(MapKey.class); if (mapKey != null) { @@ -254,7 +260,7 @@ public class AuditedPropertiesReader { public Class annotationType() { return this.getClass(); } }; - private class ComponentPropertiesSource implements PersistentPropertiesSource { + private class ComponentPropertiesSource implements PersistentPropertiesSource { private final XClass xclass; private final Component component; diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/ClassAuditingData.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/ClassAuditingData.java index 7233513aa7..43a34c0122 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/ClassAuditingData.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/ClassAuditingData.java @@ -57,6 +57,10 @@ public class ClassAuditingData implements AuditedPropertiesHolder { return properties.get(propertyName); } + public Iterable getPropertyNames() { + return properties.keySet(); + } + public Map getSecondaryTableDictionary() { return secondaryTableDictionary; } diff --git a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/PropertyAuditingData.java b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/PropertyAuditingData.java index 3537e698d8..2320b1bab7 100644 --- a/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/PropertyAuditingData.java +++ b/envers/src/main/java/org/hibernate/envers/configuration/metadata/reader/PropertyAuditingData.java @@ -46,17 +46,22 @@ public class PropertyAuditingData { private String accessType; private final List auditJoinTableOverrides = new ArrayList(0); private RelationTargetAuditMode relationTargetAuditMode; + private String auditMappedBy; + private String positionMappedBy; public PropertyAuditingData() { } public PropertyAuditingData(String name, String accessType, ModificationStore store, - RelationTargetAuditMode relationTargetAuditMode) { + RelationTargetAuditMode relationTargetAuditMode, + String auditMappedBy, String positionMappedBy) { this.name = name; this.beanName = name; this.accessType = accessType; this.store = store; this.relationTargetAuditMode = relationTargetAuditMode; + this.auditMappedBy = auditMappedBy; + this.positionMappedBy = positionMappedBy; } public String getName() { @@ -115,7 +120,23 @@ public class PropertyAuditingData { return auditJoinTableOverrides; } - public void addAuditingOverride(AuditOverride annotation) { + public String getAuditMappedBy() { + return auditMappedBy; + } + + public void setAuditMappedBy(String auditMappedBy) { + this.auditMappedBy = auditMappedBy; + } + + public String getPositionMappedBy() { + return positionMappedBy; + } + + public void setPositionMappedBy(String positionMappedBy) { + this.positionMappedBy = positionMappedBy; + } + + public void addAuditingOverride(AuditOverride annotation) { if (annotation != null) { String overrideName = annotation.name(); boolean present = false; diff --git a/envers/src/main/java/org/hibernate/envers/entities/EntityConfiguration.java b/envers/src/main/java/org/hibernate/envers/entities/EntityConfiguration.java index 196792dbc7..6279cc0778 100644 --- a/envers/src/main/java/org/hibernate/envers/entities/EntityConfiguration.java +++ b/envers/src/main/java/org/hibernate/envers/entities/EntityConfiguration.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.Map; import org.hibernate.envers.entities.mapper.ExtendedPropertyMapper; +import org.hibernate.envers.entities.mapper.PropertyMapper; import org.hibernate.envers.entities.mapper.id.IdMapper; /** @@ -52,29 +53,29 @@ public class EntityConfiguration { public void addToOneRelation(String fromPropertyName, String toEntityName, IdMapper idMapper) { relations.put(fromPropertyName, new RelationDescription(fromPropertyName, RelationType.TO_ONE, - toEntityName, null, idMapper)); + toEntityName, null, idMapper, null)); } public void addToOneNotOwningRelation(String fromPropertyName, String mappedByPropertyName, String toEntityName, IdMapper idMapper) { relations.put(fromPropertyName, new RelationDescription(fromPropertyName, RelationType.TO_ONE_NOT_OWNING, - toEntityName, mappedByPropertyName, idMapper)); + toEntityName, mappedByPropertyName, idMapper, null)); } public void addToManyNotOwningRelation(String fromPropertyName, String mappedByPropertyName, String toEntityName, - IdMapper idMapper) { + IdMapper idMapper, PropertyMapper fakeBidirectionalRelationMapper) { relations.put(fromPropertyName, new RelationDescription(fromPropertyName, RelationType.TO_MANY_NOT_OWNING, - toEntityName, mappedByPropertyName, idMapper)); + toEntityName, mappedByPropertyName, idMapper, fakeBidirectionalRelationMapper)); } public void addToManyMiddleRelation(String fromPropertyName, String toEntityName) { relations.put(fromPropertyName, new RelationDescription(fromPropertyName, RelationType.TO_MANY_MIDDLE, - toEntityName, null, null)); + toEntityName, null, null, null)); } public void addToManyMiddleNotOwningRelation(String fromPropertyName, String mappedByPropertyName, String toEntityName) { relations.put(fromPropertyName, new RelationDescription(fromPropertyName, RelationType.TO_MANY_MIDDLE_NOT_OWNING, - toEntityName, mappedByPropertyName, null)); + toEntityName, mappedByPropertyName, null, null)); } public boolean isRelation(String propertyName) { diff --git a/envers/src/main/java/org/hibernate/envers/entities/RelationDescription.java b/envers/src/main/java/org/hibernate/envers/entities/RelationDescription.java index fb42e670cc..6329b1ee9d 100644 --- a/envers/src/main/java/org/hibernate/envers/entities/RelationDescription.java +++ b/envers/src/main/java/org/hibernate/envers/entities/RelationDescription.java @@ -24,6 +24,7 @@ package org.hibernate.envers.entities; import org.hibernate.envers.entities.mapper.id.IdMapper; +import org.hibernate.envers.entities.mapper.PropertyMapper; /** * @author Adam Warski (adam at warski dot org) @@ -34,15 +35,18 @@ public class RelationDescription { private final String toEntityName; private final String mappedByPropertyName; private final IdMapper idMapper; + private final PropertyMapper fakeBidirectionalRelationMapper; private boolean bidirectional; public RelationDescription(String fromPropertyName, RelationType relationType, String toEntityName, - String mappedByPropertyName, IdMapper idMapper) { + String mappedByPropertyName, IdMapper idMapper, + PropertyMapper fakeBidirectionalRelationMapper) { this.fromPropertyName = fromPropertyName; this.relationType = relationType; this.toEntityName = toEntityName; this.mappedByPropertyName = mappedByPropertyName; this.idMapper = idMapper; + this.fakeBidirectionalRelationMapper = fakeBidirectionalRelationMapper; this.bidirectional = false; } @@ -67,6 +71,10 @@ public class RelationDescription { return idMapper; } + public PropertyMapper getFakeBidirectionalRelationMapper() { + return fakeBidirectionalRelationMapper; + } + public boolean isBidirectional() { return bidirectional; } diff --git a/envers/src/main/java/org/hibernate/envers/event/AuditEventListener.java b/envers/src/main/java/org/hibernate/envers/event/AuditEventListener.java index 0fc68afb20..670b66aa88 100644 --- a/envers/src/main/java/org/hibernate/envers/event/AuditEventListener.java +++ b/envers/src/main/java/org/hibernate/envers/event/AuditEventListener.java @@ -24,6 +24,7 @@ package org.hibernate.envers.event; import java.io.Serializable; +import java.util.List; import org.hibernate.envers.configuration.AuditConfiguration; import org.hibernate.envers.entities.RelationDescription; @@ -31,12 +32,9 @@ import org.hibernate.envers.entities.RelationType; import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData; import org.hibernate.envers.entities.mapper.id.IdMapper; import org.hibernate.envers.synchronization.AuditSync; -import org.hibernate.envers.synchronization.work.AddWorkUnit; -import org.hibernate.envers.synchronization.work.CollectionChangeWorkUnit; -import org.hibernate.envers.synchronization.work.DelWorkUnit; -import org.hibernate.envers.synchronization.work.ModWorkUnit; -import org.hibernate.envers.synchronization.work.PersistentCollectionChangeWorkUnit; +import org.hibernate.envers.synchronization.work.*; import org.hibernate.envers.tools.Tools; +import org.hibernate.envers.RevisionType; import org.hibernate.cfg.Configuration; import org.hibernate.collection.PersistentCollection; @@ -57,6 +55,7 @@ import org.hibernate.event.PreCollectionRemoveEventListener; import org.hibernate.event.PreCollectionUpdateEvent; import org.hibernate.event.PreCollectionUpdateEventListener; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.collection.AbstractCollectionPersister; import org.hibernate.proxy.HibernateProxy; /** @@ -182,7 +181,8 @@ public class AuditEventListener implements PostInsertEventListener, PostUpdateEv } private void generateBidirectionalCollectionChangeWorkUnits(AuditSync verSync, AbstractCollectionEvent event, - PersistentCollectionChangeWorkUnit workUnit) { + PersistentCollectionChangeWorkUnit workUnit, + RelationDescription rd) { // Checking if this is enabled in configuration ... if (!verCfg.getGlobalCfg().isGenerateRevisionsForCollections()) { return; @@ -190,12 +190,9 @@ public class AuditEventListener implements PostInsertEventListener, PostUpdateEv // Checking if this is not a bidirectional relation - then, a revision needs also be generated for // the other side of the relation. - RelationDescription relDesc = verCfg.getEntCfg().getRelationDescription(event.getAffectedOwnerEntityName(), - workUnit.getReferencingPropertyName()); - // relDesc can be null if this is a collection of simple values (not a relation). - if (relDesc != null && relDesc.isBidirectional()) { - String relatedEntityName = relDesc.getToEntityName(); + if (rd != null && rd.isBidirectional()) { + String relatedEntityName = rd.getToEntityName(); IdMapper relatedIdMapper = verCfg.getEntCfg().get(relatedEntityName).getIdMapper(); for (PersistentCollectionChangeData changeData : workUnit.getCollectionChanges()) { @@ -208,6 +205,38 @@ public class AuditEventListener implements PostInsertEventListener, PostUpdateEv } } + private void generateFakeBidirecationalRelationWorkUnits(AuditSync verSync, PersistentCollection newColl, Serializable oldColl, + String collectionEntityName, String referencingPropertyName, + AbstractCollectionEvent event, + RelationDescription rd) { + // First computing the relation changes + List collectionChanges = verCfg.getEntCfg().get(collectionEntityName).getPropertyMapper() + .mapCollectionChanges(referencingPropertyName, newColl, oldColl, event.getAffectedOwnerIdOrNull()); + + // Getting the id mapper for the related entity, as the work units generated will corrspond to the related + // entities. + String relatedEntityName = rd.getToEntityName(); + IdMapper relatedIdMapper = verCfg.getEntCfg().get(relatedEntityName).getIdMapper(); + + // For each collection change, generating the bidirectional work unit. + for (PersistentCollectionChangeData changeData : collectionChanges) { + Object relatedObj = changeData.getChangedElement(); + Serializable relatedId = (Serializable) relatedIdMapper.mapToIdFromEntity(relatedObj); + RevisionType revType = (RevisionType) changeData.getData().get(verCfg.getAuditEntCfg().getRevisionTypePropName()); + + // By default, the nested work unit is a collection change work unit. + AuditWorkUnit nestedWorkUnit = new CollectionChangeWorkUnit(event.getSession(), relatedEntityName, verCfg, + relatedId, relatedObj); + + verSync.addWorkUnit(new FakeBidirectionalRelationWorkUnit(event.getSession(), relatedEntityName, verCfg, + relatedId, event.getAffectedOwnerOrNull(), rd, revType, nestedWorkUnit)); + } + + // We also have to generate a collection change work unit for the owning entity. + verSync.addWorkUnit(new CollectionChangeWorkUnit(event.getSession(), collectionEntityName, verCfg, + event.getAffectedOwnerIdOrNull(), event.getAffectedOwnerOrNull())); + } + private void onCollectionAction(AbstractCollectionEvent event, PersistentCollection newColl, Serializable oldColl, CollectionEntry collectionEntry) { String entityName = event.getAffectedOwnerEntityName(); @@ -215,16 +244,28 @@ public class AuditEventListener implements PostInsertEventListener, PostUpdateEv if (verCfg.getEntCfg().isVersioned(entityName)) { AuditSync verSync = verCfg.getSyncManager().get(event.getSession()); - PersistentCollectionChangeWorkUnit workUnit = new PersistentCollectionChangeWorkUnit(event.getSession(), - entityName, verCfg, newColl, collectionEntry, oldColl, event.getAffectedOwnerIdOrNull()); - verSync.addWorkUnit(workUnit); + String ownerEntityName = ((AbstractCollectionPersister) collectionEntry.getLoadedPersister()).getOwnerEntityName(); + String referencingPropertyName = collectionEntry.getRole().substring(ownerEntityName.length() + 1); - if (workUnit.containsWork()) { - // There are some changes: a revision needs also be generated for the collection owner - verSync.addWorkUnit(new CollectionChangeWorkUnit(event.getSession(), event.getAffectedOwnerEntityName(), - verCfg, event.getAffectedOwnerIdOrNull(), event.getAffectedOwnerOrNull())); + // Checking if this is not a "fake" many-to-one bidirectional relation. The relation description may be + // null in case of collections of non-entities. + RelationDescription rd = verCfg.getEntCfg().get(entityName).getRelationDescription(referencingPropertyName); + if (rd != null && rd.getMappedByPropertyName() != null) { + generateFakeBidirecationalRelationWorkUnits(verSync, newColl, oldColl, entityName, + referencingPropertyName, event, rd); + } else { + PersistentCollectionChangeWorkUnit workUnit = new PersistentCollectionChangeWorkUnit(event.getSession(), + entityName, verCfg, newColl, collectionEntry, oldColl, event.getAffectedOwnerIdOrNull(), + referencingPropertyName); + verSync.addWorkUnit(workUnit); - generateBidirectionalCollectionChangeWorkUnits(verSync, event, workUnit); + if (workUnit.containsWork()) { + // There are some changes: a revision needs also be generated for the collection owner + verSync.addWorkUnit(new CollectionChangeWorkUnit(event.getSession(), event.getAffectedOwnerEntityName(), + verCfg, event.getAffectedOwnerIdOrNull(), event.getAffectedOwnerOrNull())); + + generateBidirectionalCollectionChangeWorkUnits(verSync, event, workUnit, rd); + } } } } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/AbstractAuditWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/AbstractAuditWorkUnit.java index cee758d3d6..baf5d62f4a 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/AbstractAuditWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/AbstractAuditWorkUnit.java @@ -64,6 +64,14 @@ public abstract class AbstractAuditWorkUnit implements AuditWorkUnit { data.put(entitiesCfg.getOriginalIdPropName(), originalId); } + public void perform(Session session, Object revisionData) { + Map data = generateData(revisionData); + + session.save(verCfg.getAuditEntCfg().getAuditEntityName(getEntityName()), data); + + setPerformed(data); + } + public Object getEntityId() { return id; } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/AddWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/AddWorkUnit.java index 450d2383de..e4bc6dab34 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/AddWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/AddWorkUnit.java @@ -30,7 +30,6 @@ import java.util.Map; import org.hibernate.envers.RevisionType; import org.hibernate.envers.configuration.AuditConfiguration; -import org.hibernate.Session; import org.hibernate.engine.SessionImplementor; import org.hibernate.persister.entity.EntityPersister; @@ -60,12 +59,9 @@ public class AddWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return true; } - public void perform(Session session, Object revisionData) { + public Map generateData(Object revisionData) { fillDataWithId(data, revisionData, RevisionType.ADD); - - session.save(verCfg.getAuditEntCfg().getAuditEntityName(getEntityName()), data); - - setPerformed(data); + return data; } public AuditWorkUnit merge(AddWorkUnit second) { @@ -84,6 +80,10 @@ public class AddWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return this; } + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + return FakeBidirectionalRelationWorkUnit.merge(second, this, second.getNestedWorkUnit()); + } + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { return first.merge(this); } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/AuditWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/AuditWorkUnit.java index 52090c5acb..367640ba5b 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/AuditWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/AuditWorkUnit.java @@ -25,7 +25,10 @@ package org.hibernate.envers.synchronization.work; import org.hibernate.Session; +import java.util.Map; + /** + * TODO: refactor constructors into factory methods * @author Adam Warski (adam at warski dot org) */ public interface AuditWorkUnit extends WorkUnitMergeVisitor, WorkUnitMergeDispatcher { @@ -35,7 +38,20 @@ public interface AuditWorkUnit extends WorkUnitMergeVisitor, WorkUnitMergeDispat boolean containsWork(); boolean isPerformed(); - + + /** + * Perform this work unit in the given session. + * @param session Session, in which the work unit should be performed. + * @param revisionData The current revision data, which will be used to populate the work unit with the correct + * revision relation. + */ void perform(Session session, Object revisionData); void undo(Session session); + + /** + * @param revisionData The current revision data, which will be used to populate the work unit with the correct + * revision relation. + * @return Generates data that should be saved when performing this work unit. + */ + Map generateData(Object revisionData); } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/CollectionChangeWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/CollectionChangeWorkUnit.java index 04870d3a62..86ae0a0673 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/CollectionChangeWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/CollectionChangeWorkUnit.java @@ -30,7 +30,6 @@ import java.util.Map; import org.hibernate.envers.RevisionType; import org.hibernate.envers.configuration.AuditConfiguration; -import org.hibernate.Session; import org.hibernate.engine.SessionImplementor; /** @@ -50,16 +49,14 @@ public class CollectionChangeWorkUnit extends AbstractAuditWorkUnit implements A return true; } - public void perform(Session session, Object revisionData) { + public Map generateData(Object revisionData) { Map data = new HashMap(); fillDataWithId(data, revisionData, RevisionType.MOD); verCfg.getEntCfg().get(getEntityName()).getPropertyMapper().mapToMapFromEntity(sessionImplementor, data, entity, null); - session.save(verCfg.getAuditEntCfg().getAuditEntityName(getEntityName()), data); - - setPerformed(data); + return data; } public AuditWorkUnit merge(AddWorkUnit second) { @@ -78,6 +75,10 @@ public class CollectionChangeWorkUnit extends AbstractAuditWorkUnit implements A return this; } + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + return second; + } + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { return first.merge(this); } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/DelWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/DelWorkUnit.java index e5a1fcda7e..b5b89fff61 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/DelWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/DelWorkUnit.java @@ -30,7 +30,6 @@ import java.util.Map; import org.hibernate.envers.RevisionType; import org.hibernate.envers.configuration.AuditConfiguration; -import org.hibernate.Session; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.engine.SessionImplementor; @@ -53,7 +52,7 @@ public class DelWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return true; } - public void perform(Session session, Object revisionData) { + public Map generateData(Object revisionData) { Map data = new HashMap(); fillDataWithId(data, revisionData, RevisionType.DEL); @@ -62,9 +61,7 @@ public class DelWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit propertyNames, state, state); } - session.save(verCfg.getAuditEntCfg().getAuditEntityName(getEntityName()), data); - - setPerformed(data); + return data; } public AuditWorkUnit merge(AddWorkUnit second) { @@ -83,6 +80,10 @@ public class DelWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return this; } + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + return this; + } + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { return first.merge(this); } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/FakeBidirectionalRelationWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/FakeBidirectionalRelationWorkUnit.java new file mode 100644 index 0000000000..9714c7c979 --- /dev/null +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/FakeBidirectionalRelationWorkUnit.java @@ -0,0 +1,119 @@ +package org.hibernate.envers.synchronization.work; + +import org.hibernate.engine.SessionImplementor; +import org.hibernate.envers.configuration.AuditConfiguration; +import org.hibernate.envers.entities.RelationDescription; +import org.hibernate.envers.RevisionType; + +import java.io.Serializable; +import java.util.Map; +import java.util.HashMap; + +/** + * A work unit that handles "fake" bidirectional one-to-many relations (mapped with {@code @OneToMany+@JoinColumn} and + * {@code @ManyToOne+@Column(insertable=false, updatable=false)}. + * @author Adam Warski (adam at warski dot org) + */ +public class FakeBidirectionalRelationWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit { + private final Object owningEntity; + private final RelationDescription rd; + private final RevisionType revisionType; + + /* + * The work unit responsible for generating the "raw" entity data to be saved. + */ + private final AuditWorkUnit nestedWorkUnit; + + + public FakeBidirectionalRelationWorkUnit(SessionImplementor sessionImplementor, String entityName, + AuditConfiguration verCfg, Serializable id, Object owningEntity, + RelationDescription rd, RevisionType revisionType, + AuditWorkUnit nestedWorkUnit) { + super(sessionImplementor, entityName, verCfg, id); + + + this.owningEntity = owningEntity; + this.rd = rd; + this.revisionType = revisionType; + this.nestedWorkUnit = nestedWorkUnit; + } + + public FakeBidirectionalRelationWorkUnit(FakeBidirectionalRelationWorkUnit original, AuditWorkUnit nestedWorkUnit) { + super(original.sessionImplementor, original.entityName, original.verCfg, original.id); + + this.owningEntity = original.owningEntity; + this.rd = original.rd; + this.revisionType = original.revisionType; + this.nestedWorkUnit = nestedWorkUnit; + } + + public AuditWorkUnit getNestedWorkUnit() { + return nestedWorkUnit; + } + + public RevisionType getRevisionType() { + return revisionType; + } + + public boolean containsWork() { + return true; + } + + public Map generateData(Object revisionData) { + // Generating data with the nested work unit. This data contains all data except the fake relation. + // Making a defensive copy not to modify the data held by the nested work unit. + Map nestedData = new HashMap(nestedWorkUnit.generateData(revisionData)); + + // Now adding data for the fake relation. + // If the revision type is "DEL", it means that the object is removed from the collection. Then the + // new owner will in fact be null. + rd.getFakeBidirectionalRelationMapper().mapToMapFromEntity(sessionImplementor, nestedData, + revisionType == RevisionType.DEL ? null : owningEntity, null); + + return nestedData; + } + + public AuditWorkUnit merge(AddWorkUnit second) { + return merge(this, nestedWorkUnit, second); + } + + public AuditWorkUnit merge(ModWorkUnit second) { + return merge(this, nestedWorkUnit, second); + } + + public AuditWorkUnit merge(DelWorkUnit second) { + return second; + } + + public AuditWorkUnit merge(CollectionChangeWorkUnit second) { + return this; + } + + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + /* + * The merging rules are the following (revision types of the first and second work units): + * - DEL, DEL - return any (the work units are the same) + * - DEL, ADD - return ADD (points to new owner) + * - ADD, DEL - return ADD (points to new owner) + * - ADD, ADD - return second (points to newer owner) + */ + + if (revisionType == RevisionType.DEL || second.getRevisionType() == RevisionType.ADD) { + return second; + } + + return this; + } + + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { + return first.merge(this); + } + + public static AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit frwu, AuditWorkUnit nestedFirst, + AuditWorkUnit nestedSecond) { + AuditWorkUnit nestedMerged = nestedSecond.dispatch(nestedFirst); + + // Creating a new fake relation work unit with the nested merged data + return new FakeBidirectionalRelationWorkUnit(frwu, nestedMerged); + } +} diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/ModWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/ModWorkUnit.java index cdfde7dac2..5d4e2f0c07 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/ModWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/ModWorkUnit.java @@ -30,7 +30,6 @@ import java.util.Map; import org.hibernate.envers.RevisionType; import org.hibernate.envers.configuration.AuditConfiguration; -import org.hibernate.Session; import org.hibernate.engine.SessionImplementor; import org.hibernate.persister.entity.EntityPersister; @@ -54,12 +53,10 @@ public class ModWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return changes; } - public void perform(Session session, Object revisionData) { + public Map generateData(Object revisionData) { fillDataWithId(data, revisionData, RevisionType.MOD); - session.save(verCfg.getAuditEntCfg().getAuditEntityName(getEntityName()), data); - - setPerformed(data); + return data; } public Map getData() { @@ -82,6 +79,10 @@ public class ModWorkUnit extends AbstractAuditWorkUnit implements AuditWorkUnit return this; } + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + return FakeBidirectionalRelationWorkUnit.merge(second, this, second.getNestedWorkUnit()); + } + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { return first.merge(this); } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/PersistentCollectionChangeWorkUnit.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/PersistentCollectionChangeWorkUnit.java index 53245ba785..8da1abdf99 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/PersistentCollectionChangeWorkUnit.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/PersistentCollectionChangeWorkUnit.java @@ -34,7 +34,6 @@ import org.hibernate.envers.configuration.AuditEntitiesConfiguration; import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData; import org.hibernate.Session; -import org.hibernate.persister.collection.AbstractCollectionPersister; import org.hibernate.engine.CollectionEntry; import org.hibernate.engine.SessionImplementor; import org.hibernate.collection.PersistentCollection; @@ -48,11 +47,11 @@ public class PersistentCollectionChangeWorkUnit extends AbstractAuditWorkUnit im public PersistentCollectionChangeWorkUnit(SessionImplementor sessionImplementor, String entityName, AuditConfiguration auditCfg, PersistentCollection collection, - CollectionEntry collectionEntry, Serializable snapshot, Serializable id) { + CollectionEntry collectionEntry, Serializable snapshot, Serializable id, + String referencingPropertyName) { super(sessionImplementor, entityName, auditCfg, new PersistentCollectionChangeWorkUnitId(id, collectionEntry.getRole())); - String ownerEntityName = ((AbstractCollectionPersister) collectionEntry.getLoadedPersister()).getOwnerEntityName(); - referencingPropertyName = collectionEntry.getRole().substring(ownerEntityName.length() + 1); + this.referencingPropertyName = referencingPropertyName; collectionChanges = auditCfg.getEntCfg().get(getEntityName()).getPropertyMapper() .mapCollectionChanges(referencingPropertyName, collection, snapshot, id); @@ -72,6 +71,10 @@ public class PersistentCollectionChangeWorkUnit extends AbstractAuditWorkUnit im return collectionChanges != null && collectionChanges.size() != 0; } + public Map generateData(Object revisionData) { + throw new UnsupportedOperationException("Cannot generate data for a collection change work unit!"); + } + @SuppressWarnings({"unchecked"}) public void perform(Session session, Object revisionData) { AuditEntitiesConfiguration entitiesCfg = verCfg.getAuditEntCfg(); @@ -109,6 +112,10 @@ public class PersistentCollectionChangeWorkUnit extends AbstractAuditWorkUnit im return null; } + public AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second) { + return null; + } + public AuditWorkUnit dispatch(WorkUnitMergeVisitor first) { if (first instanceof PersistentCollectionChangeWorkUnit) { PersistentCollectionChangeWorkUnit original = (PersistentCollectionChangeWorkUnit) first; diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/work/WorkUnitMergeVisitor.java b/envers/src/main/java/org/hibernate/envers/synchronization/work/WorkUnitMergeVisitor.java index 4f5d958dd7..84afe3dff5 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/work/WorkUnitMergeVisitor.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/work/WorkUnitMergeVisitor.java @@ -32,4 +32,5 @@ public interface WorkUnitMergeVisitor { AuditWorkUnit merge(ModWorkUnit second); AuditWorkUnit merge(DelWorkUnit second); AuditWorkUnit merge(CollectionChangeWorkUnit second); + AuditWorkUnit merge(FakeBidirectionalRelationWorkUnit second); } diff --git a/envers/src/main/java/org/hibernate/envers/tools/MappingTools.java b/envers/src/main/java/org/hibernate/envers/tools/MappingTools.java index ada4462bd2..28482e3811 100644 --- a/envers/src/main/java/org/hibernate/envers/tools/MappingTools.java +++ b/envers/src/main/java/org/hibernate/envers/tools/MappingTools.java @@ -12,4 +12,12 @@ public class MappingTools { public static String createComponentPrefix(String componentName) { return componentName + "_"; } + + /** + * @param referencePropertyName The name of the property that holds the relation to the entity. + * @return A prefix which should be used to prefix an id mapper for the related entity. + */ + public static String createToOneRelationPrefix(String referencePropertyName) { + return referencePropertyName + "_"; + } } diff --git a/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefEdEntity.java b/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefEdEntity.java new file mode 100644 index 0000000000..c3b627664e --- /dev/null +++ b/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefEdEntity.java @@ -0,0 +1,85 @@ +package org.hibernate.envers.test.entities.onetomany.detached; + +import org.hibernate.envers.Audited; + +import javax.persistence.*; + +/** + * Entity for {@link org.hibernate.envers.test.integration.onetomany.detached.JoinColumnBidirectionalList} test. + * Owned side of the relation. + * @author Adam Warski (adam at warski dot org) + */ +@Entity +@Audited +public class ListJoinColumnBidirectionalRefEdEntity { + @Id + @GeneratedValue + private Integer id; + + private String data; + + @ManyToOne + @JoinColumn(name = "some_join_column", insertable = false, updatable = false) + private ListJoinColumnBidirectionalRefIngEntity owner; + + public ListJoinColumnBidirectionalRefEdEntity() { } + + public ListJoinColumnBidirectionalRefEdEntity(Integer id, String data, ListJoinColumnBidirectionalRefIngEntity owner) { + this.id = id; + this.data = data; + this.owner = owner; + } + + public ListJoinColumnBidirectionalRefEdEntity(String data, ListJoinColumnBidirectionalRefIngEntity owner) { + this.data = data; + this.owner = owner; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public ListJoinColumnBidirectionalRefIngEntity getOwner() { + return owner; + } + + public void setOwner(ListJoinColumnBidirectionalRefIngEntity owner) { + this.owner = owner; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ListJoinColumnBidirectionalRefEdEntity)) return false; + + ListJoinColumnBidirectionalRefEdEntity that = (ListJoinColumnBidirectionalRefEdEntity) o; + + if (data != null ? !data.equals(that.data) : that.data != null) return false; + //noinspection RedundantIfStatement + if (id != null ? !id.equals(that.id) : that.id != null) return false; + + return true; + } + + public int hashCode() { + int result; + result = (id != null ? id.hashCode() : 0); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } + + public String toString() { + return "ListJoinColumnBidirectionalRefEdEntity(id = " + id + ", data = " + data + ")"; + } +} diff --git a/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefIngEntity.java b/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefIngEntity.java new file mode 100644 index 0000000000..56353a51b0 --- /dev/null +++ b/envers/src/test/java/org/hibernate/envers/test/entities/onetomany/detached/ListJoinColumnBidirectionalRefIngEntity.java @@ -0,0 +1,90 @@ +package org.hibernate.envers.test.entities.onetomany.detached; + +import org.hibernate.envers.Audited; +import org.hibernate.envers.AuditMappedBy; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; + +/** + * Entity for {@link org.hibernate.envers.test.integration.onetomany.detached.JoinColumnBidirectionalList} test. + * Owning side of the relation. + * @author Adam Warski (adam at warski dot org) + */ +@Entity +@Audited +public class ListJoinColumnBidirectionalRefIngEntity { + @Id + @GeneratedValue + private Integer id; + + private String data; + + @OneToMany + @JoinColumn(name = "some_join_column") + @AuditMappedBy(mappedBy = "owner") + private List references; + + public ListJoinColumnBidirectionalRefIngEntity() { } + + public ListJoinColumnBidirectionalRefIngEntity(Integer id, String data, ListJoinColumnBidirectionalRefEdEntity... references) { + this.id = id; + this.data = data; + this.references = new ArrayList(); + this.references.addAll(Arrays.asList(references)); + } + + public ListJoinColumnBidirectionalRefIngEntity(String data, ListJoinColumnBidirectionalRefEdEntity... references) { + this(null, data, references); + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public List getReferences() { + return references; + } + + public void setReferences(List references) { + this.references = references; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ListJoinColumnBidirectionalRefIngEntity)) return false; + + ListJoinColumnBidirectionalRefIngEntity that = (ListJoinColumnBidirectionalRefIngEntity) o; + + if (data != null ? !data.equals(that.data) : that.data != null) return false; + //noinspection RedundantIfStatement + if (id != null ? !id.equals(that.id) : that.id != null) return false; + + return true; + } + + public int hashCode() { + int result; + result = (id != null ? id.hashCode() : 0); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } + + public String toString() { + return "ListJoinColumnBidirectionalRefIngEntity(id = " + id + ", data = " + data + ")"; + } +} diff --git a/envers/src/test/java/org/hibernate/envers/test/integration/onetomany/detached/JoinColumnBidirectionalList.java b/envers/src/test/java/org/hibernate/envers/test/integration/onetomany/detached/JoinColumnBidirectionalList.java new file mode 100644 index 0000000000..a631b674cb --- /dev/null +++ b/envers/src/test/java/org/hibernate/envers/test/integration/onetomany/detached/JoinColumnBidirectionalList.java @@ -0,0 +1,204 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Middleware LLC. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.envers.test.integration.onetomany.detached; + +import org.hibernate.ejb.Ejb3Configuration; +import org.hibernate.envers.test.AbstractEntityTest; +import org.hibernate.envers.test.entities.onetomany.detached.ListJoinColumnBidirectionalRefEdEntity; +import org.hibernate.envers.test.entities.onetomany.detached.ListJoinColumnBidirectionalRefIngEntity; +import static org.hibernate.envers.test.tools.TestTools.checkList; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import org.testng.annotations.Test; + +import javax.persistence.EntityManager; +import java.util.Arrays; + +/** + * Test for a "fake" bidirectional mapping where one side uses @OneToMany+@JoinColumn (and thus owns the relatin), + * and the other uses a @ManyToOne(insertable=false, updatable=false). + * @author Adam Warski (adam at warski dot org) + */ +public class JoinColumnBidirectionalList extends AbstractEntityTest { + private Integer ed1_id; + private Integer ed2_id; + + private Integer ing1_id; + private Integer ing2_id; + + public void configure(Ejb3Configuration cfg) { + cfg.addAnnotatedClass(ListJoinColumnBidirectionalRefIngEntity.class); + cfg.addAnnotatedClass(ListJoinColumnBidirectionalRefEdEntity.class); + } + + @Test(enabled = true) + public void createData() { + EntityManager em = getEntityManager(); + + ListJoinColumnBidirectionalRefEdEntity ed1 = new ListJoinColumnBidirectionalRefEdEntity("ed1", null); + ListJoinColumnBidirectionalRefEdEntity ed2 = new ListJoinColumnBidirectionalRefEdEntity("ed2", null); + + ListJoinColumnBidirectionalRefIngEntity ing1 = new ListJoinColumnBidirectionalRefIngEntity("coll1", ed1); + ListJoinColumnBidirectionalRefIngEntity ing2 = new ListJoinColumnBidirectionalRefIngEntity("coll1", ed2); + + // Revision 1 (ing1: ed1, ing2: ed2) + em.getTransaction().begin(); + + em.persist(ed1); + em.persist(ed2); + em.persist(ing1); + em.persist(ing2); + + em.getTransaction().commit(); + + // Revision 2 (ing1: ed1, ed2) + em.getTransaction().begin(); + + ing1 = em.find(ListJoinColumnBidirectionalRefIngEntity.class, ing1.getId()); + ing2 = em.find(ListJoinColumnBidirectionalRefIngEntity.class, ing2.getId()); + ed1 = em.find(ListJoinColumnBidirectionalRefEdEntity.class, ed1.getId()); + ed2 = em.find(ListJoinColumnBidirectionalRefEdEntity.class, ed2.getId()); + + ing2.getReferences().remove(ed2); + ing1.getReferences().add(ed2); + + em.getTransaction().commit(); + em.clear(); + + // Revision 3 (ing1: ed1, ed2) + em.getTransaction().begin(); + + ed1 = em.find(ListJoinColumnBidirectionalRefEdEntity.class, ed1.getId()); + + ed1.setData("ed1 bis"); + + em.getTransaction().commit(); + em.clear(); + + // Revision 4 (ing2: ed1, ed2) + em.getTransaction().begin(); + + ing1 = em.find(ListJoinColumnBidirectionalRefIngEntity.class, ing1.getId()); + ing2 = em.find(ListJoinColumnBidirectionalRefIngEntity.class, ing2.getId()); + + ing1.getReferences().clear(); + ing2.getReferences().add(ed1); + ing2.getReferences().add(ed2); + + em.getTransaction().commit(); + em.clear(); + + // + + ing1_id = ing1.getId(); + ing2_id = ing2.getId(); + + ed1_id = ed1.getId(); + ed2_id = ed2.getId(); + } + + @Test(enabled = true, dependsOnMethods = "createData") + public void testRevisionsCounts() { + assertEquals(Arrays.asList(1, 2, 4), getAuditReader().getRevisions(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id)); + assertEquals(Arrays.asList(1, 2, 4), getAuditReader().getRevisions(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id)); + + assertEquals(Arrays.asList(1, 3, 4), getAuditReader().getRevisions(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id)); + assertEquals(Arrays.asList(1, 2, 4), getAuditReader().getRevisions(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id)); + } + + @Test(enabled = true, dependsOnMethods = "createData") + public void testHistoryOfIng1() { + ListJoinColumnBidirectionalRefEdEntity ed1_fromRev1 = new ListJoinColumnBidirectionalRefEdEntity(ed1_id, "ed1", null); + ListJoinColumnBidirectionalRefEdEntity ed1_fromRev3 = new ListJoinColumnBidirectionalRefEdEntity(ed1_id, "ed1 bis", null); + ListJoinColumnBidirectionalRefEdEntity ed2 = getEntityManager().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id); + + ListJoinColumnBidirectionalRefIngEntity rev1 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id, 1); + ListJoinColumnBidirectionalRefIngEntity rev2 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id, 2); + ListJoinColumnBidirectionalRefIngEntity rev3 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id, 3); + ListJoinColumnBidirectionalRefIngEntity rev4 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id, 4); + + assertTrue(checkList(rev1.getReferences(), ed1_fromRev1)); + assertTrue(checkList(rev2.getReferences(), ed1_fromRev1, ed2)); + assertTrue(checkList(rev3.getReferences(), ed1_fromRev3, ed2)); + assertTrue(checkList(rev4.getReferences())); + } + + @Test(enabled = true, dependsOnMethods = "createData") + public void testHistoryOfIng2() { + ListJoinColumnBidirectionalRefEdEntity ed1 = getEntityManager().find(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id); + ListJoinColumnBidirectionalRefEdEntity ed2 = getEntityManager().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id); + + ListJoinColumnBidirectionalRefIngEntity rev1 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id, 1); + ListJoinColumnBidirectionalRefIngEntity rev2 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id, 2); + ListJoinColumnBidirectionalRefIngEntity rev3 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id, 3); + ListJoinColumnBidirectionalRefIngEntity rev4 = getAuditReader().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id, 4); + + assertTrue(checkList(rev1.getReferences(), ed2)); + assertTrue(checkList(rev2.getReferences())); + assertTrue(checkList(rev3.getReferences())); + assertTrue(checkList(rev4.getReferences(), ed1, ed2)); + } + + @Test(enabled = true, dependsOnMethods = "createData") + public void testHistoryOfEd1() { + ListJoinColumnBidirectionalRefIngEntity ing1 = getEntityManager().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id); + ListJoinColumnBidirectionalRefIngEntity ing2 = getEntityManager().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id); + + ListJoinColumnBidirectionalRefEdEntity rev1 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id, 1); + ListJoinColumnBidirectionalRefEdEntity rev2 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id, 2); + ListJoinColumnBidirectionalRefEdEntity rev3 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id, 3); + ListJoinColumnBidirectionalRefEdEntity rev4 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed1_id, 4); + + assertTrue(rev1.getOwner().equals(ing1)); + assertTrue(rev2.getOwner().equals(ing1)); + assertTrue(rev3.getOwner().equals(ing1)); + assertTrue(rev4.getOwner().equals(ing2)); + + assertEquals(rev1.getData(), "ed1"); + assertEquals(rev2.getData(), "ed1"); + assertEquals(rev3.getData(), "ed1 bis"); + assertEquals(rev4.getData(), "ed1 bis"); + } + + @Test(enabled = true, dependsOnMethods = "createData") + public void testHistoryOfEd2() { + ListJoinColumnBidirectionalRefIngEntity ing1 = getEntityManager().find(ListJoinColumnBidirectionalRefIngEntity.class, ing1_id); + ListJoinColumnBidirectionalRefIngEntity ing2 = getEntityManager().find(ListJoinColumnBidirectionalRefIngEntity.class, ing2_id); + + ListJoinColumnBidirectionalRefEdEntity rev1 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id, 1); + ListJoinColumnBidirectionalRefEdEntity rev2 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id, 2); + ListJoinColumnBidirectionalRefEdEntity rev3 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id, 3); + ListJoinColumnBidirectionalRefEdEntity rev4 = getAuditReader().find(ListJoinColumnBidirectionalRefEdEntity.class, ed2_id, 4); + + assertTrue(rev1.getOwner().equals(ing2)); + assertTrue(rev2.getOwner().equals(ing1)); + assertTrue(rev3.getOwner().equals(ing1)); + assertTrue(rev4.getOwner().equals(ing2)); + + assertEquals(rev1.getData(), "ed2"); + assertEquals(rev2.getData(), "ed2"); + assertEquals(rev3.getData(), "ed2"); + assertEquals(rev4.getData(), "ed2"); + } +}