spring-data-jpa가 엔티티가 새 엔티티를 판별하는 방법, Persistable
save()의 동작원리
- 스프링이 jpa 인터페이스를 구현할 때, 그러니까 JpaRepository 과 그것을 상속한 인터페이스를 구현할 때, SimpleJpaRepository 로 구현한다.
- SimpleJpaRepository의 코드를 보면 다음과 같다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
private final EntityManager em;
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
}
- 위의 구현체는 jpa를 구현하는 것과 크게 차이가 없는 것을 확인할 수 있다. @Repository 를 사용하며 @Transactional(readOnly = true)을 사용함을 확인할 수 있다.
- save()의 경우 @Transactional이 있다. 그러므로 해당 리포지토리에 데이터를 삽입하면 db에 저장이 된다. 트랜잭션이 없는 경우 insert가 되지 않음을 확인할 수 있다.
@SpringBootTest
class MemberRepositoryNoTransactionalTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Test
void test(){
// repository.save에서는 insert query가 발생한다.
//insert into member (created_date, last_modified_date, create_by, last_modified_by, age, team_id, username, id) values (?, ?, ?, ?, ?, ?, ?, ?)
final Member member = new Member("kim");
memberRepository.save(member);
// javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
final Member member2 = new Member("lee");
em.persist(member2);
}
}
isNew()
- 그런데 save()의 구현 코드를 보면 isNew()를 기반으로 insert를 할지 update를 할지 판별한다. insert는 persist로, update는 merge로 저장한다.
- inNew()는 어떻게 판별할까? isNew는 기본적으로 @Id로 선언한 필드의 null의 여부로 판단한다. (기본타입은 0의 여부로 판단한다).
-
한편, @Id에 @GenerateValue가 없을 경우 어떻게 할까?
- 엔티티
@Entity
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Item {
@Id
private String id;
}
- 테스트
@Test
void test(){
final Item item1 = new Item("item1");
itemRepository.save(item1);
}
- sql 결과
select
item0_.id as id1_0_0_
from
item item0_
where
item0_.id=?
insert
into
item
(id)
values
(?)
- merge로 동작한다. merge는 select과 insert/update 두 개의 쿼리가 한 쌍이다. 처음으로 “item1”이란 id를 가진 값을 select으로 검색한다. 없으면 insert를 진행한다.
- 쿼리를 한 번만 동작하게 하는 방법은 없을까?
@Entity
@Setter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
public Item(String id) {
this.id = id;
}
}
- Persistable을 상속하면 isNew를 재정의 한다. 이 때 insert의 기준이 되는 @CreatedDate의 존재 여부로 영속 상태에 있었던 객체인지 아니면 새로운 객체인지를 판별한다. 물론 이 상황에서는 createDate에 대하여 setter를 모두 닫아야 할 것이다.
- 동일한 쿼리를 할 경우 select - insert 가 아닌, 바로 insert를 함을 확인할 수 있다.