Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
e88cb0b
Keep collection properties dirty-checked after reassignment; track it…
codeconsole Aug 31, 2026
a2e6292
Cover the remaining patch lines flagged by Codecov
codeconsole Sep 1, 2026
708aa2e
Merge branch '8.0.x' into fix/dirty-checking-collection-tracking-8.0.x
codeconsole Sep 2, 2026
a81e0d5
Default String id storage to ObjectId and fix association id coercion
codeconsole Sep 2, 2026
2ca0e4c
Fix checkstyle OperatorWrap violation in MongoQuery
codeconsole Sep 2, 2026
87219f1
Add unit tests for id coercion and association storage
codeconsole Sep 3, 2026
0f1bc9b
Correct idGeneration docs that still described String storage as the …
codeconsole Sep 3, 2026
5bf4e5c
Delete documents whose assigned String id is empty
codeconsole Sep 3, 2026
19a146a
Encode to-one association ids consistently in updateAll
codeconsole Sep 3, 2026
9a8506d
Apply id coercion inside negations and other junctions
codeconsole Sep 3, 2026
ded9ea7
Coerce association ids in IN criteria
codeconsole Sep 3, 2026
0fa6561
Store and query _id consistently in the non-codec engine
codeconsole Sep 3, 2026
08d1902
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 3, 2026
41b9230
Apply the ObjectId default to the codec engine only
codeconsole Sep 4, 2026
4563be7
Normalise updateAll association values into a copy of the caller's map
codeconsole Sep 4, 2026
3a99093
Store and query _id through the id mapping in the non-codec engine
codeconsole Sep 4, 2026
60c3671
Write association references through the id mapping in the non-codec …
codeconsole Sep 4, 2026
ce89107
Encode to-one associations in the non-codec engine's updateAll, and d…
codeconsole Sep 4, 2026
425cdc0
Deprecate the non-codec persistence engine
codeconsole Sep 4, 2026
b1d19ef
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 4, 2026
a9a1f1e
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 5, 2026
eaeff04
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 8, 2026
1630a25
Extract association ids from proxies in updateAll
codeconsole Sep 8, 2026
ded7116
Exclude embedded associations from updateAll id normalization
codeconsole Sep 8, 2026
564b61b
Coerce unidirectional collection and many-to-many references in the n…
codeconsole Sep 8, 2026
463e1e7
Encode collection associations in updateAll
codeconsole Sep 8, 2026
1756eae
Correct contradictory wording about the engine setting
codeconsole Sep 8, 2026
2a98407
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 8, 2026
3973ca3
Test and document generated compiler script producer wiring
codeconsole Sep 9, 2026
d9a215b
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 9, 2026
da7dad3
Merge branch '8.0.x' into fix/dirty-checking-collection-tracking-8.0.x
codeconsole Sep 9, 2026
06cb682
Merge remote-tracking branch 'upstream/8.0.x' into fix/dirty-checking…
codeconsole Sep 9, 2026
8581d56
Address review: exact-class rewrap gate, Map wrapper coverage, Sorted…
codeconsole Sep 9, 2026
d164af8
Merge remote-tracking branch 'origin/fix/dirty-checking-collection-tr…
codeconsole Sep 9, 2026
38f371c
Return Long from count() instead of narrowing to Integer
codeconsole Sep 8, 2026
47f0707
Return Long from the GraphQL count field too
codeconsole Sep 8, 2026
4b2ab65
Correct the rationale for the count width in the upgrade note
codeconsole Sep 9, 2026
04f871c
Address review feedback on the count() change
codeconsole Sep 9, 2026
2d7fdf0
Preserve plain-file compiler script producer dependencies
codeconsole Sep 9, 2026
9795e8d
Merge upstream 8.0.x and preserve both upgrade guide additions
codeconsole Sep 9, 2026
af45fc2
Fix the duplicate section number in the 8.0 upgrade notes
codeconsole Sep 9, 2026
ea9c585
Retain compiler script regression fixture and test build cache restor…
codeconsole Sep 9, 2026
3ba7fb5
Delegate wrapper equals/hashCode to the target collection
codeconsole Sep 9, 2026
dc6a9cf
Merge remote-tracking branch 'upstream/8.0.x' into feat/mongo-objecti…
codeconsole Sep 10, 2026
7deb7dc
Restrict updateAll collection normalization to real reference collect…
codeconsole Sep 10, 2026
0dfbe06
Reject bidirectional one-to-many in updateAll
codeconsole Sep 10, 2026
9528ba1
Write many-to-many updateAll values under the key the non-codec engin…
codeconsole Sep 10, 2026
b1c52c2
Reject hasOne associations in updateAll
codeconsole Sep 10, 2026
8707742
Match the encoders' null and many-to-many representation in updateAll
codeconsole Sep 10, 2026
a2ac517
Track the SequencedCollection methods and the live collection views
codeconsole Sep 10, 2026
f11e855
Re-bind a collection borrowed from another entity to the entity it is…
codeconsole Sep 10, 2026
98844a0
Merge remote-tracking branch 'upstream/8.0.x' into pr-16327
jdaugherty Sep 11, 2026
6cfdb52
Cover an ad-hoc outputs.file producer and point the guide at the vali…
jdaugherty Sep 11, 2026
8f497f5
Carry Long through the JSON API pagination and pin it in the specs
codeconsole Sep 11, 2026
e59ca12
Merge branch '8.0.x' of https://github.com/apache/grails-core into fe…
codeconsole Sep 11, 2026
377d709
Merge upstream/8.0.x into test/compiler-script-producer-wiring
codeconsole Sep 12, 2026
120a907
Merge upstream/8.0.x into feat/gorm-count-returns-long-8.0.x
codeconsole Sep 12, 2026
463767a
Merge origin/test/compiler-script-producer-wiring into test/compiler-…
codeconsole Sep 12, 2026
c8f5c8f
Fix the count documentation left behind by the Long change
codeconsole Sep 13, 2026
a46b6f3
Cover ad-hoc compiler script provider wiring
codeconsole Sep 13, 2026
0ae70ce
Re-wrap a to-many association replaced by a plain collection
codeconsole Sep 13, 2026
f799282
Keep wrapper equals reflexive when the target has identity equality
codeconsole Sep 13, 2026
a7f7edb
Stop nesting a new DirtyCheckingMap on every save
codeconsole Sep 13, 2026
2dade74
Drop the redundant iterator() overrides on the wrapper subclasses
codeconsole Sep 13, 2026
534956d
Drop the DirtyCheckingMap import left behind by the wrap(Map) change
codeconsole Sep 13, 2026
bc0b795
Pin the exact-class rule for the datastore to-many collections
codeconsole Sep 13, 2026
dcfa7be
Stop an in-memory embedded MongoDB only once
codeconsole Sep 13, 2026
be9fd0e
Stop the servers the replica set specification starts
codeconsole Sep 13, 2026
2c7455a
Leave the MongoDB suites out of the Neo4j jobs
codeconsole Sep 13, 2026
9051523
Time out the test jobs well before GitHub's six-hour default
codeconsole Sep 13, 2026
b378d75
Leave the Hibernate and Redis suites out of the Neo4j jobs
codeconsole Sep 13, 2026
42521c0
Correct why EmbeddedMongoInitializerSpec stops its own servers
codeconsole Sep 13, 2026
7dbf7e1
Run grails-redis's own tests in the Redis jobs
codeconsole Sep 13, 2026
40fe646
Regroup the spec imports and assert the subclass case is flagged
codeconsole Sep 13, 2026
18f5b07
Merge pull request #16327 from codeconsole/test/compiler-script-produ…
codeconsole Sep 14, 2026
df000a4
Merge branch '8.0.x' of https://github.com/apache/grails-core into fe…
codeconsole Sep 14, 2026
8c32742
Merge pull request #16323 from codeconsole/feat/gorm-count-returns-lo…
codeconsole Sep 14, 2026
c011bdf
Merge branch '8.0.x' into fix/dirty-checking-collection-tracking-8.0.x
codeconsole Sep 14, 2026
3067d0a
Merge pull request #16282 from codeconsole/fix/dirty-checking-collect…
codeconsole Sep 14, 2026
e1a84ec
Merge 8.0.x into 8.1.x
codeconsole Sep 14, 2026
38f32ad
Merge branch '8.0.x' into fix/embedded-mongo-shutdown-hang-8.0.x
codeconsole Sep 14, 2026
44750eb
Drop a dead type check in the updateAll bidirectional one-to-many guard
codeconsole Sep 14, 2026
55ee2e3
Accept an identifier in place of an association instance in updateAll
codeconsole Sep 14, 2026
a173953
Document updateAll and the association kinds it supports
codeconsole Sep 14, 2026
7d302c5
Name the plain to-many null case accurately, and cover the DBRef null…
codeconsole Sep 14, 2026
983fd08
Cover many-to-many updateAll on the codec engine
codeconsole Sep 14, 2026
87d38b3
Cover hasOne updateAll rejection on the non-codec engine
codeconsole Sep 14, 2026
f3cb91e
Merge branch '8.0.x' into feat/mongo-objectid-default-8.0.x
codeconsole Sep 14, 2026
1105735
Merge pull request #16343 from codeconsole/merge/8.0.x-into-8.1.x
codeconsole Sep 15, 2026
b449d53
Merge pull request #16297 from codeconsole/feat/mongo-objectid-defaul…
jdaugherty Sep 15, 2026
b4aa3c6
Address embedded MongoDB shutdown review findings
codeconsole Sep 15, 2026
35220cb
Merge branch '8.0.x' into fix/embedded-mongo-shutdown-hang-8.0.x
codeconsole Sep 15, 2026
37f8ca6
Merge pull request #16338 from codeconsole/fix/embedded-mongo-shutdow…
codeconsole Sep 15, 2026
916fefe
Merge 8.0.x into 8.1.x
codeconsole Sep 15, 2026
4e0c3f8
Merge pull request #16345 from codeconsole/merge/8.0.x-into-8.1.x
codeconsole Sep 16, 2026
8f597f0
Merge 8.1.x into 9.0.x
codeconsole Sep 19, 2026
efc506b
fix: keep the tracking reversed() override on the collection wrappers
codeconsole Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ jobs:
include-hidden-files: true
path: grails-forge/tmp1/cli/**/*
if-no-files-found: 'error'
# Every test job from here through neo4jFunctional sets timeout-minutes to twice the slowest run that
# finished on its own, passed or failed, over the 160 CI runs of 2026-09-03 to 2026-09-13, rounded up to
# the half hour. Without a limit, a test JVM that never exits holds its job for GitHub's six-hour default.
functional:
name: "Functional Tests (${{ matrix.job_name }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
Expand Down Expand Up @@ -527,6 +530,7 @@ jobs:
security:
name: "Spring Security Tests (${{ matrix.job_name }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
timeout-minutes: 150 # slowest run: 71 min
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -572,6 +576,7 @@ jobs:
securityConfig:
name: "Spring Security Config Functional Tests"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
timeout-minutes: 90 # slowest run: 39 min
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -611,6 +616,7 @@ jobs:
casSecurityConfig:
name: "Spring Security CAS Functional Tests"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
timeout-minutes: 90 # slowest run: 35 min
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -652,6 +658,7 @@ jobs:
redis:
name: "Redis Tests"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
timeout-minutes: 180 # slowest run: 86 min
runs-on: ubuntu-24.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -687,6 +694,7 @@ jobs:
mongodbFunctional:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
name: "Mongodb Functional Tests (Java ${{ matrix.java }}, MongoDB ${{ matrix.mongodb-version }}, indy=${{ matrix.indy }})"
timeout-minutes: 150 # slowest run: 70 min
runs-on: ubuntu-24.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -750,6 +758,7 @@ jobs:
hibernate5Functional:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
name: "Hibernate5 Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
timeout-minutes: 120 # slowest run: 58 min
runs-on: ubuntu-24.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -799,6 +808,7 @@ jobs:
hibernate7Functional:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
name: "Hibernate7 Functional Tests (${{ matrix.job_name }})"
timeout-minutes: 210 # slowest run: 104 min, which failed; the slowest that passed took 86
runs-on: ubuntu-24.04
strategy:
fail-fast: false
Expand Down Expand Up @@ -869,6 +879,7 @@ jobs:
neo4jFunctional:
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
name: "Neo4j Functional Tests (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
timeout-minutes: 180 # slowest run: 90 min
runs-on: ubuntu-24.04
timeout-minutes: 105
strategy:
Expand Down
1 change: 1 addition & 0 deletions gradle/hibernate5-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ tasks.withType(Test).configureEach {
'onlyHibernate7Tests',
'skipHibernate5Tests',
'onlyMongodbTests',
'onlyNeo4jTests',
'onlyRedisTests',
'onlySpringSecurityTests',
'onlyCoreTests',
Expand Down
1 change: 1 addition & 0 deletions gradle/hibernate7-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tasks.withType(Test).configureEach {
'onlyHibernate5Tests',
'skipHibernate7Tests',
'onlyMongodbTests',
'onlyNeo4jTests',
'onlyRedisTests',
'onlySpringSecurityTests',
'onlyCoreTests',
Expand Down
1 change: 1 addition & 0 deletions gradle/mongodb-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tasks.withType(Test).configureEach {
'onlyHibernate5Tests',
'onlyHibernate7Tests',
'skipMongodbTests',
'onlyNeo4jTests',
'onlyRedisTests',
'onlySpringSecurityTests',
'onlyCoreTests',
Expand Down
1 change: 1 addition & 0 deletions gradle/redis-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tasks.withType(Test).configureEach {
'onlyHibernate5Tests',
'onlyHibernate7Tests',
'onlyMongodbTests',
'onlyNeo4jTests',
'onlySpringSecurityTests',
'onlyCoreTests',
'skipTests'
Expand Down
1 change: 1 addition & 0 deletions gradle/spring-security-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tasks.withType(Test).configureEach {
'onlyHibernate5Tests',
'onlyHibernate7Tests',
'onlyMongodbTests',
'onlyNeo4jTests',
'onlyRedisTests',
'onlyCoreTests',
'skipTests'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ class Schema {
DataFetcher countFetcher = dataFetcherManager.getReadingFetcher(entity, COUNT).orElse(new CountEntityDataFetcher(entity))

final String countFieldName = namingConvention.getCount(entity)
final GraphQLOutputType countOutputType = (GraphQLOutputType) typeManager.getType(Integer)
final GraphQLOutputType countOutputType = (GraphQLOutputType) typeManager.getType(Long)

GraphQLFieldDefinition.Builder queryCount = newFieldDefinition()
.name(countFieldName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ import org.grails.gorm.graphql.fetcher.ReadingGormDataFetcher
*/
@CompileStatic
@InheritConstructors
class CountEntityDataFetcher extends DefaultGormDataFetcher<Integer> implements ReadingGormDataFetcher<Integer> {
class CountEntityDataFetcher extends DefaultGormDataFetcher<Long> implements ReadingGormDataFetcher<Long> {

protected Integer queryCount() {
protected Long queryCount() {
staticApi.count()
}

@Override
Integer get(DataFetchingEnvironment environment) {
(Integer) withTransaction(true) {
Long get(DataFetchingEnvironment environment) {
(Long) withTransaction(true) {
queryCount()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.grails.gorm.graphql

import graphql.scalars.ExtendedScalars
import graphql.schema.*
import org.grails.datastore.mapping.config.Settings
import org.grails.datastore.mapping.core.DatastoreUtils
Expand Down Expand Up @@ -67,6 +68,15 @@ class SchemaSpec extends Specification implements GraphQLSchemaSpec {
prefix + normalizeType(type)
}

void "count query fields use the 64-bit Long scalar"() {
given:
List<GraphQLFieldDefinition> countFields = queryType.fieldDefinitions.findAll { it.name.endsWith('Count') }

expect: 'the schema matches what count() returns, and the Long totalCount already in paged results'
!countFields.isEmpty()
countFields.every { it.type == ExtendedScalars.GraphQLLong }
}

void "test ComplexOperation"() {
given:
GraphQLObjectType type = schema.getType('AwesomeType')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ abstract class AbstractHibernateGormStaticApi<D> extends GormStaticApi<D> {
}

@Override
Integer count() {
(Integer) hibernateTemplate.execute({ Session session ->
Long count() {
(Long) hibernateTemplate.execute({ Session session ->
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder()
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(persistentEntity.javaClass)))
Expand All @@ -211,7 +211,7 @@ abstract class AbstractHibernateGormStaticApi<D> extends GormStaticApi<D> {
}
hibernateTemplate.applySettings(criteria)
def result = hibernateHqlQuery.singleResult()
Number num = result == null ? 0 : (Number)result
Number num = result == null ? 0L : (Number)result
return num
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
}

@Override
Integer count() {
Long count() {
String entity = persistentEntity.name
doSingleInternal("select count(*) from $entity" as String, [:], [], [:], false) as Integer
doSingleInternal("select count(*) from $entity" as String, [:], [], [:], false) as Long
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import org.bson.conversions.Bson

import org.grails.datastore.bson.codecs.PropertyEncoder
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
import org.grails.datastore.mapping.dirty.checking.DirtyCheckingMap
import org.grails.datastore.mapping.dirty.checking.DirtyCheckingSupport
import org.grails.datastore.mapping.engine.EntityAccess
import org.grails.datastore.mapping.engine.internal.MappingUtils
Expand Down Expand Up @@ -75,7 +74,7 @@ class BasicCollectionTypeEncoder implements PropertyEncoder<Basic> {
def propertyName = property.name
parentAccess.setPropertyNoConversion(
propertyName,
new DirtyCheckingMap(value, parent, propertyName)
DirtyCheckingSupport.wrap(value, parent, propertyName)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import groovy.transform.CompileStatic

import jakarta.persistence.FlushModeType

import com.mongodb.DBRef
import com.mongodb.WriteConcern
import com.mongodb.bulk.BulkWriteResult
import com.mongodb.client.FindIterable
Expand Down Expand Up @@ -59,9 +60,13 @@ import org.grails.datastore.mapping.model.MappingContext
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.config.GormProperties
import org.grails.datastore.mapping.model.types.Association
import org.grails.datastore.mapping.model.types.Embedded
import org.grails.datastore.mapping.model.types.ManyToMany
import org.grails.datastore.mapping.model.types.OneToMany
import org.grails.datastore.mapping.model.types.ToOne
import org.grails.datastore.mapping.mongo.engine.MongoCodecEntityPersister
import org.grails.datastore.mapping.mongo.engine.MongoEntityPersister
import org.grails.datastore.mapping.mongo.config.MongoAttribute
import org.grails.datastore.mapping.mongo.engine.MongoIdCoercion
import org.grails.datastore.mapping.mongo.engine.codecs.PersistentEntityCodec
import org.grails.datastore.mapping.mongo.query.MongoQuery
Expand Down Expand Up @@ -200,7 +205,9 @@ class MongoCodecSession extends AbstractMongoSession {
if (delete.vetoed) continue

final Object k = coerceIdToStoredType(delete.nativeKey, persistentEntity)
if (k) {
// Groovy truthiness would skip an empty assigned String id, which is a
// valid BSON _id -- the document would silently survive the delete.
if (k != null) {
nativeKeys << k
final List cascadeOperations = delete.cascadeOperations
addPostFlushOperations(cascadeOperations)
Expand Down Expand Up @@ -337,23 +344,111 @@ class MongoCodecSession extends AbstractMongoSession {
}
}

/**
* The identifier an updateAll association value stands for. A lazy proxy keeps its id in the
* proxy handler, so -- like ToOneEncoder -- the proxy factory is asked first. An instance of
* the associated class is reflected. Anything else is taken to be the identifier itself, so
* updateAll(project: project) and updateAll(project: project.id) write the same value.
*/
private Object identifierOf(Object value, PersistentEntity associatedEntity) {
def proxyFactory = mappingContext.proxyFactory
if (proxyFactory.isProxy(value)) {
return proxyFactory.getIdentifier(value)
}
if (associatedEntity.javaClass.isInstance(value)) {
return associatedEntity.reflector.getIdentifier(value)
}
return value
}

@Override
long updateAll(QueryableCriteria criteria, Map<String, Object> properties) {
final PersistentEntity entity = criteria.persistentEntity
final Document nativeQuery = buildNativeDocumentQueryFromCriteria(criteria, entity)
final MongoCollection collection = getCollection(entity)
final updateOptions = new UpdateOptions()
updateOptions.upsert(false)
// Normalise into a copy: the caller's map is theirs, and may be immutable. Writing the
// encoded reference back into it replaced their domain object with an ObjectId or
// DBRef, and threw UnsupportedOperationException for a Map.of/singletonMap argument.
Map<String, Object> updateProperties = new LinkedHashMap<String, Object>(properties)
for (Association association in entity.associations) {
String associationName = association.name
if (association instanceof ToOne && properties.containsKey(associationName)) {
def value = properties.get(associationName)
// Embedded extends ToOne, but an embedded value is a subdocument with no
// identity of its own -- normal persistence encodes it through the embedded
// path, not ToOneEncoder. Reflecting an id from one yields null.
// hasOne keeps the foreign key on the child, so ToOneEncoder writes nothing on the
// owner for it -- see its !isForeignKeyInChild() guard. Normalizing it here would
// put an id field on a document that never carries one.
if (association instanceof ToOne && !(association instanceof Embedded)
&& ((ToOne) association).isForeignKeyInChild()
&& updateProperties.containsKey(associationName)) {
throw new UnsupportedOperationException(
"Cannot updateAll the hasOne association [${entity.name}.${associationName}]: " +
"its foreign key is held by [${association.associatedEntity?.name}], " +
'so update the inverse side instead')
}
if (association instanceof ToOne && !(association instanceof Embedded)
&& updateProperties.containsKey(associationName)) {
def value = updateProperties.get(associationName)
if (value != null) {
properties.put(associationName, association.associatedEntity.reflector.getIdentifier(value))
// Write the reference exactly as ToOneEncoder does on the normal
// persistence path: in the target's stored _id type, as a DBRef where the
// mapping asks for one. Otherwise a bulk update leaves a reference that
// association queries and external clients cannot match.
def associatedEntity = association.associatedEntity
def associationId = MongoIdCoercion.coerceIdToStoredType(
identifierOf(value, associatedEntity), associatedEntity)
MongoAttribute attr = (MongoAttribute) association.mapping.mappedForm
if (attr?.isReference()) {
updateProperties.put(associationName,
new DBRef(getCollectionName(associatedEntity), associationId))
}
else {
updateProperties.put(associationName, associationId)
}
}
}
// A bidirectional one-to-many keeps its foreign key on the inverse side, so there
// is no field on this document to update -- OneToManyEncoder's shouldEncodeIds
// skips it for the same reason, and the decoder never reads one. Left in the $set
// the value is written as raw subdocuments and the owner stops decoding, so this
// says so rather than corrupting the document or silently doing nothing.
// ManyToMany extends ToMany, not OneToMany, so it never reaches this branch.
else if (association instanceof OneToMany && association.isBidirectional()
&& updateProperties.containsKey(associationName)) {
throw new UnsupportedOperationException(
"Cannot updateAll the bidirectional one-to-many [${entity.name}.${associationName}]: " +
"its foreign key is held by [${association.associatedEntity?.name}], " +
'so update the inverse side instead')
}
// Unidirectional OneToMany and ManyToMany carry a collection of associated instances.
// Normal persistence stores their ids -- DBRefs where the mapping asks for it -- so
// the bulk path does the same rather than sending domain objects through $set.
// Only those two kinds: Basic also extends ToMany but is a collection of simple
// values with no associated entity, and must pass through untouched.
else if ((association instanceof OneToMany || association instanceof ManyToMany)
&& association.associatedEntity != null
&& updateProperties.containsKey(associationName)) {
def value = updateProperties.get(associationName)
if (value instanceof Collection) {
def associatedEntity = association.associatedEntity
MongoAttribute attr = (MongoAttribute) association.mapping.mappedForm
def ids = value.collect { element ->
element == null ? null :
MongoIdCoercion.coerceIdToStoredType(identifierOf(element, associatedEntity), associatedEntity)
}
// Exactly OneToManyEncoder's shape: nulls are dropped before wrapping,
// never turned into DBRef(collection, null), and a non-reference list
// keeps whatever the caller passed.
def encoded = attr?.isReference()
? ids.findAll { it != null }.collect { new DBRef(getCollectionName(associatedEntity), it) }
: ids
updateProperties.put(associationName, encoded)
}
}
}
final UpdateResult updateResult = updateMany(collection, nativeQuery, new Document(MONGO_SET_OPERATOR, properties), updateOptions)
final UpdateResult updateResult = updateMany(collection, nativeQuery, new Document(MONGO_SET_OPERATOR, updateProperties), updateOptions)
if (updateResult.wasAcknowledged()) {
try {
return updateResult.modifiedCount
Expand Down
Loading
Loading