1. merge() 와 persist() 의 차이
jpa merge 란? : 준 영속상태의 엔티티를 영속 상태로 만들거나, 영속 상태의 엔티티를 업데이트하는 연산.
persist 란? : 새로 만들어진 엔티티를 영속화시키는 것!
그래서 새로운 것이면 persist 만 수행하고, 새로운 것이 아니고 id 가 존재한다면 merge 로 db 에 select 쿼리를 보내 영속성 컨텍스트에 올리게 된다.
persist 를 실행하고, db 혹은 1차 캐시에 같은 엔티티가 존재하면, EntityExistsException 에러가 발생한다.
* 도대체 언제 detached 가 되는걸끼?
- 영속성 컨텍스트 메모리 관리 : managed 이면 더티체킹으로 매우 메모리 낭비가 심해지기 때문에 detached.
- 읽기만 하고 변경 감지를 막고 싶을 때 - @Transactional(readOnly = true)
- rollback 용도 : 변경 전 상태를 유지하고 싶을 때
등의 상황에서 detached 가 된다.
2. save 구현
JPA 를 사용할 때 Class 가 아니라 JpaRepository 나 CrudRepositoy 등의 인터페이스를 extends 해서 만들게 되는데, 이것은 jpa 에서 구현한 쿼리메서드를 사용하기 위함이다. jpa 에서 따로 프록시같은 객채를 만들어 구현해주기 때문이다. 이때 구현되는 객체의 이름이 SimpleJpaRepositoy 이고, 여기서 save 나 findById, delete, deleteById 같은 default 구현 메서드들의 코드를 볼 수 있다.
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) { // new 상태일 경우 persist 실행
this.entityManager.persist(entity);
return entity;
} else {
return this.entityManager.merge(entity); // 다른 경우 merge 를 실행한다.
}
}
여기서 save 를 살펴보면, 일단 isNew 함수를 통해 생명주기를 확인한다.
new 상태의 객체이면 persist 를 하여 insert 문을 날리게 된다. 반면, new 가 아니면 merge 를 날리게 되는데, merge 는 따로 select 쿼리를 날려 해당 id 를 가진 엔티티를 가져온다.
isNew 함수를 살펴보면,
// AbstractEntityInformation.java
public boolean isNew(T entity) {
ID id = this.getId(entity);
Class<ID> idType = this.getIdType();
if (!idType.isPrimitive()) {
return id == null;
} else if (id instanceof Number) {
return ((Number)id).longValue() == 0L;
} else {
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
}
여기에서 id를 먼저 찾고, id 타입과 값을 확인하여 new 상태인지(신규) 확인한다.
1) Id가 존재하여 persist 대신 merge 를 해서 select 쿼리를 날렸는데 실제 db 에 그 값이 없으면 어떻게 되나?
이런 경우는 개발자가 직접 id를 넣은 경우다. 이때, DB에 해당 id 를 가진 엔티티가 없으면 에러가 나기보다는 새로운 엔티티를 insert 한다(이때 엔티티 설정에서 빈 값은 null 로 넣는다고 한다). 있다면 영속성 컨텍스트에 있는 내용을 db에 반영하기 위해 update 문을 날린다.
이 과정에서 개발자가 일일이 db를 확인해서 id가 있는지 없는지 알 수도 없고, 이런 경우, id 가 중심이 되어 엔티티를 관리하는 jpa 입장에서 추가 쿼리가 나가거나 하는 번거로움이 생길 수 있다.
2) id를 직접 생성할 때
그래서 보통은 다음과 같이 @GeneratedValue 로 id를 자동 생성한다. 그러나 직접 id를 생성해야 할 때도 있을 것이다.
이때, merge 관련 문제를 방법으로, entity 클래스 자체에서 Persistable 인터페이스를 상속받고, getId와 isNew 함수를 구현해줄 수 있다. isnew를 id 로 확인하지 않고 자체적으로 구현하여 true를 반환하도록 하면 merge 대신 persist 로 처리되게 될 것이다.
@Entity
public class User implements Persistable<Long> {
@Id
private Long id;
private String name;
// 신규 여부 플래그
@Transient
private boolean isNew = true;
@CreatedDate
privae LocalDateTime createdAt;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
// ID 반환
@Override
public Long getId() {
return id;
}
// isNew 여부로 INSERT/UPDATE 판단
@Override
public boolean isNew() {
if this.createdAt == null{
return true
}
return false
}
}
그것이 어떻게 가능할 수 있었을까?
SimpleJpaRepository 에서, isNew 함수는 EntityInformation 에서 실행하고 있는데, 이중 JpaEntityInformation 엔티티의 메타데이터를 관리하는 인터페이스이다.
EntityInformation 을 반환하는 JpaEntityInformationSupport라는 클래스에서 해당 함수인 getEntityInformation 의 함수를 보면,
// JpaEntityInformationSupport.java
public static <T> JpaEntityInformation<T, ?> getEntityInformation(Class<T> domainClass, EntityManager em) {
Assert.notNull(domainClass, "Domain class must not be null");
Assert.notNull(em, "EntityManager must not be null");
Metamodel metamodel = em.getMetamodel();
PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();
return (JpaEntityInformation)(Persistable.class.isAssignableFrom(domainClass) ? new JpaPersistableEntityInformation(domainClass, metamodel, persistenceUnitUtil) : new JpaMetamodelEntityInformation(domainClass, metamodel, persistenceUnitUtil));
}
다음과 같이 구현되어 있다.
마지막에 Persistable 로 구현한 엔티티일 경우 JpaPersistableEntityInformation 객체를 돌려보내는 것을 확인할 수 있다.
// JpaPersistableEntityInformation.java
public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID> extends JpaMetamodelEntityInformation<T, ID> {
public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel, PersistenceUnitUtil persistenceUnitUtil) {
super(domainClass, metamodel, persistenceUnitUtil);
}
public boolean isNew(T entity) {
return entity.isNew();
}
@Nullable
public ID getId(T entity) {
return entity.getId();
}
}
이렇게 entity의 isNew 와 getId 를 실행하는 것을 확인할 수 있다. 이렇게 되면 id 를 직접 생성하고도 persist를 사용할 수 있다.
꾸준함은 최고를, 박진형 - Spring Dat JPA 의 save 의 동작 과정 에서는, isNew를 내부적으로 확인하는 방법으로, @CreateDate(persist 할 때 생성) 이 null 이면 new, 아니면 new 가 아닌 걸로 판단하고 있었다.
* Persistable 객체란 무엇일까?
// Persistable.java 인터페이스
public interface Persistable<ID> {
@Nullable
ID getId();
boolean isNew();
}
매우 단순한 인터페이스인데, 그저 getId와 isNew 처럼 인터페이스를 지정한 것이고, 엔티티의 정보를 확인할 때, 특히 생성된 것인지, 전인지, 아이디는 뭔지 판단할 때 다른 코드를 날리기 위해 jpa 에서 이 객체를 만들어 나름의 최적화를 수행했다고 볼 수 있다.
이로서 save 메서드가 내부적으로 어떻게 동작하는지 확인할 수 있었다.