What is the correct way to use CascadeType.MERGE with Spring Data Repositories?

Viewed 1698

In this question committing the transaction that saves the instances of entity2 and entity3, which each hold a reference to an Entity1 (entity1) instance, results in two records of entity1 appearing in the database (Since the reference is saved when each of the instances are saved).

If I have 2 Spring Data Repositories - Entity2Repository and Entity3Repository and I do the following will that result in 2 instances of entity 1 being contained in the database:

Entity1 entity1 = new Entity1();
entity1.name = "Name1";
entity1.value = "Value1";

Entity2 entity2 = new Entity2();
entity2.name = "Name2";
entity2.value = "Value2";
entity2.setEntity1(entity1);

Entity3 entity3 = new Entity3();
entity3.name = "Name3";
entity3.value = "Value3";
entity3.setEntity1(entity1);

Entity2Repository.save(entity2);
Entity3Repository.save(entity3);
1 Answers

Short answer

It will work correctly (only produce one record of entity1).

Long answer

The actual result of the sample code execution depends on the @Transactional annotation presence on your test method.

Entity2Repository.save(entity2);
Entity3Repository.save(entity3);

These calls will invoke the SimpleJpaRepository#save() method, which is @Transactional itself.

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Also, as you see, you should not worry about persist/merge calls - if it's a new entity, persist() will be called. isNew() check is pretty simple (see AbstractPersistable source code):

public boolean isNew() {
    return null == getId();
}

Without @Transactional

There will be two transactions - one for each save() call.

  • entity1, entity2 and entity3 are TRANSIENT now
  • Entity2Repository.save(entity2) is invoked
  • Transaction is opened, Session is created
    • persist() is invoked for entity2, as it's new entity
    • persist() is cascaded to entity1
    • entity1 and entity2 are PERSISTENT now, they are attached to the current Session
  • Transaction is commited, Session is closed
  • entity1 and entity2 are DETACHED now
  • Entity3Repository.save(entity3) is invoked
  • Transaction is opened, Session is created
    • persist() is invoked for entity3, as it's new entity
    • persist() is cascaded to entity1
    • org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist is thrown
  • Transaction is rolled back, Session is closed

entity1 and entity2 entries are saved in the database.

With @Transactional

As default @Transactional propagation is REQUIRED - there will be only one transaction.

  • entity1, entity2 and entity3 are TRANSIENT now
  • Transaction is opened, Session is created
    • Entity2Repository.save(entity2) is invoked
    • persist() is invoked for entity2, as it's new entity
    • persist() is cascaded to entity1
    • entity1 and entity2 are PERSISTENT now, they are attached to the current Session
    • Entity3Repository.save(entity3) is invoked
    • persist() is invoked for entity3, as it's new entity
    • persist() is cascaded to entity1, managed entity found in the first-level cache
    • entity3 is PERSISTENT now, it is attached to the current Session
  • Transaction is commited, Session is closed

entity1, entity2 and entity3 entries are saved in the database.

Related