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:
parent
e8d436ffb2
commit
67f1a809b2
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue