Skip to content

Add GORM for MongoDB / Spring Data MongoDB interop module#15745

Open
codeconsole wants to merge 10 commits into
apache:8.0.xfrom
codeconsole:feat/mongo-spring-data-interop
Open

Add GORM for MongoDB / Spring Data MongoDB interop module#15745
codeconsole wants to merge 10 commits into
apache:8.0.xfrom
codeconsole:feat/mongo-spring-data-interop

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Depends on #15744 (stacked PR)

This is stacked on top of #15744 (opt-in MongoDB transactions). Until #15744 merges, the diff here also includes its commit — please review/merge #15744 first, after which this PR reduces to just the new interop module. The unified transaction here relies on the ClientSession introduced by #15744.

What

A new opt-in module, grails-data-mongodb-spring-data, that lets GORM for MongoDB and Spring Data MongoDB run side by side over the same MongoClient, database and codecs — and, within a single @Transactional method, the same MongoDB transaction.

When the module and spring-data-mongodb are on the classpath of a Spring Boot app that has a GORM MongoDatastore, it auto-configures over GORM's connection:

  • a MongoDatabaseFactory bound to GORM's client + default database (it does not close GORM's client),
  • a MongoTemplate (+ MappingMongoConverter) sharing the driver codec registry,
  • a primary transactionManager (GormSharedSessionMongoTransactionManager) that binds GORM's ClientSession into Spring Data, so a GORM save() and a MongoTemplate/repository write in one @Transactional method commit or roll back together.

