(JPA)값 타입 컬렉션
값 타입 컬렉션
값 타입 컬렉션?
- 값 타입을 하나 이상 저장할 때 사용한다.
- @ElementCollection, @CollectionTable 어노테이션을 사용
- 테이터베이스는 컬렉션을 같은 테이블에 저장할 수 없어, 컬렉션 저장을 위한 별도의 테이블이 필요하다.
값 타입 컬렉션 예제
예제 엔티티 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
@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> addressHistroy = new ArrayList<>();
}
addressHistory
와favoriteFoods
를 값 타입 컬렉션으로 생성- @ElementCollection: 값 타입 컬렉션으로 명시
- @CollectionTable: 테이블 생성 설정
favoriteFoods
는 단 하나의 컬럼만 가지기 때문에 예외적으로@Column
어노테이션을 사용할 수 있다.
값 타입 컬렉션 저장 예제
1
2
3
4
5
6
7
8
9
10
11
12
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "12345"));
member.getFavoriteFoods.add("치킨");
member.getFavoriteFoods.add("족발");
member.getFavoriteFoods.add("피자");
member.getAddressHistroy().add(new Address("old1", "street", "12345"));
member.getAddressHistroy().add(new Address("old2", "street", "12345"));
em.persist(member);
- 값 타입 저장 예제
- 값 타입 컬렉션은 따로
persist
할 필요없이, 값 타입을 가지고 있는 엔티티만persist
하면 자동으로 persist 된다.- 값 타입 컬렉션은 영속성 전이(CASCADE)와 고아 객체 제거 기능을 필수로 가지고 있다고 볼 수 있다.
값 타입 컬렉션 조회 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "12345"));
member.getFavoriteFoods.add("치킨");
member.getFavoriteFoods.add("족발");
member.getFavoriteFoods.add("피자");
member.getAddressHistroy().add(new Address("old1", "street", "12345"));
member.getAddressHistroy().add(new Address("old2", "street", "12345"));
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
- 조회 코드
1
2
3
4
5
6
7
8
9
10
11
Hibernate:
select
member0_.member_id,
member0_.city,
member0_.street,
member0_.zipcode,
member0_.username
from
Member member0_
where
member0_.member_id=?
- 조회 후 발생된 쿼리를 살펴보면,
Member
만 가지고 오는걸 확인할 수 있다. - 위에 발생된 쿼리로 알 수 있는것은 값 타입 컬렉션은 전부 지연 로딩인걸 알 수 있다.
- 임베디드 타입의
Address
의 경우 지연 로딩으로 얻을 수 있는 이점이 없으므로 즉시 로딩된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "12345"));
member.getFavoriteFoods.add("치킨");
member.getFavoriteFoods.add("족발");
member.getFavoriteFoods.add("피자");
member.getAddressHistroy().add(new Address("old1", "street", "12345"));
member.getAddressHistroy().add(new Address("old2", "street", "12345"));
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistroy = findMeber.getAddressHistroy();
for (Address address : addressHistroy) {
System.out.println("address = " + address.getCity());
}
- 값 컬렉션 조회 로직 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Hibernate:
select
member0_.member_id,
member0_.city,
member0_.street,
member0_.zipcode,
member0_.username
from
Member member0_
where
member0_.member_id=?
Hibernate:
select
addresshis0_.member_id,
addresshis0_.city,
addresshis0_.street,
addresshis0_.zipcode,
from
Address addresshis0_
where
addresshis0_.member_id=?
- 조회 후 쿼리를 살펴보면 이제
addressHistory
를 조회하는 것을 확인 할 수 있다.
값 타입 컬렉션 수정 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "12345"));
member.getFavoriteFoods.add("치킨");
member.getFavoriteFoods.add("족발");
member.getFavoriteFoods.add("피자");
member.getAddressHistroy().add(new Address("old1", "street", "12345"));
member.getAddressHistroy().add(new Address("old2", "street", "12345"));
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
/*
값 타입은 불변해야 하기 때문에 이런식의 변경은 side-effect를 발생 시킬 가능성이 있다.
findMember.getHomeAddress().setCity("newCity");
*/
// 새로운 인스턴스로 통째로 갈아 끼워야 한다.
Address old = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", old.getStreet(), old.getZipcode()));
// 치킨 -> 짜장면
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("짜장면");
// equals(), hashCode()가 제대로 구현되어 있어야 한다.
findGetAddressHistroy().remove(new Address("old1", "street", "12345"));
findGetAddressHistroy().add(new Address("newCity1", "street", "12345"));
- 값 타입안에 특정 값만 변경하는 것이 아니라, 전체 값을 갈아 끼워야
side-effect
로 부터 안전하다. favoriteFoods
값 타입 컬렉션은 변경이 아니라 삭제하고, 다시 저장해줘야 한다.- 컬렉션만 변경해도 JPA 알아서 DB 쿼리를 날려 변경시켜 준다.
- 마치 영속성 전이가 된 것 처럼 동작한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Hibernate:
delete
from
address
where
member_id=?
Hibernate:
insert
into
address(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
insert
into
address(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
AddressHistory
값 타입 컬렉션을 수정할때 발생된 쿼리이다.- 예상하기로는
remove()
한old1
값을 가진Address
만delete
되고,newCity
가insert
될 줄 알았는데 쿼리를 살펴보니Address
를 통째로 삭제하고 남아있는old2
와newCity
가insert
되는 것을 확인할 수 있다. - DB를 보면 원했던 대로 이뤄졌으나, 왜 이렇게 동작되는지 의문이 생길 수 있다.
값 타입 컬렉션의 제약 사항
- 값 타입은 Entity 와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 Entity 와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.(null과 중복 값이 허용되지 않는다.)
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에
일대다
또는다대일 양방향
관계를 고려해야 한다. 일대다
관계를 위한 Entity 를 만들고, 여기에서 값 타입을 사용한다.- 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Entity @Table(name = address) public class AddressEntity { @Id @GeneratedValue private Long id; @Embedded private Address address; public AddressEntity(String city, String street, String zipcode) { this.address = new Address(city, street, zipcode); } }
- 일대다 관계를 위한 별도의 엔티티 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
@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> addressHistroy = new ArrayList<>();
*/
@OneToMany(cascae = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "member_id")
private List<AddressEntity> addressEntity = new ArrayList<>();
}
- 기본 멤버 Entity 코드 수정
- 일대다 매핑(1 쪽(Member)에 외래키가 있다.)
정리
값 타입 컬렉션은 정말 단순한 상황에서만 사용하는 것이 좋다.(지속 추적 및 변경이 필요 없어, 식별자가 필요하지 않는 상황)
거의 왠만한건 다 Entity 이다.
- 엔티티 타입의 특징
- 식별자가 존재한다.
- 생명 주기 관리
- 공유 가능하다.
- 값 타입의 특징
- 식별자가 없다.
- 생명주기를 엔티티에 의존한다.
- 공유하지 않는것이 안전하다.(불변 객체로 만들고, 복사해서 사용)
- 엔티티와 값 타입 컬렉션을 혼동해서 사용하면 큰일난다.
REFERENCE
#JPA_값_타입_컬렉션
This post is licensed under CC BY 4.0 by the author.