Skip to content

Guard child datastore entity registration against a null GORM enhancer (late registration NPE)#15776

Merged
jamesfredley merged 3 commits into
8.0.xfrom
fix/child-datastore-initialization
Jun 30, 2026
Merged

Guard child datastore entity registration against a null GORM enhancer (late registration NPE)#15776
jamesfredley merged 3 commits into
8.0.xfrom
fix/child-datastore-initialization

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Hibernate child datastores (one per non-default dataSources connection) share the parent datastore's MappingContext. Both the parent and every child install a MappingContext.Listener whose persistentEntityAdded callback calls gormEnhancer.registerEntity(entity). Child datastores are intentionally not GORM-enhanced - their gormEnhancer field is null (the parent is the sole owner of enhancement). When a domain class is registered into the shared MappingContext after the child datastores have been constructed, the child's listener fires and dereferences its null enhancer:

java.lang.NullPointerException: Cannot invoke
"org.grails.orm.hibernate.HibernateGormEnhancer.registerEntity(org.grails.datastore.mapping.model.PersistentEntity)"
because "this.this$0.gormEnhancer" is null

This is the child-datastore-initialization defect identified in #15678 ("Thread 2"). This PR fixes it surgically and nothing else.

Root cause (exact)

  • HibernateDatastore (H5 and H7) adds, in its constructor, a MappingContext.Listener that calls gormEnhancer.registerEntity(entity) on every persistentEntityAdded.
  • The MappingContext is shared - the parent passes its context to each child datastore, so a single mappingContext.addPersistentEntity(...) notifies the parent listener and every child listener.
  • A child datastore's gormEnhancer is null by design; only the parent enhances.
  • Therefore any entity added to the context after child datastores exist (late / dynamic registration) NPEs in the child listener before the parent listener can enhance it.

Fix

Guard both listeners (H5 + H7 HibernateDatastore) so a datastore enhances only when it actually owns an enhancer:

public void persistentEntityAdded(PersistentEntity entity) {
    if (gormEnhancer != null) {
        gormEnhancer.registerEntity(entity);
    }
}

The parent (non-null enhancer) still registers the late entity for all qualifiers - including non-default dataSources names - so multi-datasource routing is unchanged; the child listener simply no-ops.

Why this approach (and not #15678's child self-enhancement)

#15678 resolved the same Thread-2 concern by making each child enhance itself (ChildHibernateDatastore.initialize() builds its own HibernateGormEnhancer). That required the heavier machinery #15678 then had to add:

  • getDatastoreForConnection returning null during initialization to avoid a ConfigurationException when a sibling is looked up before all children are registered, and
  • the bindParent() / PARENT_HOLDER thread-local to order parent assignment during the super-constructor chain.

Keeping children un-enhanced (the existing 8.0.x design) makes all of that unnecessary: there is no child enhancer to construct or order, no sibling lookup during init, and therefore no init-order ConfigurationException. The single real defect that remains in this model is the null-enhancer dereference, which this guard removes.

Changed files

  • grails-data-hibernate5/core/.../org/grails/orm/hibernate/HibernateDatastore.java - null-guard the listener.
  • grails-data-hibernate7/core/.../org/grails/orm/hibernate/HibernateDatastore.java - null-guard the listener.
  • grails-data-hibernate5/core/.../connections/MultipleDataSourceConnectionsSpec.groovy - regression test + LateRegisteredBook entity.
  • grails-data-hibernate7/core/.../connections/MultipleDataSourceConnectionsSpec.groovy - regression test + LateRegisteredBook entity.

Verification (evidence)

The regression test "late registered entity is enhanced without child datastore listener failure" registers @Entity LateRegisteredBook { static mapping = { datasource 'books' } } through the public datastore.mappingContext.addPersistentEntity(LateRegisteredBook) after child datastores are initialized, then asserts LateRegisteredBook.withNewSession { ... } resolves to the mapped datasource (jdbc:h2:mem:books).

  • Without the guard (reverted to the 8.0.x un-guarded listener): the test fails with the NPE above at MultipleDataSourceConnectionsSpec.groovy:196 - confirming the defect is real and the guard is load-bearing.
  • With the guard: green on both Hibernate lines, and the late-registered entity routes to jdbc:h2:mem:books (parent enhancement intact).

Local results (./gradlew --no-daemon --max-workers=1 ... -PmaxTestParallel=1):

  • :grails-data-hibernate5-core:test --tests MultipleDataSourceConnectionsSpec - 5/5 PASS (late-registration + existing multi-datasource routing, first-non-default-datasource, ALL-mapped, and @Transactional(connection='books') tests).
  • :grails-data-hibernate7-core:test --tests MultipleDataSourceConnectionsSpec - 5/5 PASS.
  • :grails-data-hibernate7-core:test --tests ChildHibernateDatastoreUnitSpec - PASS.

Scope / non-goals

This PR addresses only the child-datastore null-enhancer NPE (#15678 Thread 2). The O(entityCount × tenantCount) GORM API-allocation scaling (#15678 Thread 1) and the per-tenant transaction-synchronisation fixes are handled independently in #15771. The two compose cleanly: #15771 keeps the parent as the (now lazy) enhancer; this PR keeps child listeners from dereferencing a null enhancer.

Skip late entity registration from Hibernate 5 child datastore listeners that intentionally have no GORM enhancer, leaving the parent enhancer to register public APIs for shared mapping-context entities.

Assisted-by: opencode:openai/gpt-5.5 oracle
Skip late entity registration from Hibernate 7 child datastore listeners that intentionally have no GORM enhancer, leaving the parent enhancer to register public APIs for shared mapping-context entities.

Assisted-by: opencode:openai/gpt-5.5 oracle
Copilot AI review requested due to automatic review settings June 26, 2026 22:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens multi-datasource Hibernate datastores (Hibernate 5 and Hibernate 7 lines) against late entity registration when child datastores are created without a GORM enhancer, and adds regression coverage to ensure enhancement and datasource routing still behave correctly.

Changes:

  • Guard MappingContext.Listener#persistentEntityAdded in Hibernate 5/7 datastores to avoid NPEs when gormEnhancer is absent (child datastore scenario).
  • Add regression tests for late entity registration to ensure the parent enhancer still wires up public GORM APIs.
  • Verify withNewSession routing for the late-registered entity uses the mapped books datasource.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java Prevents child-datastore mapping-context listener from failing when gormEnhancer is null.
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.java Same null-guard for Hibernate 5 line to avoid listener failures on late entity registration.
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy Adds Hibernate 7 regression test validating late registration is enhanced and routes withNewSession to books.
grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/connections/MultipleDataSourceConnectionsSpec.groovy Adds Hibernate 5 regression test validating late registration is enhanced and routes withNewSession to books.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

This was found during #15678 and now carved off into dedicated PR.

@jamesfredley jamesfredley changed the title Guard child datastore entity registration Guard child datastore entity registration against a null GORM enhancer (late registration NPE) Jun 27, 2026
@testlens-app

testlens-app Bot commented Jun 29, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: c4bd064
▶️ Tests: 22801 executed
⚪️ Checks: 44/44 completed


Learn more about TestLens at testlens.app.

@jamesfredley jamesfredley merged commit 047a098 into 8.0.x Jun 30, 2026
45 checks passed
@jamesfredley jamesfredley deleted the fix/child-datastore-initialization branch June 30, 2026 14:46
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Apache Grails Jun 30, 2026
@jamesfredley

Copy link
Copy Markdown
Contributor Author

@borinquenkid FYI This fix for child datastore has been merged.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants