spring-data-jpa: repository calls merge() when it should call persist() instead
See original GitHub issueDescribe the bug
Although we do not work with detached entities, we ran into https://hibernate.atlassian.net/browse/HHH-9362 and/or https://hibernate.atlassian.net/browse/HHH-11414 for a rather simple persist scenario.
In the call stack we can see that the generated repository is calling merge()
instead of persist()
.
This is most likely due to this code in StockMethodsAdder
:
// call EntityManager.persist of the entity does not have it's ID set, otherwise call EntityManager.merge
BytecodeCreator idValueUnset;
BytecodeCreator idValueSet;
if (idType instanceof PrimitiveType) {
BranchResult idValueNonZeroBranch = save.ifNonZero(idValue);
idValueSet = idValueNonZeroBranch.trueBranch();
idValueUnset = idValueNonZeroBranch.falseBranch();
} else {
BranchResult idValueNullBranch = save.ifNull(idValue);
idValueSet = idValueNullBranch.falseBranch();
idValueUnset = idValueNullBranch.trueBranch();
}
idValueUnset.invokeStaticMethod(
MethodDescriptor.ofMethod(JpaOperations.class, "persist", void.class, Object.class),
entity);
idValueUnset.returnValue(entity);
ResultHandle entityManager = idValueSet.invokeStaticMethod(
ofMethod(JpaOperations.class, "getEntityManager", EntityManager.class));
entity = idValueSet.invokeInterfaceMethod(
MethodDescriptor.ofMethod(EntityManager.class, "merge", Object.class, Object.class),
entityManager, entity);
idValueSet.returnValue(entity);
We do pre-populate our IDs (custom strongly typed wrappers around UUID
, e.g. UserId
) so the generated code seems to think we have an update case of a detached entity, which is normally the only case when you really need merge()
.
JavaDoc of EnityManager.merge()
:
Merge the state of the given entity into the current persistence context.
Expected behavior
Quarkus spring-data-jpa should only call merge()
when really required, that is from my POV: merging a detached entity.
With a pre-populated id, the initial persist should be performed with persist()
, not merge()
.
Actual behavior
Quarkus spring-data-jpa calls merge()
:
- in a persist case when Id is pre-populated
- in all update cases (Id is present)
To Reproduce Steps to reproduce the behavior:
- Will create a reproducer if required but I am pretty swamped right now.
Configuration n/a
Screenshots n/a
Environment (please complete the following information):
- Output of
uname -a
orver
:MINGW64_NT-10.0-18363 XXX 3.0.7-338.x86_64 2019-11-21 23:07 UTC x86_64 Msys
- Output of
java -version
:openjdk version "11.0.7" 2020-04-14 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.7+10) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.7+10, mixed mode)
- GraalVM version (if different from Java): n/a
- Quarkus version or git rev:
1.4.2.Final
- Build tool (ie. output of
mvnw --version
orgradlew --version
):Apache Maven 3.6.3
Additional context
The original spring-data-jpa
is more or less doing the same, but it is also checking the value of the @Version
column:
https://github.com/spring-projects/spring-data-jpa/blob/2.3.1.RELEASE/src/main/asciidoc/jpa.adoc#entity-state-detection-strategies
See also:
- https://github.com/spring-projects/spring-data-jpa/blob/2.3.1.RELEASE/src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java#L242-L252
- https://github.com/spring-projects/spring-data-commons/blob/2.3.1.RELEASE/src/main/java/org/springframework/data/repository/core/support/AbstractEntityInformation.java#L40-L54
On a side note: I don’t know why the docs say
Option 1 is not an option for entities that use manually assigned identifiers
because the code clearly shows that the Id-check is skipped when a version column is present.
This would help in our case for the inital persist, but it would still use merge()
for updates which means we might run into HHH-9362 and it would be “incorrect” since our entities are already attached when they are updated.
Alternatively, org.springframework.data.domain.Persistable.isNew()
would also help, but again only for the intial persist. The interface is present in Quarkus spring-data-japa
, but the method is not called.
What I am proposing here for the update case actually goes futher than what Spring is doing which might justify a configuration property to control that behaviour:
Only use merge()
if EntityManager.contains()
returns false
.
Issue Analytics
- State:
- Created 3 years ago
- Comments:32 (31 by maintainers)
It’s a typical monday for me so I am probably confused too. 😉
Later today I’ll try to write down the rules I am thinking of. So maybe I’ll come to the same conclusion eventually.
Actually, I’ll take a look at adding support for
@Version
as this seems to be used quite widely and as already mentioned it is what regular spring-data-jpa does.But I don’t think I’ll be doing anything more for this ticket unless we find a really good reason to