snowdrop-zen / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
1 stars 0 forks source link

spring-data-jpa: repository calls merge() when it should call persist() instead #119

Closed snowdrop-bot closed 4 years ago

snowdrop-bot commented 4 years ago

Describe 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():

To Reproduce Steps to reproduce the behavior:

  1. 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):

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:

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.


https://github.com/quarkusio/quarkus/issues/10156


$upstream:10156$