Programming/Spring

Spring Data JPA - 새로운 엔티티 구별하기

잇(IT) 2023. 7. 26. 16:29

- JPA의 save() 메서드

1. 새로운 엔티티면 저장(persist)

2. 새로운 엔티티가 아니면 병합(merge)

 

- 새로운 엔티티를 판단하는 기본 전략

1. 식별자가 객체일 때 null 로 판단

2. 식별자가 자바 기본 타입일 때 0 으로 판단

3. Persistable 인터페이스를 구현해서 판단 로직 변경 가능


- Item 엔티티

@Entity
@Getter
public class Item {

    @Id @GeneratedValue
    private Long id;

}

 

- Item Repository

public interface ItemRepository extends JpaRepository<Item, 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);
		}
	}

 

 

- save() 메서드가 호출되면 위와 같이 entity가 null, 0인지 확인한다. null이나 0일 경우 persist를 수행한다.

- persist를 호출하게 되면 insert 쿼리 한번만 실행하게 된다.

 

- Test 코드

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item();
        itemRepository.save(item);
    }
}


- 참고

1. JPA 식별자 생성 전략이 @GenerateValue면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.

2. JPA 식별자 생성 전략이 @Id만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save()를 호출한다. 이 경우 merge()가 호출되고 merge()는 DB에서 값을 확인하기 때문에 비효율적이다. 따라서 Persistable을 사용해서 엔티티 확인 여부를 직접 구현하는게 효과적인다.


- @GenerateValue를 사용하지 않고 직접 식별자를 사용한 경우

@Entity
@Getter
//@NoArgsConstructor(access = AccessLevel.PROTECTED) 기본생성자 생성 대신 작성하면 된다.
public class Item {

    @Id
    private String id;

    public Item() {
    }

    public Item(String id) {
        this.id = id;
    }
}

 

- Test 코드

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item("A");
        itemRepository.save(item);
    }
}

- entity가 null, 0이 아니기 때문에 merge()가 실행되게 되고 DB에 쿼리를 날려서 해당 데이터가 있는지 확인한다.

- DB에서 데이터를 확인 후 insert 쿼리를 날린다.

- merger()를 사용하게 되면 쿼리가 추가로 발생하기 때문에 사용되는 것이 좋지 않다.


- Persistable 인터페이스 사용

@Entity
@Getter
//@NoArgsConstructor(access = AccessLevel.PROTECTED) 기본생성자 생성 대신 작성하면 된다.
public class Item implements Persistable<String> {

    @Id
    private String id;

    public Item() {
    }

    public Item(String id) {
        this.id = id;
    }

    @Override
    public boolean isNew() {
        return false;
    }
}

- Persistable<String> 인터페이스를 사용하게 되면 

@Override
    public boolean isNew() {
        return false;
    }

- isNew() 구현체를 구현하게 된다.

 

- 실무에서는 id를 통해 판단하기가 쉽지 않다.

@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
    
    @Id
    private String id;
    
    @CreatedDate
    private LocalDateTime createdDate;
    
    public Item(String id) {
        this.id = id;
    }
    
    @Override
    public String getId() {
        return id;
    }
    
    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}

- @CreatedDate는 JPA persist가 호출되기 전에 동작한다. 때문에 별도의 쿼리를 날리지 않는다.

- @CreatedDate의 createdDate; 의 값은 persist되기 전에 null이기 때문에 isNew()는 null로 인식하고 save() 메서드를 persist로 실행하게 된다.

 

 

 

 

 

 

출처 : 인프런 - 김영한(실전! 스프링 데이터 JPA)

728x90