ali-jawad opened a new issue, #16740:
URL: https://github.com/apache/grails-core/issues/16740
### Summary
With GORM for Hibernate 5 in Grails 8.0.0, `save()` no longer sets the
back-reference of a new child in a bidirectional one-to-one where the child
`belongsTo` the owner. On 7.2.4 it did. The owner can be new or already
persisted. Depending on the mapping, the child is then saved with a null
in-memory back-reference, its insert fails (`hasOne`), or validation rejects it
(a non-nullable back-reference).
### Versions
- Broken: Grails 8.0.0, `org.apache.grails:grails-hibernate5-bom:8.0.0` →
`org.apache.grails.data:grails-data-hibernate5-core:8.0.0`, Hibernate ORM
5.6.15.Final (`hibernate-core-jakarta`)
- Works: Grails 7.2.4,
`org.apache.grails.data:grails-data-hibernate5-core:7.2.4`, the same Hibernate
ORM 5.6.15.Final
- The change first shipped in 8.0.0-M2. The `7.2.x` branch does not have it.
### Steps to reproduce
Use an app on GORM for Hibernate 5 with H2 on the test runtime classpath,
and add this spec to `src/test/groovy`:
```groovy
import grails.gorm.annotation.Entity
import grails.test.hibernate.HibernateSpec
class OneToOneBackReferenceSpec extends HibernateSpec {
List<Class> getDomainClasses() { [Author, Profile] }
void "saving the owner sets the back-reference of its new one-to-one
child"() {
given:
Profile profile = new Profile(bio: 'Writes about GORM')
Author author = new Author(name: 'Jane', profile: profile)
when:
author.save(flush: true)
then:
profile.author.is(author)
!author.hasErrors()
Profile.count() == 1
}
}
@Entity
class Author {
String name
Profile profile
static constraints = {
profile nullable: true
}
}
@Entity
class Profile {
String bio
static belongsTo = [author: Author]
}
```
### Expected
`save()` sets `profile.author` to `author`, as on 7.2.4, and both rows are
inserted.
### Actual
`profile.author` stays `null` after `save()`. What follows depends on the
mapping:
- **The shape above, with Grails 8's defaults** (properties nullable):
`save()` succeeds and both rows are stored with the correct foreign key. Only
the in-memory back-reference stays `null`, so `profile.author.is(author)` fails.
- **`static hasOne = [profile: Profile]`** (foreign key in the child): the
flush fails with `DataIntegrityViolationException`, NULL not allowed for column
`AUTHOR_ID`, on the insert into the child's table. The same happens for a
persisted owner with a new child, and for `hasOne` without `belongsTo`.
- **With `grails.gorm.default.nullable = false`** (or `nullable: false` on
the back-reference): the cascaded validation rejects the null `author`, so
`save()` returns `null`, and `failOnError: true` throws a `ValidationException`.
Inside a Grails application, the map constructor and `bindData` set the
back-reference through data binding, which hides the bug there. Plain
assignment (`author.profile = profile`) shows it.
### Cause
`save()` calls `HibernateRuntimeUtils.autoAssociateBidirectionalOneToOnes()`
before validation:
https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormInstanceApi.groovy#L130
For each owning-side bidirectional one-to-one, it skips the association
unless `proxyHandler.isInitialized(child, '<backReference>')` is true, and then
sets the back-reference if it is null. This method is the same in 7.2.4
([L125-L134](https://github.com/apache/grails-core/blob/ea4ddc768f362a77fceb926ced14491c18524ca3/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy#L125-L134))
and in 8.0.0:
https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy#L125-L134
`HibernateProxyHandler.isInitialized(Object, String)` reads the property and
passes its value to `isInitialized(Object)`. For a new child, that value is
`null`:
- **7.2.4:** `isInitialized(Object)` delegates to
`Hibernate.isInitialized(null)` ([handler
L47-L50](https://github.com/apache/grails-core/blob/ea4ddc768f362a77fceb926ced14491c18524ca3/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java#L47-L50)),
and that call returns `true` for `null`, which matches none of its
`instanceof` branches and reaches the final `else` ([Hibernate 5.6.15
L84-L101](https://github.com/hibernate/hibernate-orm/blob/e924c27e1259b0b5915819e9521d86fcb8164a46/hibernate-core/src/main/java/org/hibernate/Hibernate.java#L84-L101)).
- **8.0.0:** `isInitialized(Object)` returns `false` for `null`:
https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler.java#L54-L73
The `null` guard came in with 70fde10c8257d029d79beef9f02446cd9376ac74
during the Hibernate 7 work, which reached `8.0.x` through #15568.
`HibernateProxyHandler5Spec` pins it
([L132-L135](https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/proxy/HibernateProxyHandler5Spec.groovy#L132-L135)).
The guard is meant to skip an uninitialized proxy, but it now also skips the
one case the method acts on: a null back-reference.
No test caught it, because the one-to-one specs set the back-reference by
hand. Examples are the TCK `OneToOneSpec`
([L73-L74](https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-datamapping-tck/src/main/groovy/org/apache/grails/data/testing/tck/tests/OneToOneSpec.groovy#L73-L74)),
`BidirectionalOneToOneWithUniqueSpec` and `UniqueWithHasOneSpec`.
The same branch also handles `static hasOne = [profile: Profile]`, because
`ToOne.isOwningSide()` is true when the foreign key is in the child. Both
shapes reproduce (see *Actual*).
### Suggested fix
In `autoAssociateBidirectionalOneToOnes`, treat a null back-reference as
nothing to initialize. The Hibernate 7 module's copy of this method already has
no such guard
([L96-L125](https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/HibernateRuntimeUtils.groovy#L96-L125)),
and #16624 makes the same change to `MappingContextTraversableResolver` ("a
null value has nothing to initialize"):
```diff
def otherSidePropertyName = otherSide.getName()
- if (!proxyHandler.isInitialized(inverseObject,
otherSidePropertyName)) {
- continue
- }
-
def associationReflector =
mappingContext.getEntityReflector(association.associatedEntity)
def propertyValue =
associationReflector.getProperty(inverseObject, otherSidePropertyName)
if (propertyValue == null) {
```
A non-null back-reference is never overwritten, initialized or not, so the
guard only ever changed the outcome for `null`. The earlier
`isInitialized(target, propertyName)` check still keeps an uninitialized child
proxy from being loaded.
Another option is to restore the 7.x contract, `isInitialized(null) ==
true`, which `GroovyProxyFactory.isInitialized(Object, String)` still follows
([L153-L156](https://github.com/apache/grails-core/blob/081cd1c93ab753a234e111e2828f089ebdd810ee/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/proxy/GroovyProxyFactory.groovy#L153-L156)).
That reverses behaviour a spec now pins, so the fix in the caller looks
narrower. Either way, a spec that saves the owner without setting the
back-reference would guard against this regression.
### Workaround
Set the back-reference before saving the owner:
```groovy
profile.author = author
author.profile = profile
author.save()
```
A fix with tests is ready: it removes that guard, adds a spec covering both
shapes, subclasses, proxies and an owner that is already set, and passes the
full hibernate5 core suite including the TCK. I can open a PR once this is
acknowledged.
--
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]