Simple query returns proxy elements?

I performed a batch operation on seemingly simple and similar data, but for some entries Hibernate used proxies whereas for others it didn’t, even though the data is very similar. I narrowed the problem down to a minimum to reproduce it.

I know how to work around this problem, but I would like to know if this is expected behavior or a bug.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Comment {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    @ManyToOne(fetch = FetchType.LAZY)
    private Comment replyTo;

    public Long getId() { return id; }

    public Comment() { }
    public Comment(String firstName) {
        this.firstName = firstName;
    }
    public Comment(String firstName, Comment replyTo) {
        this(firstName);
        this.replyTo = replyTo;
    }
}
@Entity
public class ProductComment extends Comment {
    public ProductComment() { }
    public ProductComment(String firstName) {
        super(firstName);
    }
    public ProductComment(String firstName, ProductComment replyTo) {
        super(firstName, replyTo);
    }

    @Override
    public String toString() {
        return "ProductComment: " + getId();
    }
}
public class App {
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-template");

    public static void main(String[] args) {
        // Database initialization code
        runInTx(em -> {
            ProductComment c1 = new ProductComment("person1");
            ProductComment c2 = new ProductComment("person2", c1);
            ProductComment c3 = new ProductComment("person1", c2);

            em.persist(c1);
            em.persist(c2);
            em.persist(c3);
        });
        
        runInTx(em -> {
            // Query a list of some specific comments
            List<Comment> comments = em.createQuery("select c from Comment c where c.firstName = 'person1'").getResultList();
            
            // Loop through those comments and print all replies
            for (Comment comment : comments) {
                System.out.println("-comment:" + comment);
                
                List<Comment> replies = em.createQuery("select c from Comment c where replyTo = ?1")
                        .setParameter(1, comment)
                        .getResultList();
                for (Comment reply : replies) {
                    System.out.println("\t-reply: " + reply.getId());

                    // This fails when a proxy is used
                    if (! (reply instanceof ProductComment)) {
                        throw new IllegalStateException("instanceof failed");
                    }
                }
            }
        });
    }

    private static void runInTx(Consumer<EntityManager> unitOfWork) {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        unitOfWork.accept(em);

        em.getTransaction().commit();
        em.close();
    }
}

Output:

Hibernate: insert into Comment (firstName, replyTo_id, DTYPE) values (?, ?, 'ProductComment')
Hibernate: insert into Comment (firstName, replyTo_id, DTYPE) values (?, ?, 'ProductComment')
Hibernate: insert into Comment (firstName, replyTo_id, DTYPE) values (?, ?, 'ProductComment')
Hibernate: select comment0_.id as id2_0_, comment0_.firstName as firstnam3_0_, comment0_.replyTo_id as replyto_4_0_, comment0_.DTYPE as dtype1_0_ from Comment comment0_ where comment0_.firstName='person1'
-comment:ProductComment: 1
Hibernate: select comment0_.id as id2_0_, comment0_.firstName as firstnam3_0_, comment0_.replyTo_id as replyto_4_0_, comment0_.DTYPE as dtype1_0_ from Comment comment0_ where comment0_.replyTo_id=?
	-reply: 2
Exception in thread "main" java.lang.IllegalStateException: instanceof failed
	at org.example.App.lambda$main$1(App.java:53)
	at org.example.App.runInTx(App.java:64)
	at org.example.App.main(App.java:37)

This problem doesn’t happen when the following database initialization code is used instead:

ProductComment c1 = new ProductComment("person1");
ProductComment c2 = new ProductComment("person2", c1);
ProductComment c3 = new ProductComment("person2", c1);
ProductComment c4 = new ProductComment("person3", c1);

em.persist(c1);
em.persist(c2);
em.persist(c3);
em.persist(c4);

So it looks like something like this happens when the first database initialization code is used:
c1 and c3 are fully loaded in the persistence context, using the first query. C3 has a replyTo to c2, which would be lazily loaded with a proxy if accessed. When all replies to ‘person1’ are queried (using the second query), c2 would be returned in that query. Normally Hibernate would fully load C2, but because where exists a different object (c3) that is referencing c2, c2 somehow isn’t fully loaded anymore. Where IMO the expected behavior would that that c2 would be fully loaded without proxy, since all data of c2 is queried.

Is this a bug?

This is not a bug, but a deliberate decision that was made by the Hibernate team to improve performance. The problem is that JPA mandates that an entity retains its object identity for a persistence context until it is detached, so we can never replace a proxy with the actual object once it was added. We could avoid the proxy, if we always eagerly loaded the polymorphic entity, but that has a performance cost that you probably don’t want to pay. If you must know type, add a method that returns you a class object or a discriminator and do your checks based on that state. Under the hood, Hibernate will delegate the call to the proper object.

1 Like