값 타입을 하나 이상 저장할 때 사용한다.
DB는 컬렉션을 같은 테이블에 저장할 수 있는 방법이 없다.
-> 컬렉션을 저장하기 위한 별도의 테이블을 생성해야한다.
@ElementCollection
, @CollectionTable
어노테이션을 이용한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "name")
private String username;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(
// 테이블 이름을 정의
name = "FAVORITE_FOOD",
// 외래키를 명시
joinColumns = @JoinColumn(name = "MEMBER_ID"))
// addressHistory는 Address 타입 내부에 city, address 등 다양한 필드가 있지만
// favoriteFoods는 String 하나이고 내가 정의한 타입이 아니기 때문에
//예외적으로 칼럼 이름을 지정해줄 수 있다.
@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<>();
}
public class JpaMain {
public static void main(String[] args) {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("home city", "street", "12345"));
// 값 타입 컬렉션
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "12345"));
member.getAddressHistory().add(new Address("old2", "street", "12345"));
// member만 영속
em.persist(member);
tx.commit();
}
}
Member
테이블 생성 후 FavoriteFoods
데이터 3개, AddressHistory
데이터가 2개 insert 된다.
값 타입은 member에 의존하기 때문에 member가 변경되면 같이 변경된다.
-> member와 라이프사이클이 같다.
값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
public class JpaMain {
public static void main(String[] args) {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("home city", "street", "12345"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "12345"));
member.getAddressHistory().add(new Address("old2", "street", "12345"));
em.persist(member);
// DB에는 데이터가 insert되고 영속성 컨텍스트를 초기화
em.flush();
em.clear();
// member를 다시 조회
Member findMember = em.find(Member.class, member.getId());
tx.commit();
}
}
member를 조회하면 값 타입 컬렉션인 favoriteFoods와 addressHistory는 조회되지 않는다.
값 타입 컬렉션은 지연 로딩이 적용된다.
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
이 후 값 타입 컬렉션을 조회할 때, select Query
가 나간다.
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ElementCollection{
Class targetClass () default void.class;
FetcyType fetch() default FetchType.LAZY;
}
@ElementCollection
이 LAZY(지연로딩)
으로 선언되어있는 것을 확인할 수 있다.
public class JpaMain {
public static void main(String[] args) {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("home city", "street", "12345"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "12345"));
member.getAddressHistory().add(new Address("old2", "street", "12345"));
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
// 값 타입인 임베디드 타입은 불변객체이므로 set 메소드가 존재하지 않는다.
// findMember.getHomeAddress().setCity("new city"); X
// address 인스턴스 자체를 갈아끼워야 한다.
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("new city", a.getStreet(), a.getZipcode()));
// 컬렉션 값 타입도 불변객체이다.
// 기존 값을 지우고 다시 넣는다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// 임베디드 타입과 컬렉션 값 타입을 영속화 하는 코드가 없지만 쿼리가 나간다.
// 영속성 전이와 고아 객체 제거 기능을 필수로 가지기 때문이다.
tx.commit();
}
}
컬렉션의 값만 변경해도 어떤 값이 변경되었는지 알고 JPA가 DB에 쿼리를 날려준다.
-> 마치 영속성 전이가 된 것처럼 동작한다.
값 타입 컬렉션은 member 엔티티로 라이프 사이클이 관리되기 때문이다.
값 타입 컬렉션의, 원소 하나의 필드 하나가 아니라 원소 하나를 통째로 수정하고 싶다면?
Member findMember = em.find(Member.class, member.getId());
// address를 하나만 바꾸고 싶다면 지우고 싶은 값을 넣고 remove 한다.
// 컬렉션은 대부분 equals()를 사용해 찾고 싶은 값을 그대로 찾아준다.
// 따라서 equals()를 재정의 하지 않았다면 그냥 망하는 것이다. equals()를 꼭 재정의 해주자.
findMember.getAddressHistory().remove(new Address("old1", "street", "12345"));
// 지운 값 대신 새로운 값을 넣어준다.
findMember.getAddressHistory().add(new Address("new city", "street", "12345"));
tx.commit();
값 타입은 엔티티와 다르게 식별자 개념이 없다.
-> 값은 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고,
값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
값 타입 컬렉션을 매핑하는 테이블은 모든 칼럼을 묶어서 기본 키를 구성해야한다.
null 입력 X, 중복 저장 X
Adress Entity : 엔티티를 만들고 Address
값 타입을 필드로 이용한다.
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
}
Member
@Entity
public class Member {
...
@OneToMany(cascade = ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
@OneToMany
어노테이션 +
영속성 전이 : Cascade = ALL
, 고아 객체 제거 : orphanRemoval = true
를 이용해 값 타입 컬렉션처럼 사용한다.
update Query
가 나가는 것은 어쩔 수 없다.업데이트, 추적이 필요 없는 단순한 상황일 때 사용
ex) 셀렉트 박스에서 치킨, 피자, 족발 중 선택
그런 경우가 아니라면 웬만하면 Entity로 사용한다.
꼭 값을 변경할 일이 없더라도 쿼리 자체를 그 테이블에서 할 때가 많다면 Entity로 하는 게 좋다.
ex) 주소는 입력만 하지만 조회할 일이 많으므로 Entity로 만든다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아니라 Entity이다.
참고 :
김영한. 『자바 ORM 표준 JPA 프로그래밍』. 에이콘, 2015.