Spring Data JPA에서 새로운 Entity를 저장할 때
public interface SaveTestRepository extends JpaRepository<SaveTest, Long> {
}
save
@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);
}
}
Repository의 save 메소드로 저장한다.
저장할 때 Entity가 insert 인지 update 인지 확인하는 로직이 JpaMetamodelEntityInformation에 있다.
@Override
public boolean isNew(T entity) {
if (!versionAttribute.isPresent()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity);
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
버전에 따라서 isPresent()가 isEmpty()인 경우도 있다.
isNew를 자세히 보면
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
@GeneratedValue 어노테이션으로 키 생성 전략을 사용하면 데이터베이스에 저장될 때 id가 할당된다.
데이터베이스에 저장되기 전에 메모리에서 생성된 객체는 id가 비어있기 때문에 isNew()는 true가 되어 새로운 entity로 결정된다.
@GeneratedValue 어노테이션 없이 직접 ID를 할당하는 경우
키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않는다. 이 때는 엔티티에서 Persistable<T> 인터페이스를 구현해서 JpaMetamodelEntityInformation 클래스가 아닌 JpaPersistableEntityInformation의 isNew()가 동작하도록 해야 한다.
- save(entity)의 동작 절차
1. 메서드 호출
사용자가 save(entity)를 호출합니다.
입력값에 따른 기본 동작
- 새 엔티티: 데이터베이스에 아직 저장되지 않은 엔티티.
- 기존 엔티티: 데이터베이스에서 이미 존재하는 엔티티.
2. 엔티티의 상태 확인
Spring Data JPA는 JpaEntityInformation을 사용하여 엔티티가 새로운 상태인지 판단합니다.
- 새 엔티티인지 확인 기준:
- @Id 필드 확인: @Id 값이 null 또는 기본값(예: 0)인지 확인.
- @Version 필드 확인: @Version 값이 null 또는 초기값인지 확인.
새 엔티티: isNew()가 true를 반환하면 새로운 엔티티로 간주.
기존 엔티티: isNew()가 false를 반환하면 기존 엔티티로 간주.
3. JPA EntityManager 메서드 호출
Spring Data JPA는 엔티티의 상태에 따라 EntityManager의 적절한 메서드를 호출합니다.
(1) 새로운 엔티티인 경우
새 엔티티로 판별되면 **EntityManager.persist()**를 호출합니다.
- 데이터베이스에 엔티티를 INSERT.
- @Id 값이 @GeneratedValue로 설정되어 있으면 키 생성 전략에 따라 값이 할당됩니다.
- 영속성 컨텍스트에 엔티티가 **"영속 상태"**로 저장됩니다.
(2) 기존 엔티티인 경우
기존 엔티티로 판별되면 **EntityManager.merge()**를 호출합니다.
- 데이터베이스에 엔티티를 UPDATE.
- merge()는 전달된 엔티티의 상태를 영속성 컨텍스트에 있는 엔티티로 복사합니다.
- 반환된 엔티티는 영속 상태가 되며, 원래 전달된 엔티티는 준영속 상태로 남습니다.
4. 데이터베이스 동기화
- 트랜잭션 커밋 시점 또는 명시적 flush() 호출 시점에 SQL 쿼리가 데이터베이스로 전송됩니다.
- @Transactional 애너테이션이 있는 경우, 트랜잭션 종료 시점에 변경 사항이 자동으로 플러시(Flush)됩니다.
5. 반환값
save() 메서드는 다음을 반환합니다:
- 새 엔티티의 경우: persist()로 인해 생성된 엔티티.
- 기존 엔티티의 경우: merge() 결과로 반환된 영속 상태의 엔티티.
구체적인 내부 동작 흐름
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) { // 1. 새로운 엔티티인지 확인
entityManager.persist(entity); // 2. 새 엔티티면 persist() 호출
return entity;
} else {
return entityManager.merge(entity); // 3. 기존 엔티티면 merge() 호출
}
}
추가적으로 알아야 할 사항
1. isNew()의 구현
JpaEntityInformation 클래스의 isNew() 메서드는 다음 기준으로 새로운 엔티티를 판단합니다:
- @Id 필드가 null 또는 초기값이면 새 엔티티.
- @Version 필드가 null 또는 초기값이면 새 엔티티.
2. persist() vs merge() 차이
Feature | persist() | merge() |
동작 | 엔티티를 영속성 컨텍스트에 저장 | 엔티티 상태를 병합 |
새 엔티티만 처리 | 가능 | 불가능 |
기존 엔티티만 처리 | 불가능 | 가능 |
결과 | 전달된 엔티티가 영속 상태가 됨 | 반환된 엔티티만 영속 상태가 됨 |
3. 트랜잭션 필요성
save() 메서드 호출은 영속성 컨텍스트를 사용하기 때문에 트랜잭션 내에서 호출해야 합니다.
예를 들어, Spring에서는 @Transactional을 사용하여 트랜잭션을 관리해야 합니다.
4. 주의할 점
- 변경 사항 플러시:
- save() 호출 직후 데이터베이스에 즉시 반영되지 않을 수 있습니다.
- flush()를 호출하거나 트랜잭션이 커밋될 때 반영됩니다.
- merge() 호출 후 반환된 엔티티 사용:
- merge()는 전달된 엔티티가 아닌 새로운 엔티티를 반환하므로, 반환값을 사용해야 합니다.
사용 예시
새 엔티티 저장
MyEntity entity = new MyEntity();
entity.setName("New Entity");
repository.save(entity); // persist() 호출 → INSERT 실행
기존 엔티티 업데이트
MyEntity entity = repository.findById(1L).get();
entity.setName("Updated Name");
repository.save(entity); // merge() 호출 → UPDATE 실행
'개발 > Java & Kotlin' 카테고리의 다른 글
[JPA] 엔티티 매니저 알아보기 (116) | 2024.11.30 |
---|---|
[JPA] JPA의 ddl-auto 옵션 알아보기 (140) | 2024.11.27 |
[Java] 영속성 어댑터 구현하기 (111) | 2024.09.02 |
[Java] 웹 어댑터 구현하기 (150) | 2024.07.26 |
[Java] 유스케이스 구현하기 (171) | 2024.07.23 |