- 값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용한다.
- 값 타입 컬렉션은 관계형 데이터 베이스에 담을 수 없다. 값만 넣을 수 있다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
- 값 타입, 엔티티를 구분해야 한다.
- 클래스 타입으로 컬렉션을 매핑하면 해당 속성들을 컬럼명으로 사용하고, String과 같이 데이터가 단일일 경우 해당 필드명을 컬럼명으로 사용하는 것이 기본이고 예외적으로 @Column(name = "XXX")를 통해 컬럼명을 지정할 수 있다.
- @ElementCollection, @CollectionTable 사용한다.
- 값 타입 컬렉션 사용
1. 값 타입 저장
2. 값 타입 조회 - 값 타입 컬렉션도 지연 로딩 전략 사용한다.
3. 값 타입 수정
- 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제 거 기능을 필수로 가진다고 볼 수 있다.
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")//예외적으로 컬럼명을 지정할 수 있다.
private Set<String> favoriteFoods = new HashSet<>();
- Set을 통한 String 컬렉션 생성
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
- List를 통한 Address 임베디드 타입 컬렉션 생성
- 위와 같이 실행하게 되면 JPA에서 컬렉션에 해당하는 두개의 테이블을 생성하는 것을 볼 수 있다.
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
//
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
//
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
- 컬렉션은 별도로 persist를 해주지 않아도 쿼리가 동작하였다.
- 값 타입의 생명주기는 엔티티에 있기 때문에 엔티티에 속해 있으면 엔티티의 값이 변경되거나 하면 자동으로 해당 부분의 쿼리가 자동으로 실행된다.
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
//
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
//
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
//
em.persist(member);
//
em.flush();
em.clear();
//
System.out.println("===============START======================");
Member findMember = em.find(Member.class, member.getId());
- 영속성 컨텍스트를 초기화 하고 find를 실행하게 되면, member만 가져오게 된다.
- 즉, 값 타입 컬렉션은 지연 로딩이 기본값인 걸 알 수 있다.
// 조회
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address.getCity() = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
- 컬렉션에 있는 값들을 조회하게 되면 그제서야 쿼리를 날려 데이터들을 가져오는 것을 확인 할 수 있다.
- 값 타입 수정
//값 수정
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
//수정의 경우 완전히 교체를 해주어야 한다.
- 값 타입 수정의 경우 같은 엔티티를 통해 값을 수정해 줄 경우 종속된 다른 데이터까지 변경될 수 있기 때문에 반드시 인스턴스를 새롭게 생성하여 교체해주어야 한다.
- 컬렉션 값 타입 수정
//값 타입 음식 컬렉션 수정
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
//아예 값을 삭제하고 새롭게 넣어야 한다.
- 기존의 값을 delete로 삭제하고 새롭게 입력한 값을 insert로 넣어준다.
- 임베디드 타입 컬렉션 수정
//값 타입 주소 컬렉션 수정
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
//equals로 실행되기 때문에 equals 오버라이드를 해야한다.
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
- ADDRESS 자체를 delete하는 것을 볼 수 있다.
- 또한 하나의 값을 생성하였는데 두번의 insert문이 실행되는 것을 확인 할 수 있다.
* 하나의 값을 삭제하고 하나의 값을 넣었는데 insert 쿼리가 두번 나가는 것이 이상하게 보일 수 있다.
- 값 타입 컬렉션의 제약사항
1. 값 타입은 엔티티와 다르게 식별자 개념이 없다.
2. 값은 변경하면 추적이 어렵다.
3. 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
4. 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력X, 중복 저장X
- 즉, ADDRESS와 관련된 모든 데이터를 삭제한다.
- 또한 실무에선 결국 사용하지 않는 것이 좋다.
- 값 타입 컬렉션 대안
1. 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
2. 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
3. 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
4. EX) AddressEntity
// @ElementCollection
// @CollectionTable(name = "ADDRESS", joinColumns =
// @JoinColumn(name = "MEMBER_ID"))
// private List<Address> addressHistory = new ArrayList<>();
// // 결국 안쓴다.
//
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
- 값 타입 ADDRESS를 매핑할 ADDRESS 관련 엔티티를 새롭게 하나 생성해준다.
// member.getAddressHistory().add(new Address("old1", "street", "10000"));
// member.getAddressHistory().add(new Address("old2", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
- update 쿼리가 나가는 이유는 일대다 @OneToMany는 외래키가 다른 테이블에 있기 때문에 값을 변경해야 하기 때문이다.
- 정리
1. 엔티티 타입의 특징
1.1. 식별자 O
1.2. 생명 주기 관리
1.3. 공유
2. 값 타입의 특징
2.1. 식별자 X
2.2. 생명 주기를 엔티티에 의존
2.3. 공유하지 않는 것이 안전(복사해서 사용)
2.4. 불변 객체로 만드는 것이 안전
출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (자바 ORM 표준 JPA 프로그래밍 - 기본편)
'Programming > JPA' 카테고리의 다른 글
JPA - 기본 문법 (조인, 서브쿼리, 타입 표현과 기타식) (0) | 2023.07.18 |
---|---|
JPA - 기본 문법과 기능 (기본 문법, 프로젝션, 페이징) (0) | 2023.07.18 |
JPA - 값 타입과 불변 객체, 값 타입의 비교 (0) | 2023.07.17 |
JPA - 임베디드 타입 (0) | 2023.07.17 |
JPA - 영속성 전이(CASCADE)와 고아 객체 (0) | 2023.07.17 |