코드로 보는 spring-data-jpa (1)
Prerequisite: Spring Data JPA 기본 구조와 동작원리
아래 코드는 spring-data-jpa의 2.6.x 버전입니다.
spring-data-jpa를 사용할 때 interface type으로 Repository를 정의해서 사용한다.
public interface CountryRepository extends JpaRepository<Country, Long> {
Country findByName(String name);
}
그리고 Repository의 save(T object)를 호출해서 Entity를 신규저장/업데이트한다.
아래 예제 코드처럼 Entity를 DB에 Insert(1) 조회한 Entity의 필드(칼럼)를 Update(2)하는 방식으로 많이 사용할 것이다.
<spring-data-jpa 예제 코드>
@Test
void testSaveOrUpdate() {
Country KOREA_V1 = new Country();
KOREA_V1.name = "Korea";
countryRepository.save(KOREA_V1); // (1) 이름이 "Korea" 신규 국가를 생성하여 저장소에 저장한다.
Country KOREA_V2 = countryRepository.findByName("Korea");
KOREA_V2.code = "KR";
countryRepository.save(KOREA_V2); // (2) 이름으로 조회한 후 code를 "KR"로 업데이트한다.
assertThat(countryRepository.findByName("Korea").code).isEqualTo("KR");
}
(1)과 (2) 모두 save()를 동일하게 호출하지만 동작 결과는 다르다. (1)의 경우 Insert 문이 실행되고 (2)의 경우 Update 문이 실행된다.
그렇다면 spring-data-jpa의 JpaRepository는 Insert와 Update를 어떻게 구분해서 실행할까?
위 질문에 대한 해답은 SimpleDataJpaRepository에서 찾을 수 있다.
interface로 선언된 JpaRepository의 save()를 호출하면 내부적으로 o.s.d.jpa.repository.support.SimpleDataJpaRepository의 save()를 호출한다. (SimpleDataJpaRepository가 내부적으로 어떻게/왜 호출되는지는 'JPA Repository 생성의 비밀' 에서 알아보자)
SimpleDataJpaRepository는 EntityInformation type의 필드 entityInformation를 갖고 있다. entityInformation은 SimpleDataJpaRepository가 다루는 Entity의 Metadata를 갖고 있다(이름, 타입, 필드 정보 등등).
EntityInformation의 isNew에 Entity를 파라미터로 넘겨 새롭게 생성된 Entity인지를 판단한다(아래 코드(3)).
<SimpleDataJpaRepository.class>
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) { <----(3)
em.persist(entity); <---- (4)
return entity;
} else {
return em.merge(entity); <---- (5)
}
}
EntityInformation은 interface이며 구현체인 o.s.d.j.r.s.JpaMetamodelEntityInformation의 isNew()가 호출된다. (Entity가 Persistable의 구현체인 경우 제외)
<JpaMetamodelEntityInformation.class>
@Override
public boolean isNew(T entity) {
if (!versionAttribute.isPresent()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity); // (6) super(AbstractEntityInformation)의 isNew 호출
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true); // (7) Entity의 버전 필드가 null이면 true 아니면 false를 반환한다
}
<AbstractEntityInformation.class>
public boolean isNew(T entity) {
ID id = this.getId(entity);
Class<ID> idType = this.getIdType();
if (!idType.isPrimitive()) {
return id == null; // (8) id가 null이면 true 반환
} else if (id instanceof Number) {
return ((Number)id).longValue() == 0L; // (9) id가 Number type에 값이 0이면 true 반환
} else {
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}
}
1) Version 필드가 존재하지 않는 경우 (참고: 코드 (6))
super class인 AbstractEntityInformation.isNew()를 호출하여 Id 필드가 null 또는 0인지 판단한다.
null 또는 0인 경우 New Entity로 판단한다.
2) Version 필드가 존재하는 경우
Version 필드의 값이 null이라면 New Entity로 판단한다. Version 필드를 가진 Entity라면 Id 필드를 New Entity로 판단하는데 활용하지 않는다.
<spring-data-jpa 예제 코드>로 다시 돌아가보면 왜 save()를 동일하게 실행했음에도 불구하고 다른 실행결과가 나왔는지 추측해볼 수 있다.
@Test
void testSaveOrUpdate() {
Country KOREA_V1 = new Country(); //아직 Id 필드와 Version필드에 값이 없다
KOREA_V1.name = "Korea";
countryRepository.save(KOREA_V1);
// (1) DB에 Insert 되면서 Id(또는 Version)이 할당됐다.
Country KOREA_V2 = countryRepository.findByName("Korea");
//Id (또는 Version)에 값이 존재한다
KOREA_V2.code = "KR";
countryRepository.save(KOREA_V2);
//(2) 다시 조회했을 때 Id 필드(또는 Version 필드)의 값이 할당돼있는 상태이다.
assertThat(countryRepository.findByName("Korea").code).isEqualTo("KR");
}
(1)에서 Entity를 저장할 때는 아직 Id 필드와 Version필드에 값이 없는 New Entity인 상태이다. 고로 아래 <SimpleJpaRepository.class>의 save에서 em.persist를 실행한다(4).
(2)에서 Id 필드(또는 Version)에 값이 있는 Entity를 다시 save를 친다면 isNew는 false를 반환하고 em.merge()를 실행하게 된다(5).
<SimpleJpaRepository.class>
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) { <----(3)
em.persist(entity); <---- (4)
return entity;
} else {
return em.merge(entity); <---- (5)
}
}.
이 경우 EntityManager가 해당 Entity를 persist 하게 되면 Entity가 TRANSIENT 상태로 인식하고 Insert 구문을 실행한다. 반면 merge로 Entity가 DETACHED 상태로 인식하여 Update 구문이 실행된다.
persist면 무조건 Insert이고 merge면 무조건 Update일까? 그건 또 그렇지 않다. 이후의 부분부터는 spring-data-jpa가 아니라 hibernate의 EntityManager와 그 구현체의 코드를 살펴봐야 한다. 이번 주제가 너무 커지기 때문에 여기서 멈추도록 하고 EntityManager의 persist와 merge가 어떻게 동작하는지는 따로 다루도록 하자