2025. 2. 28. 16:56ㆍJPA
◎ 값 타입 컬렉션
-> 값 타입을 컬렉션에 담아서 사용하는 방식으로 값 타입을 하나 이상 저장할 때 사용한다.
-> 데이터베이스에는 같은 테이블에 컬렉션을 저장할 수 없다. 따라서 컬렉션을 저장하기 위한 별도의 테이블을 생성해서 저장해야 한다.
-> 다음은 값 타입 저장 예제다.
@Entity
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "name")
private String username;
// Period
@Embedded
private Period workPeriod;
// Address
@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> addressHistory = new ArrayList<>();
// 값타입 컬렉션 저장
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);
tx.commit();
- 결과
- member만 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());
// 컬렉션 값 타입은 지연로딩으로 아래 코드에서 컬렉션 테이블에 있는 실제 값을 가져올 때 select 쿼리가 나간다.
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);
}
tx.commit();
- 결과
====================== START ==================
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name,
m1_0.name,
m1_0.endDate,
m1_0.startDate
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
Hibernate:
select
ah1_0.MEMBER_ID,
ah1_0.city,
ah1_0.street,
ah1_0.zipcode
from
ADDRESS ah1_0
where
ah1_0.MEMBER_ID=?
address.getCity() = old1
address.getCity() = old2
Hibernate:
select
ff1_0.MEMBER_ID,
ff1_0.FOOD_NAME
from
FAVORITE_FOOD ff1_0
where
ff1_0.MEMBER_ID=?
favoriteFood = 치킨
favoriteFood = 햄버거
favoriteFood = 피자
- 값 타입 컬렉션은 기본적으로 지연 로딩이기 때문에 컬렉션 내의 값을 조회할 때 select 쿼리가 나간다.
-> 다음은 값 타입 수정 예제다.
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());
// homeCity -> newCity
// findMember.getHomeAddress().setCity("newCity"); // -> 값 타입은 불변해야 한다.
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
// 값타입 컬렉션 수정 (치킨 -> 한식)
// 기존에 있는 값을 지우고 수정하고자 하는 값을 새로 추가헤야 한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// 주소 변경(old1 -> new1)
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
- 컬렉션 값을 수정하려면 기존 값을 지우고 수정하고자 하는 값을 새로 저장해야 한다.
- 이 방식대로 실행하면 값을 수정할 때 update 쿼리가 아니라 delete를 해서 기존 값을 지우고 insert를 통해 수정 값을 저장하도록 한다.
Hibernate:
/* one-shot delete for hellojpa.Member.addressHistory */delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert for
hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
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
(?, ?)
-> 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
◎ 값 타입 대안
-> 값 타입 컬렉션 대신 엔티티를 만들어 일대다 관계를 형성하여 값 타입을 사용한다.
@Entity
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "name")
private String username;
// Period
@Embedded
private Period workPeriod;
// Address
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
// 값 타입 컬렉션 대안
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
// 컬렉션 값 타입 대안
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "10000"));
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "10000"));
- 결과
- update 쿼리가 나간다.
◎ 값 타입 컬렉션의 제약사항
-> 값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 이 값을 변경하면 추적하기 어렵다.
-> 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
-> 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어 기본 키를 구성해야 한다.(null 입력x, 중복 저장x)
★ 참고
'JPA' 카테고리의 다른 글
값 타입1 (0) | 2025.02.27 |
---|---|
영속성 전이 CASCADE & 고아 객체 (0) | 2025.02.27 |
즉시 로딩 & 지연 로딩 (0) | 2025.02.26 |
JPA 프록시 (0) | 2025.02.22 |
@PrePersist (0) | 2023.05.17 |