Unified transactions require GORM server-side transactions (grails.mongodb.transactional = true, from #15744). Spring Data repositories are enabled the usual way via @EnableMongoRepositories, on a package separate from GORM @Entity classes.

Boundary

Only the connection, codecs and (within a transaction) the ClientSession are shared; the two object-mapping models stay separate (GORM maps @Entity; Spring Data maps its own documents). Sharing the session reaches Spring Data's package-private MongoResourceHolder, so a small helper lives in package org.springframework.data.mongodb — verified against the Spring Data MongoDB 5.x line in Spring Boot 4, class-path only (not JPMS module-path). A single flat transaction (PROPAGATION_REQUIRED) is supported; REQUIRES_NEW/NESTED are not.

Tests

  • UnifiedMongoTransactionSpec — GORM + MongoTemplate commit and roll back together on one session; coexistence read; sequential transactions without session/holder leaks; a Spring Data repository over the shared connection; auto-config wiring; the shared factory does not close GORM's client.
  • GormSpringDataSessionSupportSpec — coupling smoke test that fails loudly if the Spring Data internal shape changes.

Targets 8.0.x.

GORM for MongoDB previously treated a transaction as a client-side flush
boundary: pending writes were batched and flushed on commit, but each write
auto-committed individually and nothing rolled back when a later operation failed.

This adds real server-side transactions backed by a com.mongodb.client.ClientSession.
When grails.mongodb.transactional is enabled (default false), a GORM transaction
starts a ClientSession and MongoDB transaction and every read and write for the
session runs within it, committing or aborting atomically. A new MongoTransaction
drives the commit (retrying on an UnknownTransactionCommitResult) and the abort, and
closes the session afterwards.

The feature is opt-in and degrades gracefully: a standalone topology is detected at
runtime and falls back to the legacy flush-only behavior with a one-time warning.
Identifier generation for native Long ids is intentionally left non-transactional,
mirroring the semantics of database sequences.
@borinquenkid

Copy link
Copy Markdown
Member

Hi @codeconsole,

Please keep an eye on #15678, which modifies the way the MongoRegistry is being handled. It might be worth checking your diff against those updates, as this PR is downstream from 8.0.x-hibernate7.

Introduces an optional module that lets GORM for MongoDB and Spring Data MongoDB
run side by side over the same MongoClient, database and codecs, and - within a
single transaction - the same MongoDB ClientSession.

When the module and spring-data-mongodb are on the classpath of a Spring Boot
application that has a GORM MongoDatastore, SpringDataMongoGormAutoConfiguration
registers, over GORM's existing connection: a MongoDatabaseFactory bound to GORM's
client and default database (which does not close that client), a MongoTemplate and
MappingMongoConverter sharing the driver codec registry, and a primary
transactionManager.

That manager, GormSharedSessionMongoTransactionManager, extends GORM's
DatastoreTransactionManager and binds GORM's ClientSession into Spring Data's
thread-bound resources, so a GORM save() and a MongoTemplate/repository write in one
@transactional method commit or roll back together. Unified transactions require GORM
server-side transactions (grails.mongodb.transactional = true). The two object-mapping
models stay separate; only the connection, codecs and session are shared.
@codeconsole codeconsole force-pushed the feat/mongo-spring-data-interop branch from 36e36a5 to 342579f Compare June 17, 2026 23:16

@jdaugherty jdaugherty 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.

Whats the benefit for having Spring Data and GORM?

@codeconsole

Copy link
Copy Markdown
Contributor Author

This is for apps that already have or need both, where today the only option is two separate MongoClients with no transactional consistency between them. The module makes that coexistence correct: GORM and Spring Data share one MongoClient, database and codec registry, and — within a single @Transactional — the same MongoDB ClientSession, so a GORM save() and a MongoTemplate/repository write commit or roll back together.

Concrete cases: Spring-ecosystem libraries built on MongoTemplate/Spring Data repositories (Spring Batch, Spring Session, Spring Integration, Spring Security ACL); incremental migration in either direction without doubling connections; and reaching actively-developed Spring Data features for specific operations while keeping GORM for domain modeling.

It's opt-in — a separate module that pulls in spring-data-mongodb only when an app adds it; core stays driver-only and the mapping models stay separate.

@codeconsole

Copy link
Copy Markdown
Contributor Author

Thanks @borinquenkid — I checked #15678 against this PR's diff. It's Hibernate-only: the registry-scaling changes are all in grails-data-hibernate5/grails-data-hibernate7 (plus graphql and gradle config), with no changes to grails-data-mongodb, grails-datastore-core, or grails-datamapping-core. The only shared touchpoint is gradle/mongodb-test-config.gradle, which this PR doesn't modify, so it merges cleanly. I'll rebase onto 8.0.x and re-run the Mongo suites once #15678 lands, but there's no code overlap with the interop module here.

…tx manager

Move the Spring Boot auto-configuration registration from the non-standard
META-INF/services/spring/ location to META-INF/spring/, which is where Spring Boot's
ImportCandidates actually scans - otherwise the module never auto-configures in a real
application. Add a test that asserts the imports file is at the canonical location (and
not the old one) so the path cannot silently regress.

Also document that the unified manager is @primary (required to win over GORM's
mongoTransactionManager) and how to target the intended manager in an application that
mixes another persistence stack.
Verifies the auto-configuration through an ApplicationContextRunner so the conditions,
ordering and @ConditionalOnMissingBean back-off are actually exercised in a real context
(the existing specs only invoked the @bean methods directly): the interop beans wire when
a MongoDatastore is present, nothing activates without one, and an application-defined
mongoTemplate takes precedence. Runs offline.
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.29114% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.5005%. Comparing base (87c000a) to head (a2217c9).
⚠️ Report is 69 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...ails/datastore/mapping/mongo/MongoTransaction.java 51.0638% 18 Missing and 5 partials ⚠️
.../datastore/mapping/mongo/AbstractMongoSession.java 65.6250% 5 Missing and 6 partials ⚠️
...grails/datastore/mapping/mongo/MongoDatastore.java 50.0000% 9 Missing and 1 partial ⚠️
...g/grails/datastore/mapping/mongo/MongoSession.java 0.0000% 6 Missing ⚠️
...ta/GormSharedSessionMongoTransactionManager.groovy 80.0000% 0 Missing and 3 partials ⚠️
...ils/datastore/gorm/mongo/api/MongoStaticApi.groovy 77.7778% 1 Missing and 1 partial ⚠️
...ore/mapping/mongo/engine/MongoEntityPersister.java 0.0000% 2 Missing ⚠️
...ork/data/mongodb/GormSpringDataSessionSupport.java 80.0000% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@               Coverage Diff                @@
##             8.0.x     #15745         +/-   ##
================================================
+ Coverage         0   49.5005%   +49.5005%     
- Complexity       0      16730      +16730     
================================================
  Files            0       1951       +1951     
  Lines            0      92597      +92597     
  Branches         0      16160      +16160     
================================================
+ Hits             0      45836      +45836     
- Misses           0      39638      +39638     
- Partials         0       7123       +7123     
Files with missing lines Coverage Δ
...s/datastore/mapping/mongo/MongoCodecSession.groovy 76.2820% <100.0000%> (ø)
...tions/AbstractMongoConnectionSourceSettings.groovy 70.0000% <ø> (ø)
...ping/mongo/engine/MongoCodecEntityPersister.groovy 80.7860% <100.0000%> (ø)
...ails/datastore/mapping/mongo/query/MongoQuery.java 73.7864% <100.0000%> (ø)
...ngdata/SpringDataMongoGormAutoConfiguration.groovy 100.0000% <100.0000%> (ø)
...ork/data/mongodb/GormSpringDataSessionSupport.java 80.0000% <80.0000%> (ø)
...ils/datastore/gorm/mongo/api/MongoStaticApi.groovy 62.5000% <77.7778%> (ø)
...ore/mapping/mongo/engine/MongoEntityPersister.java 10.4478% <0.0000%> (ø)
...ta/GormSharedSessionMongoTransactionManager.groovy 80.0000% <80.0000%> (ø)
...g/grails/datastore/mapping/mongo/MongoSession.java 0.0000% <0.0000%> (ø)
... and 3 more

... and 1938 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.

# Conflicts:
#	grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java
@jdaugherty

Copy link
Copy Markdown
Contributor

Isn't the transaction support a requirement before this PR?

- setTimeout now throws TransactionUsageException for a non-default
  timeout instead of silently ignoring it. The server transaction is
  started before the manager applies a timeout, so it cannot be honored
  at this layer; DatastoreTransactionManager.doBegin catches it, rolls
  back the just-started ClientSession, and rethrows as
  CannotCreateTransactionException, so nothing leaks.
- All nine AbstractMongoSession driver helpers branch on
  hasActiveTransaction() rather than a raw clientSession null check, so a
  session that lingers after its transaction commits falls back to the
  session-less overload.
- Document on the MongoTransaction Javadoc that whole-transaction retry
  on TransientTransactionError is intentionally deferred: it requires
  re-executing the transaction body, which the Spring
  PlatformTransactionManager SPI cannot do.
- Add a spec asserting a per-transaction timeout is rejected and the
  datastore stays usable afterwards, and document the timeout behavior
  in advancedConfig.adoc.
Add an interop test asserting that a timeout on
GormSharedSessionMongoTransactionManager is refused
(CannotCreateTransactionException) before any Spring Data resource holder
is bound, that nothing persists on either stack, and that the manager
stays usable afterward. This pins the ordering guarantee that
super.doBegin runs — and can throw — before the ClientSession is shared
with Spring Data.
Reword the MongoTransaction Javadoc so it no longer reads as deferred
work: whole-transaction retry on a TransientTransactionError is left to
the application (as Spring Data MongoDB's own transaction manager does),
since re-running the transaction body would repeat its side effects.
Bring MongoTransaction and AbstractMongoSession in line with the reviewed
Layer 1 code: MongoTransaction.commit() now explicitly aborts the
server-side transaction on a failed commit rather than relying on
close() to do so implicitly, and the AbstractMongoSession clientSession
field carries its threading/nullability contract note. This makes the
interop branch's copy identical to apache#15744.
@testlens-app

testlens-app Bot commented Jul 1, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: a2217c9
▶️ Tests: 21110 executed
⚪️ Checks: 44/44 completed


Learn more about TestLens at testlens.app.

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.

3 participants