Skip to content

Latest commit

 

History

History
122 lines (92 loc) · 4.57 KB

JPA-IdClass-EntityExistsException.md

File metadata and controls

122 lines (92 loc) · 4.57 KB

JPA-EntityExistsException

다음과 같이 TraitTarget이라는 Entity가 있다. TraitTargetSourceIdType와 1:N 관계를 가지고 있다.

@Entity
public class TraitTarget {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "traitTarget")
    @LazyCollection(LazyCollectionOption.FALSE)
    private List<TraitTargetSourceIdType> sourceIdTypes;
    
    ...
}

TraitTargetSourceIdType는 다음과 같이 @IdClass로 복합키를 사용한다.

@Entity
@IdClass(TraitTargetSourceIdTypeId.class)
public class TraitTargetSourceIdType {

    @Id
    @ManyToOne(optional = false)
    @JoinColumn(name = "trait_target_id")
    private TraitTarget traitTarget;

    @Id
    @ManyToOne(optional = false)
    @JoinColumn(name = "id_type")
    private IdType idType;

    @Convert(converter = BooleanToStringConverter.class)
    private boolean selected;
    
    ...
}

복합키 클래스인 TraitTargetSourceIdTypeId는 다음과 같이 Serializable을 구현해야하고, equals()hashCode()도 구현해야 한다.

public class TraitTargetSourceIdTypeId implements Serializable {

    private Long traitTarget;
    private String idType;

    public TraitTargetSourceIdTypeId() {
    }

    public TraitTargetSourceIdTypeId(Long traitTarget, String idType) {
        this.traitTarget = traitTarget;
        this.idType = idType;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }
}

이 상태에서 Transaction 시작 없이 TraitTarget을 저장하면,

...
TraitTarget traitTarget = traitTargetRepository.findById(traitTargetId).orElseThrow(() -> new RuntimeException());
...
// @Transactional 도 없고, PlatformTransactionManager 로 Tx 설정도 없는 상태에서
traitTarget.setXXX(xxx);
traitTargetRepository.save(traitTarget);

다음과 같이 IdType의 식별자 값이 gaid인 데이터가 이미 세션에 존재한다는 에러가 난다. 뭔가 의도하지 않은 insert가 발생한다는 것 같다.

javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [a.b.c.d.IdType#gaid]

검색해보면 주로 entityManager.persist() 대신 entityManager.merge()를 사용하면 위 에러가 발생하지 않는다고 하는데,
Spring Data를 사용하면 그냥 repository.save()로 작성하면 내부적으로 entityManager.persist()entityManager.merge()를 알아서 구분해서 실행해주므로, 명시적으로 entityManager.merge()를 호출할 필요는 없다.
굳이 merge()를 호출한다해도 이 경우(@Transactional 도 없고, PlatformTransactionManager 로 Tx 설정도 없는 상태)에는 여전히 동일한 에러가 발생한다.

일반적인 경우라면, 그러니까 @Transactional이나 PlatformTransactionManager로 Tx 설정한 상태라면,
TraitTarget을 저장하는데 IdType이 저장될 필요는 없고, 실제로 저장되지도 않는다. 따라서 IdType에 대해 insert도 발생하지 않으며, 위와 같은 EntityExistsException 에러도 발생하지 않는다.

이 문제는 @Transactional이나 PlatformTransactionManager로 Tx 설정을 해주면 해결된다.

특히 PlatformTransactionManager를 사용할 때는 다음과 같이 Tx 시작 이후에 TraitTarget를 새로 조회하고 그 결과를 저장해야 에러가 발생하지 않는다.
이 때 commit 이나 rollback 을 누락하면 DB 테이블 레코드에 Lock이 걸려 해제되지 않을 수 있으므로 주의해야 한다.

혹시 누락했다면 여기를 참고해서 Lock을 해제한다.

@Autowired
private PlatformTransactionManager transactionManager;
...
    TraitTarget traitTarget = traitTargetRepository.findById(traitTargetId).orElseThrow(() -> new RuntimeException());
    ...
    TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        TraitTarget dbTraitTarget = traitTargetRepository.findById(traitTarget.getId()).orElseThrow(() -> new RuntimeException());
        dbTraitTarget.setXXX(xxx);
        traitTargetRepository.save(traitTarget);
    } catch (Exception e) {
        transactionManager.rollback(transactionStatus);
        throw new RuntimeException(e);
    }
    transactionManager.commit(transactionStatus);