(5) 값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection
, @CollectionTable
어노테이션을 사용하면 된다.
값 타입 컬렉션
import java.util.ArrayList;@Entitypublic class Member {@Id@GeneratedValueprivate Long id;@Embeddedprivate Address homeAddress;@ElementCollection@CollectionTable(name = "FAVORITE_FOODS",joinColumns = @JoinColumn(name = "MEMBER_ID"))@Column(name = "FOOD_NAME") // FAVORITE_FOODS 테이블에 값으로 사용되는 컬럼이 FOOD_NAME 하나 뿐이라서 @Column을 사용해 컬럼명을 지정할 수 있다.private Set<String> favoriteFoods = new HashSet<String>();@ElementCollection@CollectionTable(name = "ADDRESS",joinColumns = @JoinColumn(name = "MEMBER_ID"))private Set<Address> addressHistory = new ArrayList<Address>();//...}@Embeddablepublic class Address {@Columnprivate String city;private String street;private String zipcode;//...}
데이터베이스 테이블로 매핑 시 컬럼 안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @CollectionTable
를 사용해서 추가한 테이블을 매핑해야 한다.
@CollectionTable
를 생략하면 기본값을 사용해서 매핑한다.기본값 : {엔티티 이름}_{컬렉션 속성 이름}
ex) Member 엔티티의
addressHistory
는Member_addressHistory
테이블과 매핑한다.
테이블 매핑정보는 @AttributeOverride
를 사용해서 재정의할 수 있다.
9.5.1 값 타입 컬렉션 사용
Member member = new Member();// 임베디드 값 타입member.setHomeAddress(new Address("통영", "뭉돌해수욕장", "660-123"));// 기본값 타입 컬렉션member.getFavoriteFoods().add("짬뽕");member.getFavoriteFoods().add("짜장");member.getFavoriteFoods().add("탕수육");// 임베디드 값 타입 컬렉션member.getAddressHistory().add(new Address("서울", "강남", "123-123"));member.getAddressHistory().add(new Address("서울", "강북", "000-000"));em.persist(member);
마지막에 member
엔티티만 영속화했다. => JPA는 이때 member
엔티티의 값 타입도 함께 저장한다.
실제 데이터베이스에 실행되는 INSERT SQL
- member : INSERT SQL 1번
- member.homeAddress : 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함.
- member.favoriteFoods : INSERT SQL 3번
- member.addressHistory : INSERT SQL 2번
따라서 em.persist(member) 한 번 호출로 총 6번의 INSERT SQL을 실행한다. (영속성 컨텍스트 플러시할 때 SQL을 전달)
값 타입 컬렉션은
영속성 전이(Cascade)
+고아 객체 제거(ORPHAN REMOVE)
기능을 필수로 갖는다.
값 타입 컬렉션도조회 시 패치 전략을 선택할 수 있는데, LAZY
가 기본이다.
조회
// 1. member : 회원만 조회. 이때 임베디드 값 타입인 homeAddress도 함께 조회 => SELECT SQL 1번 호출Member member = em.find(Member.class, 1L);// 2. member.homeAddressAddress homeAddress = member.getHomeAddress();// 3. member.favoriteFoods : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출Set<String> favoriteFoods = member.getFavoriteFoods(); // LAZYfor(String favoriteFood : favoriteFoods){System.out.println("favoriteFood = " + favoriteFood);}// 4. member.addressHistory : LAZY로 설정해서 실제 컬렉션 사용할 때 SELECT SQL 1번 호출List<Address> addressHistory = member.getAddressHistory(); // LAZYaddressHistory.get(0);
수정
Member member = em.find(Member.class, 1L);// 1. 임베디드 값 타입 수정member.setHomeAddress(new Address("새로운 도시", "신도시1", "123456"));// 2. 기본값 타입 컬렉션 수정Set<String> favoriteFoods = member.getFavoriteFoods();favoriteFoods.remove("탕수육");favoriteFoods.add("치킨");// 3. 임베디드 값 타입 컬렉션 수정List<Address> addressHistory = member.getAddressHistory();addressHistory.remove(new Address("서울", "기존 주소", "123-123"));addressHistory.add(new Address("새로운 도시", "새로운 주소", "123-456"));
1. 임베디드 값 타입 수정
homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다
- 사실 Member 엔티티를 수정하는 것과 같다.
2. 기본값 타입 컬렉션 수정
- 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다.
- 자바의 String 타입은 수정할 수 없다.
3. 임베디드 값 타입 컬렉션 수정
- 값 타입은 불변해야 해서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록한다.
- 이때 값 타입은
equals()
,hashCode()
를 꼭 구현해야 한다.
9.5.2 값 타입 컬렉션의 제약사항
엔티티는 식별자가 있어서 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있다.
반면 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로, 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기가 어렵다.
특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 된다.
문제는 값 타입 컬렉션이다. 값 타입 컬렉션
에 보관된 값 타입들은 별도의 테이블에 보관되기 때문에 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다.
따라서 이런 문제를 해결하기 위해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성
해야 한다. 따라서 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복으로 저장할 수 없는 제약도 있다.
실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계
를 고려해야 한다.
그리고 추가적으로 영속성 전이(Cascade)
+ 고아 객체 제거(ORPHAN REMOVE)
기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
import java.util.ArrayList;@Entitypublic class AddressEntity {@Id@GeneratedValueprivate Long id;@EmbeddedAddress address;...}public class Member {...@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)@JoinColumn(name = "MEMBER_ID")private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();}