matrei commented on PR #16297:
URL: https://github.com/apache/grails-core/pull/16297#issuecomment-5614755247

   Round 4
   
   ## Review Findings
   
   The latest update resolves all five findings from the previous round: 
proxy-safe `updateAll`, embedded exclusion, mapping-engine unidirectional 
collection and many-to-many reference coercion, collection-association encoding 
in `updateAll`, and the engine documentation wording. The focused tests pass.
   
   However, the new to-many branch in `updateAll` is broader than the normal 
persistence path it mirrors. It catches association kinds that normal 
persistence handles differently, and one of them is a regression of a common 
operation.
   
   ## Merge Blockers
   
   ### High: `updateAll` on a basic collection now throws `NullPointerException`
   
   References:
   
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoCodecSession.groovy:394-410`
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoSession.java:414-436`
   - 
`grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/Basic.java:38`
   
   `Basic` extends `ToMany`, so a collection of simple values such as:
   
   ```groovy
   class Note {
       String id
       String title
       List<String> labels = []
       static hasMany = [labels: String]
   }
   ```
   
   now enters the new to-many branch. A `Basic` association has no associated 
entity, so the reflector lookup dereferences `null`:
   
   ```groovy
   Note.where { title == 'note' }.updateAll(labels: ['y', 'z'])
   ```
   
   Codec engine:
   
   ```text
   java.lang.NullPointerException: Cannot invoke 
"org.grails.datastore.mapping.model.PersistentEntity.getReflector()" because 
the return value of "...castToType(Object, java.lang.Class)" is null
   ```
   
   Mapping engine:
   
   ```text
   java.lang.NullPointerException: Cannot invoke 
"org.grails.datastore.mapping.model.PersistentEntity.getName()" because 
"persistentEntity" is null
   ```
   
   Before this PR the same call sent the String list through `$set` and worked. 
The branch should be restricted to `OneToMany` and `ManyToMany` with a non-null 
associated entity, or skip any association whose associated entity is `null`. 
Both engines need a test for a basic collection in `updateAll`.
   
   ### High: `updateAll` on a bidirectional one-to-many corrupts the owning 
document in the codec engine
   
   References:
   
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoCodecSession.groovy:394-410`
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoSession.java:414-436`
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/codecs/PersistentEntityCodec.groovy:524,600-602`
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/AbstractMongoObectEntityPersister.java:455,480`
   
   Normal persistence only stores an id array for a to-many when it is 
unidirectional or many-to-many. For a bidirectional one-to-many the foreign key 
lives on the inverse side, and `OneToManyDecoder` does not consume a value for 
that field, it initializes a lazy collection instead.
   
   The new `updateAll` branch does not make that distinction. For:
   
   ```groovy
   class Parent {
       String id
       Set<Child> children
       static hasMany = [children: Child]
   }
   
   class Child {
       String id
       Parent parent
       static belongsTo = [parent: Parent]
   }
   ```
   
   this call:
   
   ```groovy
   Parent.where { name == 'bi-parent' }.updateAll(children: [child])
   ```
   
   writes a `children` array onto the parent document. The decoder then hits 
that unread field and every subsequent read of the parent fails:
   
   ```text
   org.bson.BsonInvalidOperationException: ReadBSONType can only be called when 
State is TYPE, not when State is VALUE.
        at 
org.grails.datastore.bson.codecs.BsonPersistentEntityCodec.decode(BsonPersistentEntityCodec.groovy:192)
   ```
   
   The child's `parent` reference is not updated either, so the relationship 
does not change. In the mapping engine the same call leaves a stray `children` 
array and the relationship is likewise unchanged.
   
   The bulk path should mirror the `shouldEncodeIds` rule in 
`OneToManyEncoder`: skip bidirectional one-to-many properties, or reject them 
with a clear exception, rather than writing a field normal persistence never 
writes or reads.
   
   ### Medium: Mapping-engine `updateAll` on a many-to-many writes to the wrong 
field
   
   References:
   
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoSession.java:414-436`
   - 
`grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/engine/AbstractMongoObectEntityPersister.java:388,414`
   
   The mapping engine stores and reads many-to-many ids under a suffixed key:
   
   ```java
   manyToMany.getName() + "_$$manyToManyIds"
   ```
   
   The new `updateAll` branch writes the encoded ids under the plain 
association name. After:
   
   ```groovy
   ((MongoSession) session).updateAll(criteria, [rights: [right2]])
   ```
   
   the document contains both `rights_$$manyToManyIds` with the old id and 
`rights` with the new id, and reading the entity back still returns the old 
association. The update is silently a no-op.
   
   The mapping engine is deprecated, but this commit specifically claims to 
encode collection associations for it, and `MappingEngineStringIdStorageSpec` 
has no many-to-many or to-many `updateAll` coverage. Either use the same key as 
`setManyToMany` or document that many-to-many bulk updates are unsupported on 
this engine.
   
   ## Observations
   
   - The mapping engine's unidirectional one-to-many read path resolves coerced 
ObjectId keys correctly, and `updateAll(tags: [...])` on it works. Neither is 
covered by a test in `MappingEngineStringIdStorageSpec`, which only checks the 
raw document. A read-back assertion would guard the `retrieveAllEntities` 
conversion.
   - The `EmbeddedCollection` exclusion in both to-many branches is redundant, 
since `EmbeddedCollection` extends `Association` directly rather than `ToMany`. 
Harmless, but the comment implies a case that cannot occur.
   
   ## Resolved Findings
   
   - `updateAll` extracts ids from lazy proxies in both engines.
   - Embedded to-one associations are excluded from id normalization and are 
written as subdocuments.
   - Mapping-engine unidirectional collection and many-to-many references are 
stored in the target's `_id` type.
   - Ordinary unidirectional and many-to-many collections in `updateAll` are 
encoded as stored ids in the codec engine.
   - Engine documentation wording is consistent with the implementation.
   
   ## Verification
   
   The following focused tests passed:
   
   ```text
   ./gradlew :grails-data-mongodb-core:test \
     --tests 
'org.grails.datastore.gorm.mongo.bugs.MappingEngineStringIdStorageSpec' \
     --tests 
'org.grails.datastore.gorm.mongo.bugs.StringIdAssociationStorageSpec' \
     --tests 
'org.grails.datastore.gorm.mongo.bugs.StringIdWithObjectIdStorageSpec' \
     --tests 
'org.grails.datastore.gorm.mongo.bugs.StringIdDefaultStoredAsConfigSpec' \
     --tests 'org.grails.datastore.gorm.mongo.bugs.MongoIdCoercionSpec' \
     --no-daemon
   ```
   
   The three findings above were reproduced with a temporary probe spec against 
both engines, which was removed afterwards.
   
   The complete `grails-data-mongodb-core` test task also passed:
   
   | Tests | Failures | Errors | Skipped |
   | --- | --- | --- | --- |
   | 715 | 0 | 0 | 45 |
   
   `./gradlew :grails-data-mongodb-core:check -x test` passed as well, with no 
Checkstyle or CodeNarc violations.
   
   The basic-collection regression and the bidirectional one-to-many corruption 
should be fixed and covered with tests in both engines before approval. The 
mapping-engine many-to-many field mismatch should be fixed or explicitly 
documented as unsupported.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to