Data lost when any query executed in EntityListener

I have an interesting case that might be a critical bug in Hibernate - or not?
I tested it using Hibernate 6.6.9.Final, but it also happens with 6.4.

I have the following entities:

public class DirectoryObject
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @OneToOne
    @JoinColumn(name = "DIRECTORY_OBJECT_DATA_ID")
    private DirectoryObjectData data;
}
public class DirectoryObjectData
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @OneToOne
    @JoinColumn(name = "DIRECTORY_OBJECT_ID")
    private DirectoryObject object;
}
@EntityListeners({DirectoryObjectAssignmentEntityListener.class})
public class DirectoryObjectAssignment
{
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "PARENT_DIRECTORY_OBJECT_ID")
    private DirectoryObject parent;

    @ManyToOne(optional = false)
    @JoinColumn(name = "CHILD_DIRECTORY_OBJECT_ID")
    private DirectoryObject child;
}

So the DirectoryObject and DirectoryObjectData entities both have FKs to each other. This means that an UPDATE statement is necessary after persisting both.

The DirectoryObjectAssignmentEntityListener executes a query to load totally unrelated entities (in our real application a Cache is loaded) in the same entity manager/transaction. To achieve using the same entity manager in the test case template I had to store it in a ThreadLocal.

The test case does the following:

  1. Persists a bunch of DirectoryObject entities
  2. Persists a bunch of related DirectoryObjectData entities
  3. It connects the two together by setting them in each other
  4. Persists a bunch of DirectoryObjectAssignment entities
  5. Step 4 calls the entity listener, which executes an unrelated query in the same entity manager/transaction

Result: The data from step 3 is missing, DirectoryObject#data is simply NULL. The UPDATE statements are never executed.

When adding a flush() between steps 3 and 4 this does not happen. When removing step 5 this does not happen. When executing step 5 in a new transaction this does not happen.

I was able to create a reproducer:

Why is this happening? Do you think it’s a bug?

It’s happening because this is unsupported. You can’t invoke operations on the EntityManager in a listener. Also see the JPA spec warning:

  • In general, the lifecycle method of a portable application should not invoke EntityManager or query operations, access other entity instances, or modify relationships within the same persistence context[44][45]. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.

Executing a query will lead to auto-flushing and loading of new entities, which messes with the persistence context. As you can imagine, messing with the persistence context, while a flush is going on is highly delicate, which is why this is not supported.

You can try to create a new session for queries through e.g. the Session#sessionWithOptions() method and do the query through that, since that will have an isolated persistence context.

Thank you very much for your reply @beikov! I also read the spec and that section, but wasn’t sure and couldn’t find what a “portable application” was. So I wasn’t sure if it applied. Do you know what it is or what other application types there are?

Thanks for your suggestion with the new transaction. That’s also what we’re doing now since we noticed the problem. We’re calling an EJB method that requires a new transaction to be created.

Essentially the spec says that if one persistence provider supports this, that’s ok, but applications should not rely on this to work if they want to allow switching the JPA implementation. A portable application is one that can work with different JPA implementations i.e. only relies on specified behavior.

1 Like