Skip to content

Hibernate 7 - Step 2#15568

Open
jdaugherty wants to merge 1286 commits into
8.0.xfrom
8.0.x-hibernate7
Open

Hibernate 7 - Step 2#15568
jdaugherty wants to merge 1286 commits into
8.0.xfrom
8.0.x-hibernate7

Conversation

@jdaugherty

@jdaugherty jdaugherty commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Reopening. This PR replaces #15530

Please note that I split off a prerequisite PR #15654

This allows us to better see what changed between hibernate 5 & 7 - otherwise the hibernate 7 code looks like it was just added instead of changed. Commit a47d8cb is the original commit prior to this split if we need to compare this branch to that for any reason (mistakes, etc).

borinquenkid and others added 23 commits April 8, 2026 09:49
- PropertyConfigSpec: 14 new tests covering column(Closure) with
  firstColumnIsColumnCopy, getJoinTableColumnConfig, configureExisting
  with pre-existing columns, column-delegate getters (getEnumType,
  getSqlType, getIndexName, getLength, getPrecision), toString, and
  clone with typeParams
- HibernateAssociationQuerySpec: add negation() test covering L88

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tructor test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…der branch

Add test covering L77-78: when componentBinder is set on the binder and
the property is HibernateEmbeddedCollectionProperty, bindCollectionSecondPass
delegates to componentBinder.bindEmbeddedCollectionComponent() and sets
the result as the collection element.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix 17 plain executeUpdate('...') calls across 7 specs in
  grails-test-examples/gorm to use executeUpdate('...', [:]).
  H7's HibernateGormStaticApi rejects plain CharSequence args
  (requires either a GString with interpolated params or the
  Map overload).

- Add getDomainClasses() override to BookHibernateSpec in app1.
  H7's HibernateSpec uses HibernateDatastoreSpringInitializer
  which requires explicit domain class declaration; H5 auto-detected
  via classpath scanning.

- Remove grails-test-examples-app1 and grails-test-examples-gorm
  from h7IncompatibleProjects list — both now run cleanly under
  Hibernate 7 via dependency substitution.

