1. 값 타입 컬렉션
JPA에서 엔티티가 여러 개의 동일한 값 타입을 컬렉션으로 하나 이상 저장할 때 사용한다.
말이 좀 헷갈리니 바로 코드로 보자.
![](https://blog.kakaocdn.net/dn/b4F67I/btsIyHDyAps/nT2YK1QopUATYR1wb9o3OK/img.png)
✅값 타입 컬렉션 사용 예시
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
@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> addressesHistory = new ArrayList<>();
public void setHomeAddress(Address homeAddress){
this.homeAddress = homeAddress.copy();
}
}
- @ElementCollection을 사용한다.
- 컬렉션을 저장하기 위해 별도의 테이블이 필요하다. -> @CollectionTable로 테이블명 지정.
✅main 메서드(저장, 조회, 수정)
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
//멤버 엔티티 저장
Member member = new Member();
member.setUsername("memberA");
member.setHomeAddress(new Address("homeCity","street","10000"));
//FavoriteFood 저장
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
//Address 저장
member.getAddressesHistory().add(new Address("oldCity1","street","10000"));
member.getAddressesHistory().add(new Address("oldCity2","street","10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("======= 조회 START =======");
Member findMember = em.find(Member.class, member.getId());
List<Address> addressesHistory = findMember.getAddressesHistory();
for (Address address : addressesHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
//수정 (List<Address>)
findMember.getAddressesHistory().remove(new Address("oldCity1", "street", "10000"));
findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));
//수정 (Set<String>)
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("햄버거");
tx.commit();
}catch (Exception e){
tx.rollback();
e.printStackTrace(); //예외 출력
}finally {
em.close();
}
}
- 값 타입 컬렉션도 기본적으로 지연 로딩 전략을 사용한다.
- set으로 수정을 하는 것이 아니라 remove()로 삭제를 했다가 add()로 추가해줘야한다.
✅수정쿼리 비교(List<Address> VS Set<String>)
✔️List<Address> 수정 쿼리
Hibernate:
/* one-shot delete for hellojpa.Member.addressesHistory */
delete from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert for
hellojpa.Member.addressesHistory */
insert into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
hellojpa.Member.addressesHistory */
insert into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
findMember.getAddressesHistory().remove(new Address("oldCity1", "street", "10000"));
findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));
위 코드를 보면,
remove()하는 값과 동일한걸 찾아서 해당하는 값만 삭제하고 add()하는 값만 삽입할 것 같은데, 실제 동작하는 쿼리는 다르다.
임베디드 타입 컬렉션에 변경 사항이 생기면, 주인 엔티티(여기서는 findMember)와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
💡실무에서는 값 타입 컬렉션 대신에 일대다 관계를 사용한다. (영속성 전이, 고아객체 제거 옵션을 사용해서 값 타입 컬렉션처럼 사용)
✔️Set<String> 수정 코드
Hibernate:
/* delete for hellojpa.Member.favoriteFoods */
delete from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert for
hellojpa.Member.favoriteFoods */
insert into
FAVORITE_FOOD (MEMBER_ID, FOOD_NAME)
values
(?, ?)
- WHERE 조건절에 FOOD_NAME을 비교하는 조건이 있으니 이 값 타입은 원하는 값만 지우고 원하는 값만 삽입한다.
2. 값 타입 컬렉션 특징
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값을 변경하면 추적이 어렵다.
- 생명 주기를 엔티티에 의존한다.
- 공유하지 않는 것이 안전하다.(복사해서 사용)
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야한다.
3. 값 타입 컬렉션 대신 일대다 관계 사용하기
List<Address> 수정 쿼리를 보면 주인 엔티티와 관련된 모든 데이터를 삭제한 후, 값 타입 컬렉션에 있는 모든 값을 다시 저장하는 것을 볼 수 있다.
이 문제를 해결하기 위해 실무에서는 일대다 관계로 풀어내기도 한다.
✅AddressEntity 엔티티 추가
@Entity
@Getter
@Setter
@Table(name="ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity() {
}
public AddressEntity(Address address) {this.address = address;}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
- AddressEntity를 만들어 식별자로 테이블을 관리할 수 있다.
✅Member 엔티티 수정
// @ElementCollection
// @CollectionTable(name = "ADDRESS"
// , joinColumns = @JoinColumn(name = "MEMBER_ID"))
// private List<Address> addressesHistory = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressesHistory = new ArrayList<>();
- 값 타입 컬렉션 대신 일대다 관계로 매핑했다.
- 영속성 전이, 고아 객체 제거 옵션을 추가해서 값 타입 컬렉션처럼 사용할 수 있다.
4. 주의할 점
- 값 타입은 정말 값 타입일때만 사용한다.
- 식별자가 필요하고, 지속적으로 값을 추적하고 변경해야한다면 값타입이 아닌 엔티티다.
'인프런 김영한 강의 정리 > 자바 ORM 표준 JPA 프로그래밍 기본편' 카테고리의 다른 글
JPA 기본 | JPQL 기본 문법(1) 쿼리 API, 파라미터 바인딩 (0) | 2024.07.15 |
---|---|
JPA 기본 | 객체지향 쿼리 언어 알아보기(JPQL, QueryDSL 등) (0) | 2024.07.15 |
JPA 기본 | 데이터 타입(2) - 값 타입 공유와 불변객체 (0) | 2024.07.14 |
JPA 기본 | 데이터 타입(1) - 기본 값과 임베디드 값 타입 (0) | 2024.07.14 |
JPA 기본 | 영속성 전이(CASCADE)와 고아 객체 (0) | 2024.07.13 |