This bug describes the behavior and fix for the issue of nested flush() calls when using the @PrePersist annotation callback method on an unmanaged entity containing a read query method that causes a flush() when the entity is the target of a relationship from a managed entity that has pending changes.
This bug is also encountered when a secondary persist is executed inside of a @PrePersist callback method on the Entity being itself persisted. Soo there are two use cases here - we will concentrate on the read query causing a secondary flush to sync changes before the read - causing an infinite loop or double persist which causes a PK conflict.
See p.139 of Pro EJB 3.0 "Some providers will flush the persistence context to ensure that the query incorporates all pending changes" - EclipseLink is one of these providers.
Here is what the JPA specification says about performing persists inside the @PrePersist method. Essentially it states that we should not be performing persists inside a @PrePersist callback - however it may be the responsibility of the container to handle any that may be done depending on the nature of the persistence.
"In general, portable applications should not invoke EntityManager or Query operations, access other entity instances, or modify relationships in a lifecycle callback method."
" The semantics of such operations may be standardized in a future release of this specification."
"In general, portable applications should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked."
This matches the data model used in Chapter 5 of Pro EJB 3.0.
The result is that the @PrePersist method causes it's own flush() within a flush() - we need to disallow this.
This use case currently causes an infinite loop on the following line up to build 20090105.
[EL Finest]: 2009.01.05 13:09:22.915--UnitOfWork(2804823)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Cell@8180602( id: null state: null left: null right: null).
The @PrePersist of the first entity will cause a persist of the second entity that also has it's own @PrePersist (but without another persist).
Normal functionality with no infinite loop.
The entityManagerFactory is thread-safe, the entityManager is not. However, the boolean flag withinFlush is a non-static field on each instance of EntityManager so as long as the lifecyle of the entityManager is thread-safe then the addition of this flag will have no issues.
We will not perform nested flush() calls, a break will be required and a warning will printed if the entityManager is in FlushModeType.AUTO.
We will accomplish this by adding a boolean flag withinFlush to the EntityManagerImpl class that will be true for the lifetime of any flush() call. We will check this flag before issuing a flush in the preparation within EJBQueryImpl.executeReadQuery() - which occurs in the case that a query requires pending uncommitted changes written before running it's query. This would happen if a query or persist were done inside of a @PrePersist callback method.
The call to calculateChanges() can be recursive before we even get to commitToDatabaseWithPreBuiltChangeSet(). In this case we will commit in a PrePersist and then later fail on a second commit. We therefore need to move the flag set before the try/catch block.
that will be set inside flush() just before writeChanges() to true and then reset to false on the finaly block. The use of this flag will be thread safe as far as the entityManager instance is used in a thread safe manner.
EJBQueryImpl when executing a read (possibly from a @PrePersist callback) will check that it is not already in a flush() before doing a pre-flush() - normally done so that any outstanding (uncommitted) changes are included in this query.
The following log will not be used.
The following log will be used.
[EL Warning]: 2009.01.11 21:36:21.786--Thread(Thread[main,5,main])--The em: org.eclipse.persistence.internal.jpa.EntityManagerImpl@1777b1 is already flushing. The query will be executed without further changes being written to the database. If the query is conditional upon changed data the changes may not be reflected in the results. Users should issue a flush() call upon completion of the dependent changes and prior to this flush() to ensure correct results.
The following logs and stacktrace illustrate the current nested flush() behavior when performing a @PrePersist containing a query that requires a flush() on uncommitted managed entity changes.
Client code is persisting 2 Cell entities that themselves persist new Cell entities in their @PrePersist method.
[EL Finest]: 2009.01.11 21:36:21.483--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Employee@18433730( id: null).
[EL Finest]: 2009.01.11 21:36:21.495--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Employee@21684929( id: null).
[EL Finest]: 2009.01.11 21:36:21.495--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.ParkingSpace@23930626( id: null).
[EL Finest]: 2009.01.11 21:36:21.501--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Address@15594377( id: null).
[EL Warning]: 2009.01.11 21:36:21.786--Thread(Thread[main,5,main])--The em: org.eclipse.persistence.internal.jpa.EntityManagerImpl@1777b1 is already flushing. The query will be executed without further changes being written to the database.  If the query is conditional upon changed data the changes may not be reflected in the results.  Users should issue a flush() call upon completion of the dependent changes and prior to this flush() to ensure correct results.
[EL Finest]: 2009.01.11 21:36:21.802--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Phone@30008954( id: null).
[EL Finest]: 2009.01.11 21:36:21.806--UnitOfWork(7634850)--Thread(Thread[main,5,main])--PERSIST operation called on: org.eclipse.persistence.example.business.Phone@3823508( id: null).
Notice that there is no second flush.
I have found that increasing the stack to the value below pushed the StackOverflowError experienced in a bidirectionally linked 6 dimensional Hypercube model (64 entities) to a 13 dimensional Hypercube (8192 entities) in bug# 310662.
During the research for this bug, the following items were identified as out of scope but are captured here as potential future enhancements. If agreed upon during the review process these should be logged in the bug system.
This page was last modified 10:55, 28 April 2010 by Michael OBrien.