[JPA 활용] 7. Spring Data JPA
- Spring Data JPA
- 스프링 데이터 JPA와 DB 설정 주의 사항
- Entity에서 ToString시 주의 사항
- Repository 차이
- 주요 메서드
- 메소드 이름으로 쿼리 생성
- JPA NamedQuery
- @Query, 리포지토리 메소드에 쿼리 정의하기
- @Query, 값, DTO 조회하기
- 반환 타입
- 정렬
- 벌크성 수정 쿼리
- @EntityGraph
- JPA Hint & Lock
- 사용자 정의 리포지토리 구현
- Auditing
- Web 확장 - 도메인 클래스 컨버터
- Web 확장 - 페이징과 정렬
- 스프링 데이터 JPA 구현체 분석
- 새로운 엔티티를 구별하는 방법
Spring Data JPA
JPA 학습을 마쳤다! 기초 지식을 바탕으로, 이제 실무에 적용 가능한 Spring Data JPA를 본격적으로 학습해 볼 계획이다.
스프링 데이터 JPA와 DB 설정 주의 사항
yml을 기준으로 spring.jpa.hibernate.ddl-auto 옵션이 있다. create, create-drop, update는 DB의 데이터가 많을때 락 현상이 발생 할 수 있으므로, 되도록 사용하지 말자.
Entity에서 ToString시 주의 사항
ToString시 양방향 관계 참조시 순환 참조 현상이 발생 될 수 있다. 롬복 사용시 아래와 같이 방지 할 수 있다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
...
Repository 차이
// 기존 JPA
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
...
}
// Spring Data JPA
// @Repository 생략
// JpaRepository 상속
// Generic : <엔티티, 식별자 타입>
public interface MemberRepository extends JpaRepository<Member, Long> {
}
주요 메서드
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
- findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다
메소드 이름으로 쿼리 생성
// 순수 JPA 방법
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
}
// spring Data JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고: (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation)
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회: find…By ,read…By ,query…By get…By,
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.querymethods.query-creation
- 예:) findHelloBy 처럼 …에 식별하기 위한 내용(설명)이 들어가도 된다.
- COUNT: count…By 반환타입 long
- EXISTS: exists…By 반환타입 boolean
- 삭제: delete…By, remove…By 반환타입 long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFirst3, findFirst, findTop, findTop3
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limitquery-result
참고
이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지않으면 애플리케이션을 시작하는 시점에 오류가 발생한다. 이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
JPA NamedQuery
// @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
// 순수 JPA
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
// 스프링 데이터 JPA로 NamedQuery 사용
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
// @Query 를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
// 메서드 이름(findByUsername)이 엔티티에 정의된 Named 쿼리의 이름(Member.findByUsername)과 정확히 일치해야 한다
// 스프링 데이터 JPA는 도메인 클래스 + .(점) + 메서드 이름으로 찾는다.
// 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
List<Member> findByUsername(@Param("username") String username);
참고
스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다. 대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
@Query, 리포지토리 메소드에 쿼리 정의하기
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username= :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") intage);
}
@Query, 값, DTO 조회하기
// 단순히 값 하나를 조회(JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다)
@Query("select m.username from Member m")
List<String> findUsernameList();
// DTO로 직접 조회
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
// 파라미터 바인딩
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
// 컬렉션 파라미터 바인딩
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
반환 타입
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
스프링 데이터 JPA 공식 문서:
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types
조회 결과가 많거나 없으면?
- 컬렉션
- 결과 없음: 빈 컬렉션 반환
- 단건 조회
- 결과 없음: null 반환
- 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
정렬
순수 JPA와 SpringDataJPA의 페이징을 비교해 보자
- 검색 조건: 나이가 10살
- 정렬 조건: 이름으로 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
순수 JPA 페이징과 정렬
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age",Long.class)
.setParameter("age", age)
.getSingleResult();
}
스프링 데이터 JPA 페이징과 정렬
- 페이징과 정렬 파라미터
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
- 특별한 반환 타입
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회)
- List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환
페이징과 정렬 사용 예제
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
순수 JPA와 비교
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
Page 사용 예제 실행 코드
//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);//then
List<Member> content = page.getContent(); //조회된 데이터
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}
- 두 번째 파라미터로 받은 Pageable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
- PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력 한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.
주의: Page는 1부터 시작이 아니라 0부터 시작이다.
Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
참고: count 쿼리를 다음과 같이 분리할 수 있음
@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
Top, First 사용 참고
List<Member> findTop3By();
페이지를 유지하면서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
주의
- 카운트 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨)
- 실무에서 매우 중요!!!(전체 count 쿼리는 매우 무겁다)
벌크성 수정 쿼리
// 순수 JPA
public int bulkAgePlus(int age) {
int resultCount = em.createQuery(
"update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
// 스프링 데이터 JPA
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true) 이 옵션의 기본값은 false
- 영속성 컨텍스트와 DB의 싱크가 안맞을 수 있음
- 권장하는 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
- 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
순수 JPA의 경우 한번에 조회하려면 페치 조인이 필요하다.
EntityGraph은 사실상 페치 조인(FETCH JOIN)의 간편 버전
LEFT OUTER JOIN 사용
사용 예제
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
NamedEntityGraph 사용 방법
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
// --------------------------------
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA Hint & Lock
JPA Hint
- JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
- org.hibernate.readOnly 쿼리 힌트를 설정하면 데이터를 읽기 전용 모드로 조회할 수 있다.
- 성능 향상: 읽기 전용 모드로 데이터를 조회하면, Hibernate는 해당 데이터의 변경 사항을 추적할 필요가 없게 된다.
- 단점: 수정 불가능
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
forCounting = true 옵션은 페이징 처리를 위한 카운트 쿼리의 성능을 최적화하기 위해 사용되며, ORM 프레임워크가 더 효율적으로 쿼리를 처리할 수 있도록 도와준다.(실제로 뭘 도와주는지는 모르겠다)
Lock
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
- @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션은 비관적 쓰기 락으로 JPA(Java Persistence API)에서 데이터베이스 트랜잭션 내에서 특정 엔티티에 대한 락(lock)을 적용하는 데 사용 된다.
- 비관적 락은 데이터 무결성을 보장하는 강력한 방법이지만, 동시성을 제한하므로 성능에 영향을 줄 수 있다. 따라서 비관적 락을 사용할 때는 애플리케이션의 요구 사항과 성능 영향을 신중하게 고려해야 한다.
사용자 정의 리포지토리 구현
Querydsl, MyBatis등 다른 리포지토리를 쓰고 싶을때 구현
구현 순서
- 아무 이름의 인터페이스 생성
- 만들고 싶은 메서드 생성
- 상속받는 Impl 클래스 생성(규칙: 리포지토리 인터페이스 이름 + Impl, 변경도 가능하지만 굳이..)
- (스프링 데이터 2.x 부터는 ‘사용자 정의 인터페이스’ 명 + Impl 방식도 지원)
- 메서드 구현
- JpaRepository를 상속받은 repository 인터페이스에 커스텀으로 만든 인터페이스를 상속
1. 아무 이름의 인터페이스 생성
public interface MemberRepositoryCustom {
List<Member> findMemberCustom(); // 만들고 싶은 메서드 생성
}
2. 상속받는 Impl 클래스 생성
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
// 예제에선 JPA를 썼지만 Mybatis, QueryDsl등을 구현 할 수 있다.
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
3. JpaRepository를 상속받은 repository 인터페이스에 커스텀으로 만든 인터페이스를 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
Auditing
- 엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하는 기법
- 기존엔 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate로 직접 구현
설정
- 스프링 부트 설정 클래스에 적용
- @EnableJpaAuditing
- 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
- @EntityListeners(AuditingEntityListener.class) 엔티티에 적용
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
어노테이션
- @CreatedDate
- @LastModifiedDate
- @CreatedBy
- @LastModifiedBy
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
참고
실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
전체 적용
@EntityListeners(AuditingEntityListener.class) 를 생략하고 스프링 데이터 JPA 가 제공하는 이벤트를 엔티티 전체에 적용하려면 orm.xml에 다음과 같이 등록하면 된다
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
version="2.2">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
Web 확장 - 도메인 클래스 컨버터
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩(솔직히 안쓸 것 같지만 다른사람이 쓸 수도 있으니 기록..)
엔티티를 파라미터로 받으면 도메인 클래스 컨버터가 중간에 동작.(트랜잭션 범위 밖으로 더티체킹 안됨)
// Before
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
// After
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
Web 확장 - 페이징과 정렬
// 요청 예) /members?page=0&size=3&sort=id,desc&sort=username,desc
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
글로벌 설정
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
개별 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
접두사
- 페이징 정보가 둘 이상이면 접두사로 구분
- @Qualifier 에 접두사명 추가 “{접두사명}_xxx”
- 예제: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
Page 내용을 DTO로 변환하기
- 엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.
- Page는 map() 을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
// 최적화
...
return memberRepository.findAll(pageable).map(MemberDto::new);
...
스프링 데이터 JPA 구현체 분석
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
- @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
- @Transactional 트랜잭션 적용
- JPA의 모든 변경은 트랜잭션 안에서 동작
- 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
- 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
- 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
- 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)
- @Transactional(readOnly = true)
- 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
새로운 엔티티를 구별하는 방법
- save() 메서드
- 새로운 엔티티면 저장( persist )
- 새로운 엔티티가 아니면 병합( merge )
- 새로운 엔티티를 판단하는 기본 전략
- 식별자가 객체일 때 null 로 판단
- 식별자가 자바 기본 타입일 때 0 으로 판단
- Persistable 인터페이스를 구현해서 판단 로직 변경 가능
public interface Persistable<ID> {
ID getId();
boolean isNew();
}
참고
JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.
그런데 JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이있는 상태로 save() 를 호출한다. 따라서 이 경우 merge() 가 호출된다.
merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다.
따라서 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.
참고로 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수있다.
(@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
@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;
}
}