Remaining excluded: datasources (ChainedTransactionManager),
views-functional-tests (HAL/JSON diffs), scaffolding-fields
(grails-fields rendering diffs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…urn types, cross-property arithmetic

Bug 2: HibernateQueryExecutor.singleResult() now catches both
org.hibernate.NonUniqueResultException and jakarta.persistence.NonUniqueResultException
(H7 throws the JPA variant; the original catch missed it) and returns the
first result instead of propagating.

Bug 4: HqlQueryContext.aggregateTargetClass() now returns precise types per
function: count() → Long, avg() → Double, sum/min/max() → Number.
Previously all aggregates were bound to Long, causing QueryTypeMismatchException
in H7's strict SQM type checking.

Bug 5: Cross-property arithmetic in where-DSL (e.g. pageCount > price * 10)
was silently dropped — the RHS property reference was coerced to a literal.
Fixed via:
- PropertyReference: Groovy wrapper returned by propertyMissing for numeric
  properties; *, +, -, / operators produce a PropertyArithmetic value object
- PropertyArithmetic: value type carrying (propertyName, Operator, operand)
- HibernateDetachedCriteria: H7-only DetachedCriteria subclass that overrides
  propertyMissing to return PropertyReference for numeric properties, and
  newInstance() to preserve the subtype through cloning
- HibernateGormStaticApi: overrides where/whereLazy/whereAny to use
  HibernateDetachedCriteria as the closure delegate
- PredicateGenerator: resolveNumericExpression() detects PropertyArithmetic
  and builds cb.prod/sum/diff/quot(path, operand) instead of a literal

H5 and MongoDB are unaffected — all new types are confined to grails-data-hibernate7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ve on managed entities

H7 enforces strict collection identity during flush. GORM's addTo* and
save() flow had two failure modes:

1. When an entity is already managed in the current Hibernate session,
   calling session.merge() causes H7 to create a second PersistentCollection
   for the same role+key alongside the one already tracked in the session
   cache -> 'Found two representations of same collection'.

   Fix (HibernateGormInstanceApi.performMerge): check session.contains(target)
   before merging. If the entity is already managed, skip merge entirely;
   dirty-checking and cascade will handle children on flush.

2. When addTo* is called on a managed entity, GormEntity.addTo uses direct
   field access (reflector.getProperty) which bypasses H7's bytecode-enhanced
   interceptor, sees null, and creates a plain ArrayList on the field. H7's
   session cache already tracks a PersistentBag/Set for that role -> two
   representations on the next save.

   Fix (HibernateEntity.addTo): override addTo in the H7 trait; for managed
   entities (id != null), trigger the H7 interceptor via InvokerHelper.getProperty
   to obtain the live PersistentCollection before delegating to
   GormEntity.super.addTo.

   Fix (HibernateEntityTransformation): re-target the concrete addToXxx
   generated methods so their internal addTo call dispatches through
   HibernateEntity.addTo rather than being hard-wired to GormEntity.addTo.

   Fix (HibernateGormInstanceApi.reconcileCollections): detect stale
   PersistentCollections (session != current session) and replace them with
   plain collections before merge, covering any edge cases where the H7
   interceptor path is not taken.

Adds AddToManagedEntitySpec with 4 tests covering:
- addTo on an already-persisted entity
- multiple addTo on a fresh transient entity
- modify child + save twice
- removeFrom + save

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…and proper applicationClass

- Replace executeQuery(plainString) and executeUpdate(plainString) calls
  with the (String, Map) overloads (empty map for parameterless queries).
  HibernateGormStaticApi intentionally rejects plain String in the
  no-arg overload to prevent HQL injection; parameterless static queries
  must use the Map overload.

- Add applicationClass = Application to @Integration so the spec shares
  the same application context and transaction manager as the other specs
  in this module, preventing test-data bleed between specs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	NOTICE
#	dependencies.gradle
#	gradle/publish-root-config.gradle
…ibernate7

# Conflicts:
#	.idea/codeStyles/Project.xml
#	NOTICE
#	build-logic/plugins/build.gradle
#	dependencies.gradle
#	gradle/publish-root-config.gradle
  Following the pattern used in Hibernate 5, I have moved the vendored Spring Framework ORM Hibernate 7 integration classes from the core module to a dedicated spring-orm module:
   - Created Module: grails-data-hibernate7-spring-orm (located in grails-data-hibernate7/spring-orm).
   - Moved Classes: All classes in org.grails.orm.hibernate.support.hibernate7 (including HibernateTransactionManager, HibernateTemplate, LocalSessionFactoryBean, etc.) are now in the new module.
   - Updated Dependencies:
       - Added the new module to settings.gradle.
       - Updated grails-data-hibernate7-core, grails-plugin, boot-plugin, and dbmigration to depend on the new spring-orm module.
       - Updated gradle/publish-root-config.gradle to ensure the new module is included in the publishing process.
- HibernateDetachedCriteria.isNumericPropertyType: box primitive types
  before the Number.isAssignableFrom check so that domains declaring
  numeric properties as primitives (int/long/double/float/short/byte)
  work correctly in where-DSL arithmetic expressions.
  Method is protected to allow subclass overrides.

- ToManyEntityMultiTenantFilterBinder.bind: add null guard for
  getHibernateAssociatedEntity() return value to prevent
  NullPointerException on partially-resolved associations.

- grails-data-hibernate7/README.md: add grails-data-hibernate7-spring-orm
  to the Module Structure table.

- Tests: new HibernateDetachedCriteriaSpec covering boxed and all 6
  primitive numeric types, non-numeric delegation, and unknown property.
  Added null-associated-entity test to ToManyEntityMultiTenantFilterBinderSpec.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	grails-data-mongodb/core/src/test/groovy/org/apache/grails/data/mongo/core/GrailsDataMongoTckManager.groovy
#	grails-data-mongodb/core/src/test/groovy/org/grails/datastore/gorm/mongo/BeforeUpdatePropertyPersistenceSpec.groovy
#	grails-datamapping-core-test/src/test/groovy/org/grails/datastore/gorm/CustomAutoTimestampSpec.groovy
#	grails-datamapping-core-test/src/test/groovy/org/grails/datastore/gorm/NestedCriteriaWithNamedQuerySpec.groovy
#	grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/GormStaticApi.groovy
#	grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/NamedCriteriaProxy.groovy
#	grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/NamedQuerySpec.groovy
#	grails-test-suite-uber/src/test/groovy/grails/compiler/DomainClassWithInnerClassUsingStaticCompilationSpec.groovy
@jdaugherty jdaugherty mentioned this pull request Apr 10, 2026
@jdaugherty jdaugherty changed the base branch from 7.0.x to 8.0.x April 10, 2026 14:02
@testlens-app

This comment has been minimized.

@borinquenkid

Copy link
Copy Markdown
Member

I believe I have explanations or resolutions to all of my critical issues. These 3 remain:

  1. grails-gsp/.../GroovyPagesServlet.java
    A change to the thread context class loader flagged by PMD. I think it's probably fine but this has caused issues historically. I'll leave the conversation unresolved in case @davydotcom wants to respond.
  2. grails-datastore-core/.../ConnectionSource.java
    The default datastore name was changed. I am OK with it but I'd like to understand the rationale — why was it necessary to change the default?
  3. I left a TODO in the mongo doc build since we can't include hibernate5 & hibernate7. Hopefully @jamesfredley has some feedback here.
  1. Leaving this unresolved pending response from @davydotcom. PMD flags the thread context class loader change but behavior is unchanged from H5; flagged for committer review before merge.

  2. Hibernate 7's HibernateConnectionSourceFactory registers its datastores using the key "default" (lowercase). The old constant ConnectionSource.DEFAULT = "DEFAULT" (uppercase) caused silent lookup
    misses because HashMap key comparison is case-sensitive — the constant and the registered key never matched. Changing the constant to "default" aligns it with what H7 actually registers. Backward
    compatibility is preserved two ways: OLD_DEFAULT = "DEFAULT" is kept @deprecated for any code referencing the old string literal, and GormRegistry.normalizeQualifier() coerces any incoming "DEFAULT" to
    "default" transparently via OLD_DEFAULT.equalsIgnoreCase().

    1. MongoDB doc TODO

This is a structural problem: the Grails doc build is a single pass and can only pull from one GORM data module at a time (H5 or H7), but MongoDB is independent of both. There are three realistic
options:

Option A — separate doc builds (recommended long-term)
Build the guide twice — once with H5 and once with H7 — publishing to versioned paths (/docs/h5/, /docs/h7/). MongoDB docs live in a shared adoc include that both pick up. This is the cleanest but
requires CI and site infra changes.

Option B — conditional AsciiDoc includes (quick fix)
Use AsciiDoc attributes/ifdef in the doc source to conditionally include H5 vs H7 content, driven by a build flag. MongoDB content that applies to both goes in a shared include file. This is what most
projects do when they can't split the build yet.

Option C — link out to standalone GORM docs (unblock now)
Replace the TODO with a link to the standalone GORM for MongoDB docs (published independently). Removes the coupling entirely. Weakest option but unblocks the PR immediately.

@jamesfredley Is Option A (separate builds) is on the roadmap or Option B (build flag) is acceptable for M2.

borinquenkid and others added 2 commits May 24, 2026 20:10
The ASF infrastructure gateway requires all third-party actions to use
commit SHAs listed in apache/infrastructure-actions approved_patterns.yml.
The previously used hash (4d9f0ba, v5.0.0) was not in the approved list,
causing startup_failure on pull_request and workflow_dispatch events.

Update all 10 workflow files to use 50e97c2 (v6.1.0), the latest
ASF-approved commit SHA for gradle/actions/setup-gradle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tyMapping

    The TCK tests for MongoDB were failing because `getStoredAs()` in the `IdentityMapping` interface returned a default of `null`, and `DefaultIdentityMapping` did not override it. This caused the
  MongoDB engine to lose track of explicit `storedAs` values (like `ObjectId`), breaking coercion.

    This commit overrides `getStoredAs()` in `DefaultIdentityMapping` to return `getMappedForm().getStoredAs()`, restoring the intended behavior.
@borinquenkid

Copy link
Copy Markdown
Member

To help guide the review of this PR, I want to provide a high-level overview of the major architectural changes and design decisions introduced in this branch. Given the magnitude of the changes, this
context should clarify the mechanical shifts across the modules.

  1. Hibernate Criteria API to JPA Criteria API Migration
    Hibernate 6 and 7 completely removed the legacy native Criteria API, replacing it with the JPA Criteria API. Because our HibernateCriteriaBuilder heavily depended on that legacy API, we had to rethink
    how queries are built. To bridge this gap, we implemented a chain using HibernateCriteriaBuilder and HibernateQuery. This chain now leverages GORM's DetachedCriteria internally to collect all the
    necessary query elements and state before eventually constructing and executing the final JPA query.
  2. HQL Query Compilation and State Management
    On the HQL side of things, we introduced the HQLQueryContext. This new context object is responsible for cleanly collecting all state and handling the compilation of HQL queries, providing better
    isolation and structure for HQL execution.
  3. GrailsDomainBinder Decomposition
    The GrailsDomainBinder had grown significantly and was handling too many responsibilities. As part of this update, it has been decomposed into multiple focused, smaller classes. This improves
    maintainability and makes it much easier to reason about the binding lifecycle.
  4. HibernateMappingContext Hierarchy
    We tightened the HibernateMappingContext hierarchy to be strictly Hibernate-specific. This ensures that the context semantics are well-defined and prevents leakage of generic data-mapping concepts
    where Hibernate-specific behavior is required.

These architectural shifts are the primary drivers of the footprint of this PR. The accompanying style adjustments (CodeNarc, Checkstyle, PMD) were applied mechanically to ensure the new monorepo
maintains high code quality from day one and to prevent accumulating technical debt moving forward.

Let me know if you have any questions about specific implementation details in these areas!***

Comment thread .github/workflows/codeanalysis.yml
…le-quotes inside single-quoted strings

Three regex bugs in registerCodenarcFixTask:

1. SpaceAroundMapEntryColon matched the first ':' of '::' method references,
   turning String::trim into String: :trim.
   Fix: add (?!:) lookahead so only standalone map-entry colons are matched.

2. UnnecessaryGString fused the closing quote of a GString with the opening
   quote of an adjacent plain string (e.g. obj."${name}"("arg") became
   obj."${name}'('arg")).
   Fix: add (?<!}) lookbehind so a double-quote preceded by '}' is never
   treated as an opening delimiter.

