apache / apache/grails-core

Calling save(merge: true) on a detached instance does not merge state into an existing instance

Open
#14,443 0 comments 0 reactions 0 assignees View on GitHub
relates-to: grails-data-hibernate5
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

We are in the middle of porting an application over to using GORM & spring boot. As we were porting over one of our controllers, we ran into an issue involving merging a detached entity. Here is the scenario:

- We have a PUT endpoint which receives an instance of an existing domain object as a detached entity
- We send this entity to the service layer
- We call save(merge: true) on the instance

As an example:
```groovy
@RestController
class MyController {

@Autowired
private UserService userService

@PutMapping("/users/{id}")
void updateUser(@RequestBody User userToUpdate) {
userService.save(userToUpdate)
}
}

@Transactional
@Service
class UserService {

void save(User user) {
user.save(merge: true)
}
}
```

From the documentation I can find related to how merge works in GORM, as well as how merge is supposed to work in JPA in general, my expectation would be that the following would happen:
- The service layer will look up the persistent state of the user with the given ID from the database
- The new values from detached `userToUpdate` instance will be copied over to the persistent instance
- The persistent instance will be returned

However, what seems to be happening instead is that hibernate is running an insert statement. Is there a different API I should be using to make this behave like a true JPA merge?

**NOTE**: the `merge: true` flag does not seem to be documented anywhere that I could find. I discovered this flag by looking through the grails source code.

**Sample Application**
I've attached a sample application which includes a controller similar to the code snippets I've included above. It also includes some unit tests which demonstrate the issue. Interestingly enough, in the attached `MergeEntityUnitSpec`, not even an insert gets run - the merge call seems to have no effect whatsoever. However, I do see a select statement being run, which I assume is the call to load the persistent instance.

To run the sample app:
- cd to the application directory
- execute `./gradlew bootRun`
- to manually test, run the CURL commands pasted in `UserController.groovy`
- to run the unit tests, execute `./gradlew test`

I ran the application using the following java version:
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.6+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.6+10, mixed mode)

I did a bit of digging into the issue on my own. I found that grails uses a custom dirtiness strategy called `GrailsEntityDirtinessStrategy`. In order for this class to track changes correctly, it requires all field modifications to be made through setter methods, since this is what triggers the dirtiness tracking. However, if you look into the hibernate merge internals, it appears to be using direct field access (at least in my sample app). If this is the case, I suspect hibernate's merge is sidestepping the grails dirtiness checking.
[merge-issue.zip](https://github.com/grails/gorm-hibernate5/files/4369896/merge-issue.zip)

Contributor guide

Open the contributing guide

Research direction

Start with the attached sample application, especially MergeEntityUnitSpec and UserController.groovy, then run ./gradlew test and reproduce the detached-entity save(merge: true) behavior. Read the GORM merge documentation and the GrailsEntityDirtinessStrategy context described in the issue. Done means the expected persistent state update is demonstrated by a regression test rather than an insert or no-op.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy, spring-boot
Domain
api, backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.