Skip to content

#1578 Apply the willBeSaved guard to the CHECK_ON_FLUSH pass of the Hibernate patch - #1579

Open
holajsh wants to merge 1 commit into
playframework:masterfrom
holajsh:1578_hibernate_autoflush_fix
Open

holajsh wants to merge 1 commit into
playframework:masterfrom
holajsh:1578_hibernate_autoflush_fix

Conversation

@holajsh

@holajsh holajsh commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Pull Request Checklist

Helpful things

Fixes

Fixes #1578

Purpose

Since 1.11.0 (Hibernate 6.6), an entity on which save() has not been called can still make an
auto-flush fail:

org.hibernate.TransientObjectException: persistent instance references an unsaved transient
instance of '<Child>' (save the transient instance before flushing)
        at org.hibernate.engine.spi.CascadingActions$9.cascade(CascadingActions.java:389)
        at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:572)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:494)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:255)
        at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:194)
        at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:190)
        at org.hibernate.event.internal.AbstractFlushingEventListener.preFlush(AbstractFlushingEventListener.java:107)
        at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoPreFlush(DefaultAutoFlushEventListener.java:104)
        at org.hibernate.internal.SessionImpl.autoPreFlush(SessionImpl.java:1391)
        at play.db.jpa.JPQL.count(JPQL.java:32)

This contradicts Play 1.x's contract that an entity is only written when save() is called on it.
The entity is never flushed (HibernateInterceptor.findDirty() returns an empty array while
willBeSaved is false), yet its associations are still validated, and the exception aborts an
unrelated query. The pattern "build the object graph, then save the parts in order" is common in
Play 1.x application code and worked under 1.7, so the failure surfaces in production paths, not
only in tests.

Reproducer, with AnEntity.parent a @ManyToOne without cascade:

AnEntity entity = new AnEntity();
entity.save();

entity.parent = new AnotherEntity();   // transient, not saved
AnEntity.count();                      // auto-flush -> TransientObjectException

This PR extends the existing willBeSaved guard of the Hibernate patch to the second pass of
prepareEntityFlushes(), ships the rebuilt jar, and adds a regression test.

Fix

The existing reflection block is extracted into a willBeSaved(Object) helper and applied to both
loops of prepareEntityFlushes():

-                       if ( flushable( entry ) ) {
+                       if ( willBeSaved( me.getKey() ) && flushable( entry ) ) {
                                cascadeOnFlush( session, entry.getPersister(), me.getKey(), context );
-                       if ( flushable( entry ) ) {
+                       if ( willBeSaved( me.getKey() ) && flushable( entry ) ) {
                                Cascade.cascade(
                                                CascadingActions.CHECK_ON_FLUSH,

The helper body is the existing code moved verbatim: same superclass walk, same swallowed
ReflectiveOperationException, and the same default of true, so non-Play entities and setups
without Play on the classpath keep standard Hibernate behaviour. The first loop behaves as before
(continue on !willBeSaved becomes a short-circuit in the same position, before flushable). The
patch's net line count is unchanged, so the later collection-interceptor hunks keep their existing
offsets.

Background Context

Root cause

In Hibernate 6.x, AbstractFlushingEventListener.prepareEntityFlushes() iterates the persistence
context twice:

  1. the cascade-persist pass, calling cascadeOnFlush(...);
  2. a second pass, new in 6.x, calling
    Cascade.cascade(CascadingActions.CHECK_ON_FLUSH, CascadePoint.BEFORE_FLUSH, ...) to detect
    references to transient instances.

hibernate-6.6.31-patch-play.patch inserts the play.db.jpa.JPABase.willBeSaved guard into loop 1
only. Loop 2 is unguarded and runs for every flushable managed entity on every auto-flush.

The check hits associations without cascade specifically: CascadeStyles.NONE.doCascade(action)
returns true only for CHECK_ON_FLUSH, so no-cascade associations are exactly the ones inspected
(a cascade = PERSIST association returns false and is skipped, which is why adding it works as
an application-side workaround).

Compatibility note

This is a deliberate reduction in Hibernate's checking, not only a bug fix: after this change a Play
application gets the transient-reference check during an explicit save() (where willBeSaved is
true) and never on an incidental auto-flush. That is Play 1.x's documented model and matches the
1.7 / Hibernate 5 behaviour. Applications that relied on 1.11.0 to catch missing saves early will
get the exception at save() time instead of at flush time.

No API change, no configuration change, no change for non-Play users of the patched jar.

The rebuilt jar

framework/lib/hibernate-core-6.6.31.Final.patched.jar is updated in this PR, since the vendored jar
is what framework/dependencies.yml resolves and the new test fails against the 1.11.0 jar.

The binary change is deliberately minimal and auditable: only
org/hibernate/event/internal/AbstractFlushingEventListener.class differs from the 1.11.0 jar
(entry CRC 91b5f23d to 1c5c3599). It was produced by applying the patch's
AbstractFlushingEventListener section to the pristine 6.6.31.Final source, recompiling that single
file against the jars already in framework/lib with javac --release 11 to match the existing
bytecode level (major version 55), and replacing the one entry in the archive. No other class,
resource or manifest entry is touched.

To verify:

unzip -v <1.11.0 jar>      # compare the CRC-32 column with
unzip -v <this PR's jar>   # only AbstractFlushingEventListener.class differs
javap -p -c -cp <this PR's jar> org.hibernate.event.internal.AbstractFlushingEventListener

Because it was not produced by the Hibernate gradle build described in
framework/patches/hibernate-6.6.31-patch-play.README, it is not byte-identical to an official
rebuild. Happy to replace it with a full gradle-built artifact if you prefer that for the release.

References

Testing done

  • Added samples-and-tests/just-test-cases/test/FlushTransientReferenceTest.java, using the
    existing AnEntity.parent mapping (a @ManyToOne with no cascade).
    • Against the 1.11.0 jar, unsavedOwnerIsNotCheckedForTransientReferences fails with
      TransientObjectException from AbstractFlushingEventListener.prepareEntityFlushes, triggered
      by the auto-flush of AnEntity.count().
    • Against the jar in this PR, both tests pass.
  • The updated patch applies with no fuzz to pristine hibernate-core-6.6.31.Final sources from
    Maven Central.
  • The rebuilt jar differs from the 1.11.0 one by exactly one entry
    (AbstractFlushingEventListener.class), at the same bytecode level (major version 55). Its
    bytecode shows the willBeSaved guard ahead of both cascadeOnFlush and the CHECK_ON_FLUSH
    cascade, and the existing collection-interceptor hunks still test their boolean results
    (ifeq, not pop).

I put the target as master but it would be welcome to have a 1.11.1 with this change only included if possible.

…epareEntityFlushes()

that checks references to transient instances (CascadingActions.CHECK_ON_FLUSH). The Play
patch only guarded the first pass with JPABase.willBeSaved, so an auto-flush could fail with
a TransientObjectException on an entity that was never saved.

Extract the guard into a willBeSaved(Object) helper, apply it to both passes, ship the
rebuilt hibernate-core jar, and add FlushTransientReferenceTest to just-test-cases.

Fixes playframework#1578
@mergify

mergify Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@xael-fry
xael-fry self-requested a review October 14, 2026 14:12
@xael-fry xael-fry added this to the 1.12.0 milestone Oct 14, 2026
@holajsh

holajsh commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Deprecated by #1583 which is probably by far a better solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TransientObjectException on auto-flush for entities that were never save()d (1.11.0 / Hibernate 6.6)

2 participants