HHH-17578 - Fix the intermittent ClassCastException that occurs when trying to call a method on a HibernateProxy where the parameter type is defined by generics/class hierarchy.

Signed-off-by: Jan Schatteman <jschatte@redhat.com>
This commit is contained in:
Jan Schatteman 2023-12-20 18:45:33 +01:00 committed by Jan Schatteman
parent e8d436ffb2
commit 67f1a809b2
2 changed files with 268 additions and 1 deletions

View File

@ -181,7 +181,11 @@ public class EntityRepresentationStrategyPojoStandard implements EntityRepresent
BytecodeProvider bytecodeProvider, BytecodeProvider bytecodeProvider,
RuntimeModelCreationContext creationContext) { RuntimeModelCreationContext creationContext) {
final Set<Class<?>> proxyInterfaces = new java.util.HashSet<>(); // HHH-17578 - We need to preserve the order of the interfaces to ensure
// that the most general @Proxy declared interface at the top of a class
// hierarchy will be used first when a HibernateProxy decides what it
// should implement.
final Set<Class<?>> proxyInterfaces = new java.util.LinkedHashSet<>();
final Class<?> mappedClass = mappedJtd.getJavaTypeClass(); final Class<?> mappedClass = mappedJtd.getJavaTypeClass();
Class<?> proxyInterface; Class<?> proxyInterface;

View File

@ -0,0 +1,263 @@
package org.hibernate.orm.test.proxy;
import jakarta.persistence.*;
import org.hibernate.annotations.Proxy;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* @author Oliver Henlich
*/
/**
* Test that demonstrates the intermittent {@link ClassCastException} that occurs
* when trying to call a method on a {@link HibernateProxy} where the parameter
* type is defined by generics.
*/
@JiraKey("HHH-17578")
@DomainModel(
annotatedClasses = {
ProxyWithGenericsTest.AbstractEntityImpl.class,
ProxyWithGenericsTest.AbstractShapeEntityImpl.class,
ProxyWithGenericsTest.CircleEntityImpl.class,
ProxyWithGenericsTest.SquareEntityImpl.class,
ProxyWithGenericsTest.CircleContainerEntityImpl.class,
ProxyWithGenericsTest.SquareContainerEntityImpl.class,
ProxyWithGenericsTest.MainEntityImpl.class
}
)
@SessionFactory
@SuppressWarnings("ALL")
public class ProxyWithGenericsTest {
@BeforeEach
void setUp(SessionFactoryScope scope) {
scope.inTransaction(session -> {
EntityManager em = session.unwrap(EntityManager.class);
// Shape 1
CircleEntityImpl cirlce1 = new CircleEntityImpl();
cirlce1.radius = BigDecimal.valueOf(1);
em.persist(cirlce1);
// Shape 2
SquareEntityImpl square1 = new SquareEntityImpl();
square1.width = BigDecimal.valueOf(1);
square1.height = BigDecimal.valueOf(2);
em.persist(square1);
// Container 1
CircleContainerEntity circleContainer1 = new CircleContainerEntityImpl();
em.persist(circleContainer1);
// Container 2
SquareContainerEntity squareContainer1 = new SquareContainerEntityImpl();
em.persist(squareContainer1);
// Main
MainEntityImpl main = new MainEntityImpl();
main.circleContainer = circleContainer1;
main.squareContainer = squareContainer1;
em.persist(main);
});
}
@Test
void test(SessionFactoryScope scope) throws Exception {
scope.inTransaction(session -> {
EntityManager em = session.unwrap(EntityManager.class);
MainEntityImpl main = em.find(MainEntityImpl.class, 1L);
assertNotNull(main);
CircleContainerEntity circleContainer = main.getCircleContainer();
assertNotNull(circleContainer);
assertTrue(circleContainer instanceof HibernateProxy);
CircleEntity circle1 = em.find(CircleEntityImpl.class, 1L);
circleContainer.add(circle1); // This method fails with ClassCastException without the fix
SquareContainerEntity squareContainer = main.getSquareContainer();
assertNotNull(squareContainer);
assertTrue(squareContainer instanceof HibernateProxy);
SquareEntity square1 = em.find(SquareEntityImpl.class, 2L);
squareContainer.add(square1); // This method fails with ClassCastException without the fix
});
}
// Shapes hierarchy -------------------------------------------------------
public interface ShapeEntity {
BigDecimal getArea();
}
public interface CircleEntity extends ShapeEntity {
BigDecimal getRadius();
}
public interface SquareEntity extends ShapeEntity {
BigDecimal getWidth();
BigDecimal getHeight();
}
@MappedSuperclass
@Access(AccessType.FIELD)
public static abstract class AbstractEntityImpl {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
Long id;
}
@Entity
@Table(name = "SHAPE")
@Access(AccessType.FIELD)
@Proxy(proxyClass = ShapeEntity.class)
@DiscriminatorColumn(name = "TYPE", length = 20)
public static abstract class AbstractShapeEntityImpl
extends AbstractEntityImpl
implements ShapeEntity {
public abstract BigDecimal getArea();
}
@Entity
@Proxy(proxyClass = CircleEntity.class)
@Access(AccessType.FIELD)
@DiscriminatorValue("CIRCLE")
public static class CircleEntityImpl
extends AbstractShapeEntityImpl
implements CircleEntity {
@Column(name = "RADIUS", nullable = true)
private BigDecimal radius;
@Override
public BigDecimal getArea() {
return new BigDecimal(Math.PI).multiply(radius.pow(2));
}
@Override
public BigDecimal getRadius() {
return radius;
}
}
@Entity
@Proxy(proxyClass = SquareEntity.class)
@Access(AccessType.FIELD)
@DiscriminatorValue("SQUARE")
public static class SquareEntityImpl
extends AbstractShapeEntityImpl
implements SquareEntity {
@Column(name = "WIDTH", nullable = true)
private BigDecimal width;
@Column(name = "HEIGHT", nullable = true)
private BigDecimal height;
@Override
public BigDecimal getArea() {
return width.multiply(height);
}
@Override
public BigDecimal getWidth() {
return width;
}
@Override
public BigDecimal getHeight() {
return height;
}
}
// ShapeContainer hierarchy -----------------------------------------------
public interface ShapeContainerEntity<T extends ShapeEntity> {
void add(T shape);
}
public interface CircleContainerEntity extends ShapeContainerEntity<CircleEntity> {
}
public interface SquareContainerEntity extends ShapeContainerEntity<SquareEntity> {
}
@Entity
@Table(name = "CONTAINER")
@Access(AccessType.FIELD)
@Proxy(proxyClass = ShapeContainerEntity.class)
@DiscriminatorColumn(name = "TYPE", length = 20)
public static abstract class AbstractShapeContainerEntityImpl<T extends ShapeEntity>
extends AbstractEntityImpl
implements ShapeContainerEntity<T> {
}
@Entity
@Proxy(proxyClass = SquareContainerEntity.class)
@Access(AccessType.FIELD)
@DiscriminatorValue("SQUARE")
public static class SquareContainerEntityImpl
extends AbstractShapeContainerEntityImpl<SquareEntity>
implements SquareContainerEntity {
@Override
public void add(SquareEntity shape) {
}
}
@Entity
@Proxy(proxyClass = CircleContainerEntity.class)
@Access(AccessType.FIELD)
@DiscriminatorValue("CIRCLE")
public static class CircleContainerEntityImpl
extends AbstractShapeContainerEntityImpl<CircleEntity>
implements CircleContainerEntity {
@Override
public void add(CircleEntity shape) {
}
}
/**
* Main test entity that has lazy references to the two types of {@link ShapeContainerEntity containers}.
*/
@Entity
@Table(name = "Main")
@Access(AccessType.FIELD)
public static class MainEntityImpl
extends AbstractEntityImpl {
@ManyToOne(targetEntity = AbstractShapeContainerEntityImpl.class, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "CIRCLE_CONTAINER_ID")
private CircleContainerEntity circleContainer;
@ManyToOne(targetEntity = AbstractShapeContainerEntityImpl.class, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "SQUARE_CONTAINER_ID")
private SquareContainerEntity squareContainer;
public CircleContainerEntity getCircleContainer() {
return circleContainer;
}
public SquareContainerEntity getSquareContainer() {
return squareContainer;
}
}
}