Give read-only transactions the same meaning on Mongo as on Hibernate - #16214
Open
codeconsole wants to merge 4 commits into
Open
Give read-only transactions the same meaning on Mongo as on Hibernate#16214codeconsole wants to merge 4 commits into
codeconsole wants to merge 4 commits into
Conversation
@readonly means different things depending on the datastore. On Hibernate it genuinely suppresses the flush: // GrailsHibernateTransactionManager:55 if (definition.isReadOnly()) { holder.session.setHibernateFlushMode(FlushMode.MANUAL) } On the DatastoreTransactionManager path there was no equivalent. doBegin set FlushModeType.COMMIT, which is already the Session default and is as strict as the JPA enum gets, and doCommit's `if (!status.isReadOnly())` guard was then defeated by transaction.commit() — both MongoTransaction and SessionOnlyTransaction flush unconditionally. A read-only transaction therefore wrote whatever the surrounding session had queued. Session.beginTransaction(TransactionDefinition) already existed for this; AbstractSession discarded the argument. It now passes the definition to an overridable beginTransactionInternal(TransactionDefinition) whose default delegates to the no-arg version, so datastores that do not override it — Neo4j and the simple map datastore — keep exactly the path they had. Mongo overrides it, and both transaction types decline to flush when the definition is read-only. A read-only transaction has no pending operations of its own, so the only writes this suppresses are ones a read had no business flushing. Read-write commits are unchanged, which the added tests assert alongside the read-only case on both the server-transaction and session-only paths.
codeconsole
force-pushed
the
fix/readonly-no-flush-8.0.x
branch
from
August 24, 2026 23:50
fd1f4f3 to
05fb17e
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16214 +/- ##
==================================================
+ Coverage 54.7365% 54.7484% +0.0119%
- Complexity 20470 20479 +9
==================================================
Files 2103 2103
Lines 101077 101086 +9
Branches 17928 17930 +2
==================================================
+ Hits 55326 55343 +17
+ Misses 37876 37867 -9
- Partials 7875 7876 +1
🚀 New features to boost your workflow:
|
This was referenced Aug 25, 2026
Passing the definition down left both transaction types with the constructor they had before read-only existed, and nothing in the tree calls either one now. MongoTransaction's has only ever appeared in the 8.0.0 milestones: apache#15744 added the class as an internal replacement for SessionOnlyTransaction with AbstractMongoSession as its only caller, and the Spring Data interop that followed in apache#15745 takes the ClientSession off the session rather than constructing a transaction of its own. It is removed rather than carried into the release. SessionOnlyTransaction's shipped in 7.0.x and in GORM before that, where an out-of-tree datastore may be constructing it, so it is deprecated for removal in favour of the constructor that states whether the transaction is read-only instead of assuming it is not.
GormSharedSessionMongoTransactionManager extends DatastoreTransactionManager, so it inherited the read-only handling without a specification naming it. A read-only transaction there now discards a queued GORM write while a MongoTemplate write, which went straight into the shared ClientSession, still commits: one transaction, two write paths, and only one of them answers to the flag. The existing unified specifications all save with flush: true, so none of them would notice the flush on commit going away. Both new cases save without flushing. The read-only one fails without the change and the read-write one holds its scope, and neither needs Docker.
✅ All tests passed ✅🏷️ Commit: f25990c Learn more about TestLens at testlens.app/docs. |
codeconsole
requested review from
borinquenkid,
jdaugherty,
matrei and
sbglasius
September 9, 2026 23:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
@ReadOnlymeans different things depending on which datastore is underneath.On Hibernate it genuinely suppresses the flush:
On the
DatastoreTransactionManagerpath there is no equivalent.doBeginsetsFlushModeType.COMMIT— already theSessiondefault, and as strict as the JPA enum gets, since it has noMANUAL/NEVER— anddoCommit's guard is then defeated by the transaction's own commit:Both implementations flush unconditionally —
MongoTransaction.commit()andSessionOnlyTransaction.commit(). So a read-only transaction writes whatever the surrounding session had queued.That matters because a read-only transaction has no pending operations of its own. Anything it flushes belongs to the caller's session, and gets written at a moment the caller did not choose. The sharp version is a read taken during a flush — a referential check in a validator, or a
beforeInserthook — where the commit re-validates the entity being saved and the validator reads again, recursing until the stack is gone.Change
Session.beginTransaction(TransactionDefinition)already existed for exactly this;AbstractSessiondiscarded the argument:It now passes the definition to an overridable
beginTransactionInternal(TransactionDefinition)whose default delegates to the existing no-arg method, so datastores that do not override it — Neo4j and the simple map datastore — keep exactly the path they had today. Mongo overrides it, andMongoTransaction/SessionOnlyTransactiondecline to flush when the definition is read-only.Read-write commits are untouched.
Tests
Three added, on both paths — server-side transactions (
MongoTransactionSpec) and the session-only fallback (MongoTransactionDisabledSpec):Both read-only tests fail on
8.0.xwithout the source change and pass with it; the read-write test passes either way. Full:grails-datastore-core:test,:grails-data-mongodb-core:testand:grails-data-simple:testare green.Notes
Independent of #16212, which removes a
@ReadOnlyfromGormServicein scaffolding. Neither depends on the other; either can land alone. #16212 is where this surfaced.This is a behaviour change: an application that writes inside an
@ReadOnlymethod on aDatastoreTransactionManagerdatastore gets those writes at commit today, and would not after this. Hibernate already behaves that way, so this makes the two consistent rather than introducing a new rule. If it should wait for a major, or land with a warning when a read-only commit finds pending operations, I can rework it.