apache / apache/grails-core

Creating domain from properties of other domain fails in application and unit tests

Open
#9,544 5 comments 3 reactions 0 assignees View on GitHub
status: acknowledged
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

The common way to create copy of existing domain object in Grails 2 was to use:

```
new Domain(domain.properties)
```

This does not work in Grails 3 and gives a lot of problems with app upgrade - especially when domain has associations.

Assume we have two domains:

```
class DomainOne {
DomainTwo domainTwo
Integer quantity

static hasMany = [domainsTwo: DomainTwo]
}
class DomainTwo {
String name
}
```

if we try to create new `DomainOne` from existing `DomainOne` using code like:

```
DomainTwo domainTwo = new DomainTwo(name: "saved domain").save(failOnError: true)
DomainOne domainOne = new DomainOne(domainTwo: domainTwo, quantity: 10).save(failOnError: true)
DomainOne copiedDomain = new DomainOne(domainOne.properties)
copiedDomain.save(failOnError: true)
```

it will fail on save of last domain with `nullable` errors - no properties are copied to new object

```
ERROR org.grails.web.errors.GrailsExceptionResolver - ValidationException occurred when processing request: [GET] /domain
Validation Error(s) occurred during save():
- Field error in object 'error.DomainOne' on field 'domainTwo': rejected value [null]; codes [error.DomainOne.domainTwo.nullable.error.error.DomainOne.domainTwo,error.DomainOne.domainTwo.nullable.error.domainTwo,error.DomainOne.domainTwo.nullable.error.error.DomainTwo,error.DomainOne.domainTwo.nullable.error,domainOne.domainTwo.nullable.error.error.DomainOne.domainTwo,domainOne.domainTwo.nullable.error.domainTwo,domainOne.domainTwo.nullable.error.error.DomainTwo,domainOne.domainTwo.nullable.error,error.DomainOne.domainTwo.nullable.error.DomainOne.domainTwo,error.DomainOne.domainTwo.nullable.domainTwo,error.DomainOne.domainTwo.nullable.error.DomainTwo,error.DomainOne.domainTwo.nullable,domainOne.domainTwo.nullable.error.DomainOne.domainTwo,domainOne.domainTwo.nullable.domainTwo,domainOne.domainTwo.nullable.error.DomainTwo,domainOne.domainTwo.nullable,nullable.error.DomainOne.domainTwo,nullable.domainTwo,nullable.error.DomainTwo,nullable]; arguments [domainTwo,class error.DomainOne];
...
- Field error in object 'error.DomainOne' on field 'quantity' ....
```

Both properties were not copied.

There is different behaviour in unit tests for mocked domains. If we do similar as above in spec, we get error about read-only property `domainTwoId`:

```
grails.validation.ValidationException: Validation error occurred during call to save():
- Field error in object 'error.DomainOne' on field 'domainTwoId': rejected value [null]; codes [error.DomainOne.domainTwoId.typeMismatch.error,error.DomainOne.domainTwoId.typeMismatch,domainOne.domainTwoId.typeMismatch.error,domainOne.domainTwoId.typeMismatch,typeMismatch.error.DomainOne.domainTwoId,typeMismatch.domainTwoId,typeMismatch.java.lang.Object,typeMismatch]; arguments [domainTwoId]; default message [Cannot set readonly property: domainTwoId for class: error.DomainOne]

at org.grails.datastore.gorm.GormInstanceApi.doSave(GormInstanceApi.groovy:191)
at org.grails.datastore.gorm.GormInstanceApi.save_closure5(GormInstanceApi.groovy:170)
at groovy.lang.Closure.call(Closure.java:426)
at org.grails.datastore.mapping.core.DatastoreUtils.execute(DatastoreUtils.java:310)
at org.grails.datastore.gorm.AbstractDatastoreApi.execute(AbstractDatastoreApi.groovy:37)
at org.grails.datastore.gorm.GormInstanceApi.save(GormInstanceApi.groovy:169)
at org.grails.datastore.gorm.GormEntity$Trait$Helper.save(GormEntity.groovy:140)
at error.PropertiesConstructorSpec.ensure not saved association is copied(PropertiesConstructorSpec.groovy:43)
```

In unit tests, if we use `domainOne.properties.findAll {!it.key.endsWith('Id')}` instead of `domainOne.properties` in constructor - to exclude properties with association ids, it will pass.

But in application code it will cause another exception:

```
ERROR org.grails.web.errors.GrailsExceptionResolver - IllegalStateException occurred when processing request: [GET] /domain
No Datastore Session bound to thread, and configuration does not allow creation of non-transactional one here. Stacktrace follows:
java.lang.reflect.InvocationTargetException: null
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_60]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_60]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_60]
Caused by: java.lang.IllegalStateException: No Datastore Session bound to thread, and configuration does not allow creation of non-transactional one here
at grails.beans.util.LazyMetaPropertyMap.entrySet(LazyMetaPropertyMap.java:224) ~[grails-core-3.1.0.RC1.jar:3.1.0.RC1]
```

This last thing is potentially another issue. Should we report it separately? It is also faced when using debugger in IDE and trying to show properties of domain object.

I have created a sample project with both Application code in `Bootstrap.groovy` which prevents application from start and with spec - `PropertiesConstructorSpec.groovy` in project https://github.com/droggo/PropertiesConstructorError

Contributor guide

Open the contributing guide

Research direction

Reproduce the application failure from Bootstrap.groovy and the mocked-domain failure in PropertiesConstructorSpec.groovy from the linked sample project. Compare construction from domainOne.properties with the association-id-filtered variant, then determine the intended consistent behavior for application and unit tests. Done means the copied domain saves with its properties and association intact without the reported exceptions.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy
Domain
backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.