문제
우리는 JPA 엔티티를 Ksuid라고 하는 고유 식별자를 사용하기로 했다. UUID 같은거다.
생성시간 기반의 20byte 고유 식별자로, 시간순으로 정렬이 가능해서 인덱싱의 이점을 누리면서도 랜덤값도 포함되어 중복 가능성이 거의 없으며, 길이도 짧다.
따라서 엔티티를 저장할 때 id를 넣어서 생성하도록 했다.
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) {
this.entityManager.persist(entity);
return entity;
} else {
return this.entityManager.merge(entity);
}
}
하지만 JpaRepository의 save 메서드는 isNew 메서드를 통해 현재 엔티티가 새로 생성되었는지 여부를 확인하는데,
@Transient
public boolean isNew() {
return this.getId() == null;
}
그 조건은 id가 null 인 경우다.
우리는 id를 넣어준 후 save 하기 때문에 항상 isNew가 false 가 되어 persist가 아닌 merge를 실행하게 된다.
select c1_0.id,ci1_0.coupon_id,ci1_0.id,ci1_0.status,ci1_0.user_id,c1_0.max_quantity,c1_0.name,c1_0.now_quantity,c1_0.version from coupons c1_0 left join coupon_issues ci1_0 on c1_0.id=ci1_0.coupon_id where c1_0.id=?
insert into coupons (max_quantity,name,now_quantity,version,id) values (?,?,?,?,?)
그러면 생성 전에, id가 있는지 여부를 확인하기 위해 select 문이 나가고, id가 없으면 Insert 문이 나가게 된다.
해결
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostPersist;
import lombok.Getter;
import org.springframework.data.domain.Persistable;
@Getter
@MappedSuperclass
public abstract class BaseEntity implements Persistable<String> {
private transient boolean isNew = true; // 새로운 엔티티 여부를 추적할 필드
@Override
public boolean isNew() {
return this.isNew;
}
// persist 후에 기존 엔티티로 인식되도록 설정
@PostPersist
public void postPersist() {
this.isNew = false;
}
}
모든 엔티티에 대해서 Ksuid를 사용하기 때문에, 이를 BaseEntity에 넣어서 각각의 엔티티가 중복 구현하지 않도록 했다.
우선 Persistable<T> 인터페이스를 구현했는데, 여기서 T는 키의 타입을 넣어주면 된다.
isNew 라는 변수를 통해 현재 엔티티가 새로 생성되었는지 아닌지를 판단하도록 했고, private를 통해 자식 엔티티가 접근하지 못하도록 하고, transient를 통해 DB에 실제로 저장이 되지 않도록 했다. 그리고 기본값으로 true를 두었다.
다음으로 @PostPersist 어노테이션을 통해, 새로 생성 시 isNew는 false가 되도록 했고, isNew 메서드를 override 해서 변수를 리턴하도록 했다.
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@Table(name = "coupons")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Coupon extends BaseEntity { // 여기!!!
@Id
private String id;
private String name;
private Integer nowQuantity;
private Integer maxQuantity;
@OneToMany(mappedBy = "coupon", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
private Set<CouponIssue> couponIssues;
public static Coupon create(String id, String name, Integer nowQuantity, Integer maxQuantity) {
Coupon coupon = new Coupon();
coupon.id = id;
coupon.name = name;
coupon.nowQuantity = nowQuantity;
coupon.maxQuantity = maxQuantity;
return coupon;
}
}
그리고 실제 엔티티에서는 BaseEntity를 상속받아서 사용하도록 했다.
이러면 엔티티에서는 생성인지 영속인지 모르고 사용할 수 있다!!
insert into coupons (max_quantity,name,now_quantity,version,id) values (?,?,?,?,?)
insert 문만 나가는 것을 볼 수 있다!!
'TIL ✍️' 카테고리의 다른 글
TIL #113 : Assertj로 LocalDateTime.now() 단위 테스트 코드 작성하기 (0) | 2024.11.13 |
---|---|
TIL #112 : 자바 애플리케이션을 도커 이미지로 만들 때 용량 줄이기 (6) | 2024.11.13 |
24/10/04(금) 110번째 TIL : 컴포넌트 스캔 패키지 구조 문제 (0) | 2024.10.07 |
24/09/30(월) 109번째 TIL : 카프카 직렬화 및 역직렬화 (0) | 2024.10.01 |
24/08/28(수) 108번째 TIL : EmbeddedId 식별자 값객체 (0) | 2024.09.09 |