diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/BasicMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/BasicMetadataGenerator.java index 2ce167ab9a..23ef510e52 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/BasicMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/BasicMetadataGenerator.java @@ -87,4 +87,21 @@ boolean addBasic(Element parent, PropertyAuditingData propertyAuditingData, return true; } + + @SuppressWarnings({"unchecked"}) + boolean addKeyManyToOne(Element parent, PropertyAuditingData propertyAuditingData, Value value, + SimpleMapperBuilder mapper) { + Type type = value.getType(); + + Element manyToOneElement = parent.addElement("key-many-to-one"); + manyToOneElement.addAttribute("name", propertyAuditingData.getName()); + manyToOneElement.addAttribute("class", type.getName()); + MetadataTools.addColumns(manyToOneElement, value.getColumnIterator()); + + // A null mapper means that we only want to add xml mappings + if (mapper != null) { + mapper.add(propertyAuditingData.getPropertyData()); + } + return true; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java index d1147e62aa..f1c22a6b39 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/metadata/IdMetadataGenerator.java @@ -41,6 +41,7 @@ import org.hibernate.mapping.Component; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; /** @@ -61,11 +62,17 @@ private boolean addIdProperties(Element parent, Iterator properties, S Property property = properties.next(); Type propertyType = property.getType(); if (!"_identifierMapper".equals(property.getName())) { - // Last but one parameter: ids are always insertable - boolean added = mainGenerator.getBasicMetadataGenerator().addBasic(parent, - getIdPersistentPropertyAuditingData(property), - property.getValue(), mapper, true, key); - + boolean added = false; + if (propertyType instanceof ManyToOneType) { + added = mainGenerator.getBasicMetadataGenerator().addKeyManyToOne(parent, + getIdPersistentPropertyAuditingData(property), + property.getValue(), mapper); + } else { + // Last but one parameter: ids are always insertable + added = mainGenerator.getBasicMetadataGenerator().addBasic(parent, + getIdPersistentPropertyAuditingData(property), + property.getValue(), mapper, true, key); + } if (!added) { // If the entity is audited, and a non-supported id component is used, throwing an exception. // If the entity is not audited, then we simply don't support this entity, even in diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/entities/EntityInstantiator.java b/hibernate-envers/src/main/java/org/hibernate/envers/entities/EntityInstantiator.java index 05b0dd7bb8..b4e07432f2 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/entities/EntityInstantiator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/entities/EntityInstantiator.java @@ -22,16 +22,22 @@ * Boston, MA 02110-1301 USA */ package org.hibernate.envers.entities; -import java.util.Collection; -import java.util.List; -import java.util.Map; import org.hibernate.envers.configuration.AuditConfiguration; import org.hibernate.envers.entities.mapper.id.IdMapper; +import org.hibernate.envers.entities.mapper.id.MultipleIdMapper; +import org.hibernate.envers.entities.mapper.relation.lazy.ToOneDelegateSessionImplementor; import org.hibernate.envers.exception.AuditException; import org.hibernate.envers.reader.AuditReaderImplementor; import org.hibernate.envers.tools.reflection.ReflectionTools; import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.LazyInitializer; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * @author Adam Warski (adam at warski dot org) @@ -70,6 +76,11 @@ public Object createInstanceFromVersionsEntity(String entityName, Map versionsEn IdMapper idMapper = verCfg.getEntCfg().get(entityName).getIdMapper(); Map originalId = (Map) versionsEntity.get(verCfg.getAuditEntCfg().getOriginalIdPropName()); + // Fixes HHH-4751 issue (@IdClass with @ManyToOne relation mapping inside) + // Note that identifiers are always audited + // Replace identifier proxies if do not point to audit tables + replaceNonAuditIdProxies(originalId, revision); + Object primaryKey = idMapper.mapToIdFromMap(originalId); // Checking if the entity is in cache @@ -106,6 +117,25 @@ public Object createInstanceFromVersionsEntity(String entityName, Map versionsEn return ret; } + @SuppressWarnings({"unchecked"}) + private void replaceNonAuditIdProxies(Map originalId, Number revision) { + for (Object key : originalId.keySet()) { + Object value = originalId.get(key); + if (value instanceof HibernateProxy) { + HibernateProxy hibernateProxy = (HibernateProxy) value; + LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer(); + final String entityName = initializer.getEntityName(); + final Serializable entityId = initializer.getIdentifier(); + if (verCfg.getEntCfg().isVersioned(entityName)) { + final String entityClassName = verCfg.getEntCfg().get(entityName).getEntityClassName(); + final ToOneDelegateSessionImplementor delegate = new ToOneDelegateSessionImplementor(versionsReader, ReflectionTools.loadClass(entityClassName), entityId, revision, verCfg); + originalId.put(key, + versionsReader.getSessionImplementor().getFactory().getEntityPersister(entityName).createProxy(entityId, delegate)); + } + } + } + } + @SuppressWarnings({"unchecked"}) public void addInstancesFromVersionsEntities(String entityName, Collection addTo, List versionsEntities, Number revision) { for (Map versionsEntity : versionsEntities) { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/entities/mapper/relation/AbstractOneToOneMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/entities/mapper/relation/AbstractOneToOneMapper.java index 2a96959f09..b1440b7305 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/entities/mapper/relation/AbstractOneToOneMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/entities/mapper/relation/AbstractOneToOneMapper.java @@ -53,8 +53,6 @@ public void nullSafeMapToEntityFromMap(AuditConfiguration verCfg, Object obj, Ma protected abstract Object queryForReferencedEntity(AuditReaderImplementor versionsReader, EntityInfo referencedEntity, Serializable primaryKey, Number revision); - - @Override public void mapModifiedFlagsToMapFromEntity(SessionImplementor session, Map data, Object newObj, Object oldObj) { } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/ClassType.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/ClassType.java new file mode 100644 index 0000000000..b077b2d6e4 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/ClassType.java @@ -0,0 +1,68 @@ +package org.hibernate.envers.test.integration.ids.idclass; + +import java.io.Serializable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.envers.Audited; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +@Audited +@Entity +public class ClassType implements Serializable { + @Id + @Column(name = "Name") + private String type; + + private String description; + + public ClassType() { + } + + public ClassType(String type, String description) { + this.type = type; + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClassType)) return false; + + ClassType classType = (ClassType) o; + + if (type != null ? !type.equals(classType.type) : classType.type != null) return false; + + return true; + } + + @Override + public String toString() { + return "ClassType(type = " + type + ", description = " + description + ")"; + } + + @Override + public int hashCode() { + return type != null ? type.hashCode() : 0; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/IdClassWithRelationTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/IdClassWithRelationTest.java new file mode 100644 index 0000000000..7556b990e1 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/IdClassWithRelationTest.java @@ -0,0 +1,116 @@ +package org.hibernate.envers.test.integration.ids.idclass; + +import junit.framework.Assert; +import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase; +import org.hibernate.envers.test.Priority; +import org.hibernate.testing.TestForIssue; +import org.junit.Test; + +import javax.persistence.EntityManager; +import java.util.Arrays; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +@TestForIssue(jiraKey = "HHH-4751") +public class IdClassWithRelationTest extends BaseEnversJPAFunctionalTestCase { + private RelationalClassId entityId = null; + private String typeId = null; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{SampleClass.class, RelationalClassId.class, ClassType.class}; + } + + @Test + @Priority(10) + public void initData() { + EntityManager em = getEntityManager(); + + // Revision 1 + em.getTransaction().begin(); + ClassType type = new ClassType("type", "initial description"); + SampleClass entity = new SampleClass(); + entity.setType(type); + entity.setSampleValue("initial data"); + em.persist(type); + em.persist(entity); + em.getTransaction().commit(); + + typeId = type.getType(); + entityId = new RelationalClassId(entity.getId(), new ClassType("type", "initial description")); + + // Revision 2 + em.getTransaction().begin(); + type = em.find(ClassType.class, type.getType()); + type.setDescription("modified description"); + em.merge(type); + em.getTransaction().commit(); + + // Revision 3 + em.getTransaction().begin(); + entity = em.find(SampleClass.class, entityId); + entity.setSampleValue("modified data"); + em.merge(entity); + em.getTransaction().commit(); + + em.close(); + } + + @Test + public void testRevisionsCounts() { + Assert.assertEquals(Arrays.asList(1, 2), getAuditReader().getRevisions(ClassType.class, typeId)); + Assert.assertEquals(Arrays.asList(1, 3), getAuditReader().getRevisions(SampleClass.class, entityId)); + } + + @Test + public void testHistoryOfEntity() { + // given + SampleClass entity = new SampleClass(entityId.getId(), entityId.getType(), "initial data"); + + // when + SampleClass ver1 = getAuditReader().find(SampleClass.class, entityId, 1); + + // then + Assert.assertEquals(entity.getId(), ver1.getId()); + Assert.assertEquals(entity.getSampleValue(), ver1.getSampleValue()); + Assert.assertEquals(entity.getType().getType(), ver1.getType().getType()); + Assert.assertEquals(entity.getType().getDescription(), ver1.getType().getDescription()); + + // given + entity.setSampleValue("modified data"); + entity.getType().setDescription("modified description"); + + // when + SampleClass ver2 = getAuditReader().find(SampleClass.class, entityId, 3); + + // then + Assert.assertEquals(entity.getId(), ver2.getId()); + Assert.assertEquals(entity.getSampleValue(), ver2.getSampleValue()); + Assert.assertEquals(entity.getType().getType(), ver2.getType().getType()); + Assert.assertEquals(entity.getType().getDescription(), ver2.getType().getDescription()); + } + + @Test + public void testHistoryOfType() { + // given + ClassType type = new ClassType(typeId, "initial description"); + + // when + ClassType ver1 = getAuditReader().find(ClassType.class, typeId, 1); + + // then + Assert.assertEquals(type, ver1); + Assert.assertEquals(type.getDescription(), ver1.getDescription()); + + // given + type.setDescription("modified description"); + + // when + ClassType ver2 = getAuditReader().find(ClassType.class, typeId, 2); + + // then + Assert.assertEquals(type, ver2); + Assert.assertEquals(type.getDescription(), ver2.getDescription()); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/RelationalClassId.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/RelationalClassId.java new file mode 100644 index 0000000000..a158b9b177 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/RelationalClassId.java @@ -0,0 +1,60 @@ +package org.hibernate.envers.test.integration.ids.idclass; + +import java.io.Serializable; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class RelationalClassId implements Serializable { + private Long id; + private ClassType type; + + public RelationalClassId() { + } + + public RelationalClassId(Long id, ClassType type) { + this.id = id; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RelationalClassId)) return false; + + RelationalClassId that = (RelationalClassId) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (type != null ? !type.equals(that.type) : that.type != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "RelationalClassId(id = " + id + ", type = " + type + ")"; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ClassType getType() { + return type; + } + + public void setType(ClassType type) { + this.type = type; + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/SampleClass.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/SampleClass.java new file mode 100644 index 0000000000..a5dd4df63a --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/ids/idclass/SampleClass.java @@ -0,0 +1,96 @@ +package org.hibernate.envers.test.integration.ids.idclass; + +import java.io.Serializable; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import org.hibernate.envers.Audited; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +@Audited +@Entity +@IdClass(RelationalClassId.class) +public class SampleClass implements Serializable { + @Id + @GeneratedValue + private Long id; + + @Id + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "ClassTypeName", referencedColumnName = "Name", + insertable = true, updatable = true, nullable = false) + private ClassType type; + + private String sampleValue; + + public SampleClass() { + } + + public SampleClass(ClassType type) { + this.type = type; + } + + public SampleClass(Long id, ClassType type) { + this.id = id; + this.type = type; + } + + public SampleClass(Long id, ClassType type, String sampleValue) { + this.id = id; + this.type = type; + this.sampleValue = sampleValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SampleClass)) return false; + + SampleClass sampleClass = (SampleClass) o; + + if (id != null ? !id.equals(sampleClass.id) : sampleClass.id != null) return false; + if (type != null ? !type.equals(sampleClass.type) : sampleClass.type != null) return false; + if (sampleValue != null ? !sampleValue.equals(sampleClass.sampleValue) : sampleClass.sampleValue != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (sampleValue != null ? sampleValue.hashCode() : 0); + return result; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ClassType getType() { + return type; + } + + public void setType(ClassType type) { + this.type = type; + } + + public String getSampleValue() { + return sampleValue; + } + + public void setSampleValue(String sampleValue) { + this.sampleValue = sampleValue; + } +}