From 92bd6f89dd65e513d4667d0eb8a3c2fa45b37b9e Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Mon, 13 Jan 2020 18:28:53 -0500 Subject: [PATCH] HHH-13760 Fix ClassCastException when Envers inserts audit rows that use lazy many-to-one mappings --- .../metadata/CollectionMetadataGenerator.java | 4 +- .../ToOneRelationMetadataGenerator.java | 5 +- .../mapper/relation/ToOneIdMapper.java | 22 +++- .../integration/manytoone/lazy/Address.java | 55 +++++++++ .../manytoone/lazy/AddressVersion.java | 61 ++++++++++ .../test/integration/manytoone/lazy/Base.java | 19 ++++ .../manytoone/lazy/BaseDomainEntity.java | 56 +++++++++ .../lazy/BaseDomainEntityMetadata.java | 55 +++++++++ .../lazy/BaseDomainEntityVersion.java | 52 +++++++++ .../lazy/ManyToOneLazyFetchTest.java | 78 +++++++++++++ .../integration/manytoone/lazy/Shipment.java | 107 ++++++++++++++++++ 11 files changed, 510 insertions(+), 4 deletions(-) create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Address.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/AddressVersion.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Base.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntity.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityMetadata.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityVersion.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/ManyToOneLazyFetchTest.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Shipment.java diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/CollectionMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/CollectionMetadataGenerator.java index b239396a86..1adb53b3d4 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/CollectionMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/CollectionMetadataGenerator.java @@ -277,7 +277,9 @@ public final class CollectionMetadataGenerator { // 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 ), - referencingEntityName, false + referencingEntityName, + false, + false ); final String positionMappedBy; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java index c3bd7ff157..7a9d154ed0 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java @@ -28,6 +28,7 @@ import org.dom4j.Element; * * @author Adam Warski (adam at warski dot org) * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + * @author Chris Cranford */ public final class ToOneRelationMetadataGenerator { private final AuditMetadataGenerator mainGenerator; @@ -99,11 +100,13 @@ public final class ToOneRelationMetadataGenerator { parent.add( element ); } + boolean lazy = ( (ToOne) value ).isLazy(); + // Adding mapper for the id final PropertyData propertyData = propertyAuditingData.getPropertyData(); mapper.addComposite( propertyData, - new ToOneIdMapper( relMapper, propertyData, referencedEntityName, nonInsertableFake ) + new ToOneIdMapper( relMapper, propertyData, referencedEntityName, nonInsertableFake, lazy ) ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java index 7c4115db23..2d8753375c 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java @@ -19,26 +19,31 @@ import org.hibernate.envers.internal.reader.AuditReaderImplementor; import org.hibernate.envers.internal.tools.EntityTools; import org.hibernate.envers.internal.tools.query.Parameters; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.HibernateProxy; /** * @author Adam Warski (adam at warski dot org) * @author HernпїЅn Chanfreau * @author Michal Skowronek (mskowr at o2 dot pl) + * @author Chris Cranford */ public class ToOneIdMapper extends AbstractToOneMapper { private final IdMapper delegate; private final String referencedEntityName; private final boolean nonInsertableFake; + private final boolean lazyMapping; public ToOneIdMapper( IdMapper delegate, PropertyData propertyData, String referencedEntityName, - boolean nonInsertableFake) { + boolean nonInsertableFake, + boolean lazyMapping) { super( delegate.getServiceRegistry(), propertyData ); this.delegate = delegate; this.referencedEntityName = referencedEntityName; this.nonInsertableFake = nonInsertableFake; + this.lazyMapping = lazyMapping; } @Override @@ -49,10 +54,23 @@ public class ToOneIdMapper extends AbstractToOneMapper { Object oldObj) { final HashMap newData = new HashMap<>(); + Object oldObject = oldObj; + Object newObject = newObj; + if ( lazyMapping ) { + if ( nonInsertableFake && oldObject instanceof HibernateProxy ) { + oldObject = ( (HibernateProxy) oldObject ).getHibernateLazyInitializer().getImplementation(); + } + + + if ( !nonInsertableFake && newObject instanceof HibernateProxy ) { + newObject = ( (HibernateProxy) newObject ).getHibernateLazyInitializer().getImplementation(); + } + } + // If this property is originally non-insertable, but made insertable because it is in a many-to-one "fake" // bi-directional relation, we always store the "old", unchaged data, to prevent storing changes made // to this field. It is the responsibility of the collection to properly update it if it really changed. - delegate.mapToMapFromEntity( newData, nonInsertableFake ? oldObj : newObj ); + delegate.mapToMapFromEntity( newData, nonInsertableFake ? oldObject : newObject ); for ( Map.Entry entry : newData.entrySet() ) { data.put( entry.getKey(), entry.getValue() ); diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Address.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Address.java new file mode 100644 index 0000000000..e2823fe0dd --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Address.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.time.Instant; +import java.util.Collection; +import java.util.LinkedList; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +/** + * @author Chris Cranford + */ +@Entity +@Table(name = "address") +public class Address extends BaseDomainEntity { + private static final long serialVersionUID = 7380477602657080463L; + + @Column(name = "name") + private String name; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "id", cascade = CascadeType.ALL) + Collection versions = new LinkedList<>(); + + Address() { + } + + Address(Instant when, String who, String name) { + super( when, who ); + this.name = name; + } + + public AddressVersion addInitialVersion(String description) { + AddressVersion version = new AddressVersion( getCreatedAt(), getCreatedBy(), this, 0, description ); + versions.add( version ); + return version; + } + + public String getName() { + return name; + } + + public Collection getVersions() { + return versions; + } +} \ No newline at end of file diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/AddressVersion.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/AddressVersion.java new file mode 100644 index 0000000000..09fcb5e13b --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/AddressVersion.java @@ -0,0 +1,61 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.time.Instant; +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * @author Chris Cranford + */ +@Entity +@Table(name = "address_version") +public class AddressVersion extends BaseDomainEntityVersion { + private static final long serialVersionUID = 1100389518057335117L; + + @Id + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "id", referencedColumnName = "id", updatable = false, nullable = false) + private Address id; + + @Column(name = "description", updatable = false) + private String description; + + AddressVersion() { + } + + AddressVersion(Instant when, String who, Address id, long version, String description) { + setCreatedAt( when ); + setCreatedBy( who ); + setVersion( version ); + this.id = Objects.requireNonNull(id ); + this.description = description; + } + + @Override + public Address getId() { + return id; + } + + public String getDescription() { + return description; + } + + public AddressVersion update(Instant when, String who, String description) { + AddressVersion version = new AddressVersion( when, who, id, getVersion() + 1, description ); + id.versions.add( version ); + return version; + } +} \ No newline at end of file diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Base.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Base.java new file mode 100644 index 0000000000..a6d03f4ef8 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Base.java @@ -0,0 +1,19 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.MappedSuperclass; + +/** + * @author Chris Cranford + */ +@MappedSuperclass +@Access(AccessType.FIELD) +public class Base { +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntity.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntity.java new file mode 100644 index 0000000000..1d4534836d --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntity.java @@ -0,0 +1,56 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.time.Instant; +import java.util.Objects; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +/** + * @author Chris Cranford + */ +@MappedSuperclass +public abstract class BaseDomainEntity extends BaseDomainEntityMetadata { + private static final long serialVersionUID = 1023010094948580516L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected long id = 0; + + BaseDomainEntity() { + + } + + BaseDomainEntity(Instant timestamp, String who) { + super( timestamp, who ); + } + + public long getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseDomainEntity that = (BaseDomainEntity) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityMetadata.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityMetadata.java new file mode 100644 index 0000000000..254d1007d8 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityMetadata.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.io.Serializable; +import java.time.Instant; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +import org.hibernate.annotations.CreationTimestamp; + +/** + * @author Chris Cranford + */ +@MappedSuperclass +public abstract class BaseDomainEntityMetadata extends Base implements Serializable { + private static final long serialVersionUID = 2765056578095518489L; + + @Column(name = "created_by", nullable = false, updatable = false) + private String createdBy; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + BaseDomainEntityMetadata() { + + } + + BaseDomainEntityMetadata(Instant timestamp, String who) { + this.createdBy = who; + this.createdAt = timestamp; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} \ No newline at end of file diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityVersion.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityVersion.java new file mode 100644 index 0000000000..d87ee30a2a --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/BaseDomainEntityVersion.java @@ -0,0 +1,52 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +/** + * @author Chris Cranford + */ +@MappedSuperclass +public abstract class BaseDomainEntityVersion extends BaseDomainEntityMetadata { + private static final long serialVersionUID = 1564895954324242368L; + + @Id + @Column(name = "version", nullable = false, updatable = false) + private long version; + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public abstract Object getId(); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseDomainEntityVersion that = (BaseDomainEntityVersion) o; + return Objects.equals(getId(), that.getId()) && version == that.version; + } + + @Override + public int hashCode() { + return Objects.hash(getId(), version); + } +} \ No newline at end of file diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/ManyToOneLazyFetchTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/ManyToOneLazyFetchTest.java new file mode 100644 index 0000000000..5191cc1803 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/ManyToOneLazyFetchTest.java @@ -0,0 +1,78 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; + +import org.hibernate.Hibernate; +import org.hibernate.envers.test.BaseEnversFunctionalTestCase; +import org.hibernate.envers.test.Priority; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * Tests that proxies are resolved correctly by the ToOneIdMapper such that when the values + * are inserted for the join columns, they're resolved correclty avoiding ClassCastException + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-13760") +public class ManyToOneLazyFetchTest extends BaseEnversFunctionalTestCase { + private Long shipmentId; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Shipment.class, Address.class, AddressVersion.class }; + } + + @Test + @Priority(10) + public void initData() { + this.shipmentId = doInHibernate( this::sessionFactory, session -> { + final Shipment shipment = new Shipment( Instant.now(), "system", Instant.now().plus( Duration.ofDays( 3 ) ), "abcd123", null, null ); + session.persist( shipment ); + session.flush(); + + final Address origin = new Address( Instant.now(), "system", "Valencia#1" ); + final Address destination = new Address( Instant.now(), "system", "Madrid#3" ); + final AddressVersion originVersion0 = origin.addInitialVersion( "Poligono Manises" ); + final AddressVersion destinationVersion0 = destination.addInitialVersion( "Poligono Alcobendas" ); + session.persist( origin ); + session.persist( destination ); + session.flush(); + + shipment.setOrigin( originVersion0 ); + shipment.setDestination( destinationVersion0 ); + session.merge( shipment ); + session.flush(); + + return shipment.getId(); + } ); + + doInHibernate( this::sessionFactory, session -> { + final Shipment shipment = session.get( Shipment.class, shipmentId ); + + Hibernate.initialize( shipment.getOrigin() ); + Hibernate.initialize( shipment.getDestination() ); + shipment.setClosed( true ); + + session.merge( shipment ); + session.flush(); + } ); + } + + @Test + public void testRevisionHistory() { + assertEquals( Arrays.asList( 1, 2 ), getAuditReader().getRevisions( Shipment.class, shipmentId ) ); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Shipment.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Shipment.java new file mode 100644 index 0000000000..9344c49bec --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/manytoone/lazy/Shipment.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.manytoone.lazy; + +import java.time.Instant; +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; + +import org.hibernate.envers.AuditTable; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; + +/** + * @author Chris Cranford + */ +@Entity +@Table(name = "shipment", uniqueConstraints = @UniqueConstraint(columnNames = { "identifier" })) +@Audited +@AuditTable(value = "shipment_audit") +public class Shipment extends BaseDomainEntity { + private static final long serialVersionUID = 5061763935663020703L; + + @Column(name = "due_date", nullable = false, updatable = false) + private Instant dueDate; + + @Column(name = "identifier", nullable = false, updatable = false) + private String identifier; + + @Version + @Column(name = "mvc_version", nullable = false) + private Long mvcVersion; + + @Column(name = "closed") + private Boolean closed; + + @ManyToOne(optional = true, fetch = FetchType.LAZY, targetEntity = AddressVersion.class) + @JoinColumns(value = { + @JoinColumn(name = "origin_address_id", referencedColumnName = "id", nullable = true), + @JoinColumn(name = "origin_address_version", referencedColumnName = "version", nullable = true) + }) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private AddressVersion origin; + + @ManyToOne(optional = true, fetch = FetchType.LAZY, targetEntity = AddressVersion.class) + @JoinColumns(value = { + @JoinColumn(name = "destination_address_id", referencedColumnName = "id", nullable = true), + @JoinColumn(name = "destination_address_version", referencedColumnName = "version", nullable = true) + }) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private AddressVersion destination; + + Shipment() { + } + + Shipment(Instant when, String who, Instant dueDate, String identifier, AddressVersion origin, AddressVersion dest) { + super( when, who ); + this.dueDate = dueDate; + this.identifier = Objects.requireNonNull(identifier ); + this.origin = origin; + this.destination = dest; + } + + public Instant getDueDate() { + return dueDate; + } + + public String getIdentifier() { + return identifier; + } + + public Boolean getClosed() { + return closed; + } + + public void setClosed(Boolean closed) { + this.closed = closed; + } + + public AddressVersion getOrigin() { + return origin; + } + + public void setOrigin(AddressVersion origin) { + this.origin = origin; + } + + public AddressVersion getDestination() { + return destination; + } + + public void setDestination(AddressVersion destination) { + this.destination = destination; + } +} \ No newline at end of file