[DDD START] 4장 리포지터리와 모델구현(JPA 중심) - Entity와 Value 매핑 구현
29 Jun 2019Entity와 Value 기본 매핑 구현
- 애그리거트 루트는 Entity이므로 @Entity로 매핑 설정한다
주문 애그리거트의 루트 Entity인 Order는 JPA의 @Entity로 매핑한다
@Entity
@Table(name = "purchase_order")
public class Order {
...
}
한 테이블에 Entity와 Value데이터가 같이 있다면
- Value는 @Embeddable로 매핑 설정한다
- Value 타입 프로퍼티는 @Embedded로 매핑 설정한다
Order에 속하는 Orderer는 Value이므로 @Embeddable로 매핑한다
@Embeddable
public class Orderer {
// MemberId에 정의된 칼럼 이름을 변경하기 위해
// @AttributeOverride 사용
@Embedded
@AttributeOverrides(
@AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
)
private MemberId memberId;
@Column(name = "orderer_name")
private String name;
}
@Embeddable
public class MemberId implements Serializable {
@Column(name = "member_id")
private String id;
}
Order 애그리거트 루트 Entity는 @Embedded를 이용해서 Value타입 프로퍼티를 설정한다
@Entity
public class Order {
...
@Embedded
private Orderer orderer;
@Embedded
private ShippingInfo shippingInfo;
...
}
Value 컬렉션을 별도 테이블에 매핑할 때
- Order Entity는 한 개 이상의 OrderLine을 가질 수 있다
- @ElementCollection과 @CollectionTable을 함께 사용한다
@Entity
@Table(name = "purchase_order")
public class Order {
...
@ElementCollection
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
@OrderColumn(name = "line_idx")
private List<OrderLine> orderLines;
}
@Embeddable
public class OrderLine {
@Embedded
private ProductId productId;
private Monen price;
private int quantity;
private Money amounts;
}
Value 컬렉션을 한 개 칼럼에 매핑할 때
- AttributeConverter를 사용한다
Value를 별도 테이블에 매핑할 때
- Article과 ArticleContent를 별도 테이블에 저장하고자할 때
- @SecondaryTable과 @AttributeOverride를 사용한다
@Entity
@Table(name = "article")
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
@Id
private Long id;
private String title;
@AttributeOverrides({
@AttributeOverride(name = "content", column = @Column(table = "article_content")),
@AttributeOverride(name = "contentType", column = @Column(table = "article_content"))
})
private ArticleContent content;
}
@SecondaryTable을 이용할 때 조회 성능
- @SecondaryTable을 이용하면 아래 코드를 실행할 때 두 테이블을 조인해서 데이터를 조회한다
// @SecondaryTable로 매핑된 article_content 테이블을 조인
Article article = entityManager.find(Article.class , 1L);
게시글 목록을 보여주는 화면에 article_content 데이터가 필요하지 않다면 조회 전용 기능을 구현한다
Value 컬렉션을 @Entity로 매핑하기
구현 기술의 한계나 팀 표준 때문에 @Entity를 사용해야 할 때도 있다
- 상속 구조 Value 타입을 사용할 때
- JPA는 @Embeddable 타입의 클래스 상속 매핑을 지원하지 않는다
- 따라서 상속 구조 Value 타입을 사용하려면 @Embeddable 대신 @Entity를 이용한 상속 매핑을 처리해야 한다
예)제품의 이미지 업로드 방식에 따라 이미지 경로와 썸네일 이미지 제공 여부가 달라질 때 Image 클래스 구현
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE): 한 테이블에 Image 및 하위 클래스를 매핑한다
- @DiscriminatorColumn: 타입을 구분하는 용도로 사용할 칼럼을 지정한다
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
@Table(name = "image")
public abstract class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "image_id")
private Long id;
@Column(name = "image_path")
private String path;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "upload_time")
private Date uploadTime;
protected Image() {}
public Image(String path) {
this.path = path;
this.uploadTime = new Date();
}
protected String getPath() {
return path;
}
public Date getUploadTime() {
return uploadTime;
}
public abstract String getUrl();
public abstract boolean hasThumbnail();
public abstract String getThumbnailUrl();
}
자식 클래스 구현
- @Entity와 @DiscriminatorValue를 사용해서 매핑을 설정한다
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image {
protected InternalImage() {}
public InternalImage(String path) {
super(path);
}
@Override
public String getUrl() {
return "/images/original/" + getPath();
}
@Override
public boolean hasThumbnail() {
return true;
}
@Override
public String getThumbnailUrl() {
return "/images/thumbnail/"+getPath();
}
}
@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image {
protected ExternalImage() {}
public ExternalImage(String path) {
super(path);
}
@Override
public String getUrl() {
return getPath();
}
@Override
public boolean hasThumbnail() {
return false;
}
@Override
public String getThumbnailUrl() {
return null;
}
}
Image를 사용하는 Product Entity에서 매핑 처리
- Image는 Value이므로 독자적인 라이프사이클을 갖지 않고 Product에 완전히 의존해야 한다
- CascadeType.PERSIST: Product를 저장할 때 함께 저장
- CascadeType.REMOVE: Product를 삭제할 때 함께 삭제
- orphanRemoval = true: 리스트에서 Image를 제거하면 DB에서 함께 삭제
@Entity
@Table(name = "product")
public class Product {
@EmbeddedId
private ProductId id;
...
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
...
public void changeImages(List<Image> newImages) {
images.clear();
images.addAll(newImages);
}
...
}
@OneToMany 컬렉션의 삭제 비효율성
- 하이버네이트에서 위 소스의 changeImages() 메소드를 호출할 경우 clear() 메서드 실행 시
- select 쿼리로 대상 Entity를 로딩하고
- 각 개별 Entity에 대해 delete 쿼리를 실행한다
- 즉 images에 보관된 Image가 4개이면 5번의 쿼리가 실행된다
Image 클래스 구현을 @Entity가 아닌 @Embeddable로 바꾸면
- 하이버네이트는 @Embeddable 타입에 대한 컬렉션의 clear() 메서드를 호출하면 컬렉션 객체에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제 처리를 수행한다
- 하지만 다형성을 포기해야 한다
@Embeddable로 구현
@Embeddable
public class Image {
private String imageType;
@Column(name = "image_path")
private String path;
public boolean hasThumbnail() {
// 성능을 위해 다형을 포기하고 if-else로 구현
if (imageType.equals("TI")) {
return true;
} else {
return false;
}
}
}
images의 변경 빈도에 따라 성능과 다형성(코드 유지보수)을 고려해서 구현 방식을 선택해야 한다