1. 값 타입 컬렉션
이전 포스팅에서 설명한 값 타입을 컬렉션으로 사용하는 방법에 대해서 알아보겠습니다.
- 값 타입을 하나 이상 저장할 때 사용
- @ElementCollection, @CollectionTable 사용
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요
Member가 favoriteFoods와 addressHistory라는 값 타입 컬렉션을 가지고 있을 때 RDB에서는 favoriteFoods, addressHistory라는 별도의 테이블로 빼서 관리를 합니다. 값 타입을 묶어서 하나의 PK를 만들어냅니다.
여기에 식별자 ID같은 개념을 넣어서 걔를 PK로 쓰게되면, 얘는 값타입이 아니라 ENTITY가 됩니다.
값타입은 값들만 저장하고 PK로 구성하면 됩니다.
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
favoriteFoods는 String이라는 값 하나이고 내가 정의한게 아니니까 컬럼명을 지정할 수 있습니다. 임베디드 타입은 테이블의 컬럼으로 들어가고, 타입 컬렉션은 새로운 테이블 생성되는 차이가 있습니다. 컬렉션들은 일대다 개념이기 때문에 한테이블에 넣을 방법이 없습니다. 그래서 일대다로 풀어서 별도의 테이블로 만들어 냅니다.
2. 값 타입 컬렉션 변경 예제
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode");
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "zipcode");
member.getAddressHistory().add(new Address("old2", "street", "zipcode");
em.persist(member);
한가지 흥미로운점은 값 타입 컬렉션을 persist 안했는데 자동으로 persist 된걸 확인할 수 있습니다. 이것은 값 타입이기 때문입니다. 값 타입 컬렉션도 본인 스스로의 라이프사이클이 없습니다. member에 소속되어 있습니다. 값 타입들은 멤버에서 값이 바뀌면 자동으로 업데이트됩니다.
DB에서 조회해기위해, em.flush(), emclear() 후 member를 조회해보겠습니다.
member.getAddressHistory().add(new Address("old1", "street", "zipcode");
member.getAddressHistory().add(new Address("old2", "street", "zipcode");
em.persist(member);
em.flush();
em.clear();
System.out.println("================== START ===================");
Member findMember = em.find(Member.class, member.getId());
콘솔창에 찍힌 메세지를 보면 member만 가지고 옵니다. 그 말은 이 컬렉션들은 지연로딩입니다.
이제 값 타입을 수정해보겠습니다. homeCity에서 newCity로 바꿔보겠습니다.
//homeCity -> newCity
findMember.getHomeAddress().setCity("newCity");
값 타입이라는 것은 이뮤터블해야합니다. 기대한대로 업데이트문이 나갑니다. 그런데 값타입은 사이드 이펙트가 생깁니다. 이런식으로 변경하면 절대 안됩니다.
업데이트를 위해서 값을 새로 넣어야합니다.
//homeCity -> newCity
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode());
값타입이라는거 자체가 추적도 안되기 떄문에 이런식으로 교체를 해줘야합니다. 이제 값타입 컬렉션을 업데이트 해보겠습니다. favoriteFoods는 단순 스트링이라 remove로 치킨을 지우고 새로 add해서 한식을 넣어야합니다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
컬렉션의 값만 변경해도 jpa가 뭐가 변경됐는지 보고 db에 반영합니다. 이제 주소를 바꿔보겠습니다. 대부분 컬렉션을 찾을때 equals를 활용합니다.
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));
이렇게 넣으면 기존에 있던거를 지워줍니다. equals와 hash를 제대로 넣어놔야합니다. 그리고 새로 add해주면 됩니다.
지금 보면 MEMBER_ID를 기준으로 ADDRESS테이블을 통째로 지웁니다. 그리고 데이터를 INSERT를 2번합니다.
테이블에 있는 데이터를 완전히 갈아낍니다. 한식 치킨에서는 됐는데 여기서는 안됩니다. 결론적으로는 원하는데로 데이터가 수정되었습니다. 값 타입은 엔티티와 다르게 식별자 개념이 없습니다. 값을 변경하면 추적이 어렵습니다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 결론적으로는 이걸 쓰면 안됩니다.
테이블 보면 이거 컬럼 4개만 있는데 변경되면 추적이 안됩니다. OrderColumn 넣고하면 어떻게든 풀 수있습니다. 다 지우고 하는게 아니라 update 쿼리가 나갑니다.
@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
컬렉션 순서 값이 들어갑니다. pk를 member_id와 address_history_order 2개로 잡습니다. 그런데 이것도 위험합니다. 의도하지 않게 동작하는 경우도 많습니다. 결론적으로는 이렇게 복잡하게 쓰려면 완전히 다르게 풀어야합니다.
[값 타입 컬렉션의 제약 사항]
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X
[값 타입 컬렉션 대안]
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬 렉션 처럼 사용
- EX) AddressEntity
@Entity
public class AddressEntity{
@Id
@GenerateValue
private Long id;
private Address address;
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
}
member에서 AddressEntity로 매핑합니다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistroy = new ArrayList<>();
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
값 타입 컬렉션을 쓸때가 있기는합니다. 진짜 단순한, 예를 들어 셀렉트 박스에 사용할 때 입니다. 이런 단순하고 값이 바뀌어도 업데이트 할 필요가 없을 때 사용합니다.
3. 정리
엔티티 타입의 특징
- 식별자O
- 생명 주기 관리
- 공유
값 타입의 특징
- 식별자X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
REFERENCE
https://www.inflearn.com/course/ORM-JPA-Basic
'JPA' 카테고리의 다른 글
[JPA] JPQL(Java Persistence Query Language) 문법(2) (0) | 2020.04.30 |
---|---|
[JPA] JPQL(Java Persistence Query Language) 문법(1) (0) | 2020.04.30 |
[JPA] 값타입과 불변 객체 (0) | 2020.04.26 |
[JPA] 임베디드 타입(복합 값 타입) (0) | 2020.04.26 |
[JPA] 고아객체 (0) | 2020.04.22 |