DB를 설계하다 보면 엔티티와 라이프 사이클을 함께 하지만 식별자를 가지지 않고 오직 값만 가지고 있는 Value Type(값 타입)
을 종종 사용하게 된다. 본 포스트는 Entity와 Value Type을 매핑하는 다양한 방법에 대해서 공부하기 위해 작성했다.
152.5cm
라는 하나의 컬럼으로 표현할 수 있다.@Entity
class Member {
@Id @Column(name="member_id")
private Long id;
@Column(name="member_length")
private Length length;
}
@Data
class Length {
private double value;
private String unit;
public Length getLength() {
return Double.toString(value) + unit;
}
public Length(String value){
this.value = Double.valueOf(value.substring(0, value.length()-2));
this.unit = value.substring(value.length()-2);
}
}
@Converter(autoApply=true)
public class LengthConverter implements AttributeConverter<Length, String> {
@Override
public String convertToDatabaseColumn(Length length) {
return length == null ? null : length.getLength(length);
}
@Override
public Length convertToEntityAttribute(String value){
return value == null ? null : new Length(value);
}
}
AttributeConverter
를 구현해서 사용할 수 있다.@Converter
의 autoApply
의 기본 값은 false
이고, 만약 false
를 사용한다면 Length Field에 다음과 같이 @Converter
어노테이션을 달아줘야 한다.@Convert(converter = LengthConverter.class)
private Length length;
@Entity
class Member {
@Id @Column(name="member_id")
private Long id;
@ElementColection(fetch = FetchType.EAGER)
@CollectionTable(name = "name_car",
joinColumns = @JoinColumn(name = "member_id"))
@OrderColumn(name = "car_idx")
private List<Car> cars;
}
@Embeddable
public class Car {
@Column(name = "number")
private int number;
}
@OrderColumn
@CollectionTable
AttributeConverter
를 활용한다.@Entity
class Member {
@Id @Column(name="member_id")
private Long id;
@Column(name="member_car")
@Convert(converter = CarSetConverter.class)
private Carset cars;
}
@Data
class Car {
private int number;
}
@Data
class CarSet {
private Set<Car> cars = new HashSet<>();
public CarSet(Set<Car> cars){
this.cars.addAll(cars);
}
}
public class CarSetConverter implements AttributeConverter<CarSet, String> {
@Override
public String convertToDatabaseColumn(CarSet attribute) {
if(attribute == null) return null;
return attribute.getCars().stream()
.map(car -> Integer.toString(car.getNumber()))
.collect(Collectors.joining(","));
}
@Override
public CarSet convertToEntityAttribute(String dbData){
if(dbData == null) return null;
String[] numbers = dbData.split(",");
Set<Car> carSet = Arrays.stream(numbers)
.map(number -> new Car(Integer.valueOf(number.getNumber)))
.collect(Collectors.toSet());
return new CarSet(carSet);
}
}
@Id
대신 @EmbeddedId
를 사용한다.@Entity
class Member {
@EmbeddedID
private MemberId id;
}
@Embeddable
class MemberId implements Serializable{
private String name;
private String number;
}
Serializable
이어야 하기 때문에 MemberId
이라는 Value Type에 Serializable
인터페이스를 꼭 상속받아야 한다.@Embeddable
를 선언해줘야 한다.@Data
@Entity
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
@Id
@Column(name = "id")
private String id;
private String title;
@AttributeOverrides({
@AttributeOverride(
name = "content",
column = @Column(table = "article_content", name = "content")),
@AttributeOverride(
name = "contentType",
column = @Column(table = "article_content", name = "content_type"))
})
@Embedded
private ArticleContent content;
}
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class ArticleContent {
private String content;
private String contentType;
}
ArticleContent
테이블에 Aticle
의 PK를 의미하는 id 컬럼이 생긴다.Article fineOne = articleService.findById("id");
Article
를 조회했을 때 @SecondaryTable
의 밸류타입을 조인해서 가지고 오기 때문에 필요하지 않은 데이터도 읽어 온다는 문제가 있다.ArticleContent
를 Entity로 만들어서 매핑하고 지연 로딩으로 설정하는 방법이 있지만 이는 좋은 방법이 아니다. @Entity
로 매핑하기@Entity
를 사용해야 할 때도 있다.@Data
@Entity
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "article_id")
)
public class Article {
@Id
@Column(name = "article_id")
private String id;
private String title;
@AttributeOverrides({
@AttributeOverride(
name = "content",
column = @Column(table = "article_content", name = "content")),
@AttributeOverride(
name = "contentType",
column = @Column(table = "article_content", name = "content_type"))
})
@Embedded
private ArticleContent content;
@OneToMany(
cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true)
@JoinColumn(name = "article_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
}
@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
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, Date uploadTime) {
this.path = path;
this.uploadTime = uploadTime;
}
protected String getPath() {
return this.path;
}
public Date getUploadTime() {
return this.uploadTime;
}
public abstract String getURL();
public abstract boolean hasThumbnail();
public abstract String getThumbnailURL();
}
Image
는 업로드 방식에 따라 InternalImage와 ExternalImage 로 나뉘어 진다.Image
는 Article
과 라이프 사이클을 동일하게 가져가기 때문에 orphanRemoval, cascade
를 설정해줬다.@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image{
...
}
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image{
...
}
@OneToMany
컬렉션 매핑에서는 컬렉션 clear()
시 삭제 과정이 효율적이지 않다. Image
테이블에 총 4 개의 컬럼이 존재하고 List<Image>
를 삭제하게 되면 Image
의 컬럼들을 조회하기 위한 select 쿼리 1번
, 4 개의 컬럼을 삭제하기 위한 delete 쿼리 4번
이 발생하게 된다.@OneToMany
방식이 아닌 @Embeddable
를 이용해 단일 클래스로 밸류 타입을 매핑해줘야 한다. JPA는 @Embedded
컬렉션을 삭제할 때는 컬렉션에 속한 객체들을 조회하지 않고 1번의 delete 쿼리
로 데이터를 삭제한다.@Data
@Embeddable
public abstract class Image {
@Column(name = "image_type")
private String imageType;
@Column(name = "image_path")
private String path;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "upload_time")
private Date uploadTime;
...
// 성능을 위해 상속을 포기하고 if~else 사용
public boolean hasThumbnail(){
if(imageType.equals("II")){
return true;
}
return false;
}
}
@Entity
public class Product {
@EmbeddedId
private ProductId id;
@ElementCollection
@CollectionTable(
name="product_category",
joinColumns=@JoinColumn(name="product_id"))
)
private Set<CategoryId> categoryIds;
}
Product
에서Category
로의 M-N연관은 ID 참조 방식으로 구현되었다. 밸류 컬렉션 매핑과 동일한 방식으로 설정되었지만, 차이점이 있다면 컬렉션의 원소 타입으로 Category
의 객체 타입이 아닌 식별자가 들어간다는 점이다.