JPA 값 타입

데이터 타입

엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자로 지속해서 추적 가능

값 타입

  • 자바 기본 타입이나 객체
  • 식별자가 없고 값만 있으므로 변경시 추적 불가

값 타입 분류

  1. 기본값 (int, double, Integer, Long, String)
  2. 임베디드
  3. 컬렉션 값

임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있음
  • @Embeddable@Embedded사용

장점

  • 재사용
  • 높은 응집도
  • 값 타입만의 의미 있는 메소드를 만들 수 있음 (객체 지향적)
  • 생명주기는 엔티티에 의존

사용

  • 당하는 객체 클래스는 @Embeddable을 걸어주고 해당 객체를 가진 클래스는 @Embedded사용

매핑

  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능
  • 중복은 에러가 난다.

@AttributeOverride or @AttributeOverrides

  • 속성 재정의
  • 한 엔티티에서 같은 값타입 사용시
  • 컬럼명이 중복
  • 위의 어노테이션을 이용해서 컬럼 속성 재정의

값 타입과 불변 객체

  • 값 타입 공유 참조 : 임베디드 타입은 여러 엔티티에서 공유하면 위험
  • 영속성이 겹쳐서 임베디드에서 set을 했을때 참조하고 있는 모든 내용이 바뀐다.

  • 따라서 값을 복사해서 사용해야함 (객체를 하나 더 생성해서 값을 넣고 거기서 set을 적용)

값 타입의 한계

  • 직접 정의한 값 타입은 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조값을 직접 대입하는것을 막을 방법이 없다.
  • 객체의 공유 참조를 피할 수 없다.

해결방안

  • 객체 타입을 수정할 수 없게 만들어야한다.
  • 값 타입은 불변 객체로 설계
  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 setter를 만들지 않음

컬렉션

  • 값타입들의 컬렉션일 경우 테이블에 각 컬럼은 pk로 구성하여 생성
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요 (일대다로 풀어야한다)
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
            joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
  • @ElementCollection : 컬렉션을 사용할때 사용하는 어노테이션
  • @CollectionTable : 새로 생성된 테이블에 대한 속성
  • @Column(name = “FOOD_NAME”) : 컬럼이 한개 일경우 이름과함께 설정 가능
create table FAVORITE_FOOD (
    MEMBER_ID bigint not null,
    FOOD_NAME varchar(255)
)
  • 위와 같이 테이블이 생성되고 MEMBER_ID와 관계가 연결된걸 볼 수 있다.

저장

-만약 위의 FAVORITE_FOOD에 값을 넣게 된다면아래와 같다

Member member = new Member();
member.setUsername("member1");
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

em.persist(member);
insert
into
    Member
    (createBy, createdDate, lastModifiedBy, lastModifiedDate, age, createDate, description, localDate, localDateTime, roleType, TEAM_ID, name, id)
values
    (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

insert collection
row hellojpa.Member.favoriteFoods */ insert
into
    FAVORITE_FOOD
    (MEMBER_ID, FOOD_NAME)
values
    (?, ?)

insert collection
row hellojpa.Member.favoriteFoods */ insert
into
    FAVORITE_FOOD
    (MEMBER_ID, FOOD_NAME)
values
    (?, ?)

insert collection
row hellojpa.Member.favoriteFoods */ insert
into
    FAVORITE_FOOD
    (MEMBER_ID, FOOD_NAME)
values
    (?, ?)

  • member의 엔티티에 생명주기가 동일

image

조회

  • 지연로딩 전략을 사용하여 먼저 member가 조회되고 값타입을 조회 할때 불러서 사용한다.

Member findMember = em.find(Member.class, member.getId());

Set<String> favoriteFoods = findMember.getFavoriteFoods();
for(String favoriteFood : favoriteFoods){
    System.out.println("favoriteFood = " + favoriteFood);
}
select
    member0_.id as id1_6_0_,
    member0_.createBy as createBy2_6_0_,
    member0_.createdDate as createdD3_6_0_,
    member0_.lastModifiedBy as lastModi4_6_0_,
    member0_.lastModifiedDate as lastModi5_6_0_,
    member0_.age as age6_6_0_,
    member0_.createDate as createDa7_6_0_,
    member0_.description as descript8_6_0_,
    member0_.localDate as localDat9_6_0_,
    member0_.localDateTime as localDa10_6_0_,
    member0_.roleType as roleTyp11_6_0_,
    member0_.TEAM_ID as TEAM_ID13_6_0_,
    member0_.name as name12_6_0_
from
    Member member0_
where
    member0_.id=?


select
    favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
    favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_
from
    FAVORITE_FOOD favoritefo0_
where
    favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자

수정

  • 값타입에 객체로 (@Embedded)로 되어있을경우 불변 객체로 setter를 사용해서 변경 불가능
  • 따라서 객체 인스턴스를 새로 만들어서 (생성자)로 변경된 객체를 만들어 변경해야한다.

  • 객체가 아닌 일반 값타입일 경우 아래와 같이 통쨰로 변경 해야한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
  • 삭제 후 새로 추가

주의

  • 객체 값타입의 경우 지울때 equals와 hashcode가 제대로 구현 되어 있어야 한다.
  • 그렇지 않으면 제대로 동작하지 않는다.

use getters code generate를 사용하여 생성 (프록시로 인해 직접 변수에 접근하는것 보다 getter에 접근하도록 생성해줌)

//예시
@Override
public boolean equals( Object o ) {
    if ( this == o ) return true;
    if ( o == null || getClass() != o.getClass() ) return false;
    Item item = (Item) o;
    return price == item.price && Objects.equals(id, item.id) && Objects.equals(name, item.name);
}

@Override
public int hashCode() {
    return Objects.hash(id, name, price);
}

제약사항

  • 엔티티와 다르게 식별자 개념이 없다.
  • 값 변경시 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 현재 값을 다시 저장.
  • 모든 컬럼을 묶어서 기본키를 구성해야 하므로 null 및 중복 저장을 하면 안된다.

대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계 고려
  • 한 객체를 id가 있는 클래스로 랩핑하고 주인 엔티티와 일대다로 맵핑
  • cascade를 all로 하고 고아객체를 true로 설정

사용 기준

  • 단순한 경우 , 추척이 필요 없을 때 사용