3. UnnecessaryGString matched double-quoted content inside single-quoted strings
   (e.g. 'format "hh:mm:ss"'), corrupting the outer string.
   Fix: the regex alternation now first matches and skips complete single-quoted
   strings so their embedded double-quoted content is never converted.

Tests added for all three cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Base automatically changed from 8.0.x-stage-hibernate7 to 8.0.x June 4, 2026 20:08
Assisted-by: opencode:openai/gpt-5.5 oracle
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 19.0184%. Comparing base (7da662f) to head (76340d7).

Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                 @@
##                8.0.x     #15568         +/-   ##
===================================================
- Coverage     48.4322%   19.0184%   -29.4138%     
+ Complexity      15265        288      -14977     
===================================================
  Files            1875         57       -1818     
  Lines           85852       3423      -82429     
  Branches        14972        597      -14375     
===================================================
- Hits            41580        651      -40929     
+ Misses          37863       2642      -35221     
+ Partials         6409        130       -6279     

see 1818 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Resolved dependencies.gradle by taking the latest base dependency versions for Spring Boot, Spock, and Sitemesh while retaining Hibernate 7 BOM-specific dependency sections.

Assisted-by: opencode:openai/gpt-5.5 oracle
@jamesfredley

Copy link
Copy Markdown
Contributor

