개발/Java & Kotlin

[JPA] Spring Data JPA에서 새로운 Entity 판단하는 방법

devhooney 2024. 11. 26. 08:47
728x90

 

 

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로 결정된다.

 

 

 

728x90

 

 

 

 

@GeneratedValue 어노테이션 없이 직접 ID를 할당하는 경우

키 생성 전략을 사용하지 않고 직접 ID를 할당하는 경우 새로운 entity로 간주되지 않는다. 이 때는 엔티티에서 Persistable<T> 인터페이스를 구현해서 JpaMetamodelEntityInformation 클래스가 아닌 JpaPersistableEntityInformation의 isNew()가 동작하도록 해야 한다.

 

 

 

 

- save(entity)의 동작 절차

1. 메서드 호출

사용자가 save(entity)를 호출합니다.

입력값에 따른 기본 동작

  • 새 엔티티: 데이터베이스에 아직 저장되지 않은 엔티티.
  • 기존 엔티티: 데이터베이스에서 이미 존재하는 엔티티.

 

2. 엔티티의 상태 확인

Spring Data JPA는 JpaEntityInformation을 사용하여 엔티티가 새로운 상태인지 판단합니다.

  • 새 엔티티인지 확인 기준:
    1. @Id 필드 확인: @Id 값이 null 또는 기본값(예: 0)인지 확인.
    2. @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. 주의할 점

  1. 변경 사항 플러시:
    • save() 호출 직후 데이터베이스에 즉시 반영되지 않을 수 있습니다.
    • flush()를 호출하거나 트랜잭션이 커밋될 때 반영됩니다.
  2. 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 실행

 

 

 

 

 

728x90

'개발 > 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