Calling save(merge: true) on a detached instance does not merge state into an existing instance
- 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
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