I found a list of items here that I think are important and will have a PR against this PR in a few hours.

Resolve dependencies.gradle conflict: take mongodb 5.8.0 from 8.0.x
(aligned with the Spring Boot 4.1 BOM) and keep the reactor.version
3.8.5 entry added on this branch.

Assisted-by: claude-code:claude-opus-4-8
@matrei

matrei commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

I found a list of items here that I think are important and will have a PR against this PR in a few hours.

@jamesfredley Ok, I started reviewing, but I'll hold off until you are done.

@jamesfredley

Copy link
Copy Markdown
Contributor

@matrei @jdaugherty @borinquenkid #15731 is the list of items I found. Feel free to commit to that branch and anything can be added or subtracted. The PR description will be update in a few minutes with a full breaking changes table between h5.6 and h7.2.

Hopefully CI will finish in that PR, if not feel free to push fixes.

@testlens-app

testlens-app Bot commented Jun 12, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 76340d7
▶️ Tests: 4218 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

@jdaugherty

Copy link
Copy Markdown
Contributor Author

@jamesfredley I have said it before but I am highly against module substitution to expand test coverage. For multiple reasons:

  1. the dependency substitution conflicts are not immediately clear
  2. we only see those in CI / its not easy to run the tests locally
  3. hibernate 7 logic now is tied to hibernate 5 and can prevent us from refactoring/ evolving

We should just copy tests to expand coverage.

@borinquenkid

borinquenkid commented Jun 13, 2026

Copy link
Copy Markdown
Member

@jamesfredley @jdaugherty

Thinking ahead to the broader roadmap: we also need to start actively planning and working toward Hibernate 8.

With that in mind, framing Grails 8.0 as a permanent, dual-baseline bridge isn't sustainable. It forces us to carry too much legacy weight while trying to leapfrog forward. Instead, we should use this PR to clean house and establish a clear, fast-moving release strategy:

The Proposed Lifecycle Compromise

  1. Grails 8.0 (The Final Legacy Baseline)

    • Focuses strictly on stabilizing the Hibernate 7 migration.
    • Hibernate 5 is placed on immediate maintenance support. It will receive only critical security patches and small bug fixes—zero feature parity or active development. This gives legacy users a stable runway to plan an upgrade.
  2. Grails 8.5 (The Clean Break & Hibernate 8 Preview)

    • Completely drops support for Hibernate 5. This allows us to fully remove the dynamic dependency substitution configurations and retire the dual-module TCK infrastructure that complicates local development.
    • Introduces Hibernate 8 as the primary, forward-looking ORM baseline.

Why This Splitting of the Difference Works

  • Prevents "Triple Refactoring": Moving to Hibernate 7 required building a complex bridge (HibernateCriteriaBuilder / HibernateQuery) to handle the total removal of the legacy native Criteria API. Since Hibernate 8 will continue to build strictly on top of modern JPA semantics, dropping Hibernate 5 early ensures we don't have to support three distinct architectural eras simultaneously.
  • Prepares the Core Early: The decomposition of GrailsDomainBinder and the tightening of the HibernateMappingContext hierarchy in this PR already move us toward a cleaner, more modular design. This makes swap-in compatibility for Hibernate 8 significantly easier down the road.
  • Unblocks the Build Pipeline: It addresses the structural documentation build issues immediately. We won't have to fight single-pass build constraints to force-compile incompatible Hibernate 5 and Hibernate 7 modules alongside independent drivers like MongoDB.

By establishing this hard cutoff, we protect developer ergonomics, clean up the CI pipeline, and immediately clear the runway to start targeting Hibernate 8 features.

@jdaugherty

Copy link
Copy Markdown
Contributor Author

@borinquenkid until there is wider adoption or a dependency change that forces the retirement of 5.x, I do not see a need to preemptively retire the other code line. We should first release dual code lines and evaluate after.

@borinquenkid

borinquenkid commented Jun 13, 2026

Copy link
Copy Markdown
Member

@borinquenkid until there is wider adoption or a dependency change that forces the retirement of 5.x, I do not see a need to preemptively retire the other code line. We should first release dual code lines and evaluate after.

I think I did not make myself clear: G8 supports 5 and 7, minor patches to 5 no new features. Release quicly a G8.1 with new features and drop H5.. 5 is just our thing for this release. That way we can do things @jamesfredley way just to get it out the door and then cut off immediately and do things your way. Moving forward we should be able to just have one hibernate module once spring 5 comes in the horizon we will have a branch for H8 or whatever is relevant at the time.

@jdaugherty

Copy link
Copy Markdown
Contributor Author

Our release policy forbids dropping support for a feature in a minor release

@borinquenkid

Copy link
Copy Markdown
Member

Our release policy forbids dropping support for a feature in a minor release

Fair enough. So that would mean dropping H5 for Grails 9, SpringBoot 4.1

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants