이 글은 김영한의 [자바 ORM 표준 JPA 프로그래밍 - 기본편]을 수강하며 정리한 글입니다.
👉 기본 환경
- Language: Java
- DB: H2 Database
- IDE: IntelliJ
값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- 데이터베이스는 컬렉션을 같은 테이블로 저장할 수 없으므로, 별도의 테이블 필요
⌨️ 코드
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
|
@Entity
public class MemberClctn {
@Id
@GeneratedValue
private long id;
private String username;
// Address
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "id"))
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "id"))
private List<Address> addressesHist = new ArrayList<>();
}
|
🖨️발생한 쿼리
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
35
36
|
Hibernate:
create table MemberClctn (
id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
username varchar(255),
primary key (id)
)
Hibernate:
create table FAVORITE_FOOD (
id bigint not null,
favoriteFoods varchar(255)
)
Hibernate:
alter table FAVORITE_FOOD
add constraint FK9inywg52proq6ctbba5w6i06l
foreign key (id)
references MemberClctn
Hibernate:
create table ADDRESS (
id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
alter table ADDRESS
add constraint FKaxd8wbkc203qq3eky4kjiv7mo
foreign key (id)
references MemberClctn
|
값 타입 컬렉션 라이프 사이클 = Entity 라이프 사이클
⭐ 값 타입은 별도로 라이프 사이클을 갖지 않음
⌨️ 코드
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
35
|
public class Main {
public static void main(String[] args) {
// 생략
try {
MemberClctn mClctn = new MemberClctn();
mClctn.setUsername("user");
mClctn.setHomeAddress(new Address("homeCity", "street", "10000"));
mClctn.getFavoriteFoods().add("pizza");
mClctn.getFavoriteFoods().add("pasta");
mClctn.getFavoriteFoods().add("risotto");
mClctn.getAddressesHist().add(new Address("old1", "street", "10000"));
mClctn.getAddressesHist().add(new Address("old2", "street", "10000"));
em.persist(mClctn);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
|
🖨️발생한 쿼리
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
Hibernate:
/* insert jpa_basic.MemberClctn
*/ insert
into
MemberClctn
(city, street, zipcode, username, id)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.addressesHist */ insert
into
ADDRESS
(id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.addressesHist */ insert
into
ADDRESS
(id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.favoriteFoods */ insert
into
FAVORITE_FOOD
(id, favoriteFoods)
values
(?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.favoriteFoods */ insert
into
FAVORITE_FOOD
(id, favoriteFoods)
values
(?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.favoriteFoods */ insert
into
FAVORITE_FOOD
(id, favoriteFoods)
values
(?, ?)
|
mClctn insert 시, 자동으로 insert
⭐ 값 타입 컬렉션: 지연 로딩 전략
⌨️ 코드
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
public class Main {
public static void main(String[] args) {
// 생략
try {
MemberClctn mClctn = new MemberClctn();
mClctn.setUsername("user");
mClctn.setHomeAddress(new Address("homeCity", "street", "10000"));
mClctn.getFavoriteFoods().add("pizza");
mClctn.getFavoriteFoods().add("pasta");
mClctn.getFavoriteFoods().add("risotto");
mClctn.getAddressesHist().add(new Address("old1", "street", "10000"));
mClctn.getAddressesHist().add(new Address("old2", "street", "10000"));
em.persist(mClctn);
em.flush();
em.clear();
System.out.println("========== START ==========");
MemberClctn findMClctn = em.find(MemberClctn.class, mClctn.getId());
System.out.println("========== addressesHist ==========");
List<Address> addressesHist = findMClctn.getAddressesHist();
for(Address address : addressesHist){
System.out.println("Address: " + address.getCity());
}
System.out.println("========== favoriteFoods ==========");
Set<String> favoriteFoods = findMClctn.getFavoriteFoods();
for(String favoriteFood : favoriteFoods){
System.out.println("food: " + favoriteFood);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
|
🖨️발생한 쿼리
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
35
36
37
38
39
40
|
========== START ==========
Hibernate:
select
memberclct0_.id as id1_7_0_,
memberclct0_.city as city2_7_0_,
memberclct0_.street as street3_7_0_,
memberclct0_.zipcode as zipcode4_7_0_,
memberclct0_.username as username5_7_0_
from
MemberClctn memberclct0_
where
memberclct0_.id=?
========== addressesHist ==========
Hibernate:
select
addressesh0_.id as id1_0_0_,
addressesh0_.city as city2_0_0_,
addressesh0_.street as street3_0_0_,
addressesh0_.zipcode as zipcode4_0_0_
from
ADDRESS addressesh0_
where
addressesh0_.id=?
Address: old1
Address: old2
========== favoriteFoods ==========
Hibernate:
select
favoritefo0_.id as id1_4_0_,
favoritefo0_.favoriteFoods as favorite2_4_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.id=?
food: risotto
food: pizza
food: pasta
|
조회 시 마다 SELECT QUERY 발생
🚨 값 타입의 일부 수정 시에는 setter가 아닌 새로운 instance 생성
⌨️ 코드
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
35
36
37
38
39
40
41
42
43
44
45
46
|
public class Main {
public static void main(String[] args) {
// 생략
try {
MemberClctn mClctn = new MemberClctn();
mClctn.setUsername("user");
mClctn.setHomeAddress(new Address("homeCity", "street", "10000"));
mClctn.getFavoriteFoods().add("pizza");
mClctn.getFavoriteFoods().add("pasta");
mClctn.getFavoriteFoods().add("risotto");
mClctn.getAddressesHist().add(new Address("old1", "street", "10000"));
mClctn.getAddressesHist().add(new Address("old2", "street", "10000"));
em.persist(mClctn);
em.flush();
em.clear();
System.out.println("========== START ==========");
MemberClctn findMClctn = em.find(MemberClctn.class, mClctn.getId());
System.out.println("========== From old1 To old2 ==========");
findMClctn.getAddressesHist().remove(new Address("old1", "street", "10000"));
// remove: equals(o, get(i))로 동작하므로 equals가 올바르게 정의되어 있어야 함
findMClctn.getAddressesHist().add(new Address("new1", "street", "10000"));
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
|
🖨️발생한 쿼리
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
|
Hibernate:
/* delete collection jpa_basic.MemberClctn.addressesHist */ delete
from
ADDRESS
where
id=?
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.addressesHist */ insert
into
ADDRESS
(id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row jpa_basic.MemberClctn.addressesHist */ insert
into
ADDRESS
(id, city, street, zipcode)
values
(?, ?, ?, ?)
|
🚨 Address의 내용이 모두 delete 후, insert가 2번 실행 됨
⭐ 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장
- 값 변경 시 추적을 위한 id가 없어 찾아서 지우기 쉽지 않음
- → @OrderColumn을 통해서 순번을 부여해서 update를 수행할 수 있지만 권장 X
1
2
3
4
5
6
7
8
|
Hibernate:
create table ADDRESS (
id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
|
⭐ 상황에 따라서 값 타입 컬렉션 대신 일대다 관계를 고려
⌨️ 코드
AddressEntity
1
2
3
4
5
6
7
8
9
10
|
@Entity
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
}
|
MemberClctn
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
|
@Entity
public class MemberClctn {
@Id
@GeneratedValue
@Column(name = "MCLCTN_NAME")
private long id;
private String username;
// Address
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "id"))
private Set<String> favoriteFoods = new HashSet<>();
// @ElementCollection
// @CollectionTable(name = "ADDRESS",
// joinColumns = @JoinColumn(name = "id"))
// private List<Address> addressesHist = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MCLCTN_NAME")
private List<AddressEntity> addressesHist = new ArrayList<>();
}
|
Main
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
35
36
37
38
39
40
41
|
public class Main {
public static void main(String[] args) {
// 생략
try {
MemberClctn mClctn = new MemberClctn();
mClctn.setUsername("user");
mClctn.setHomeAddress(new Address("homeCity", "street", "10000"));
mClctn.getFavoriteFoods().add("pizza");
mClctn.getFavoriteFoods().add("pasta");
mClctn.getFavoriteFoods().add("risotto");
mClctn.getAddressesHist().add(new AddressEntity("old1", "street", "10000"));
mClctn.getAddressesHist().add(new AddressEntity("old2", "street", "10000"));
em.persist(mClctn);
em.flush();
em.clear();
System.out.println("========== START ==========");
MemberClctn findMClctn = em.find(MemberClctn.class, mClctn.getId());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
|
🤓 실행 결과, update query 발생 이유
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Hibernate:
/* create one-to-many row jpa_basic.MemberClctn.addressesHist */ update
ADDRESS
set
MCLCTN_NAME=?
where
id=?
Hibernate:
/* create one-to-many row jpa_basic.MemberClctn.addressesHist */ update
ADDRESS
set
MCLCTN_NAME=?
where
id=?
|
@OneToMany와 관련된 엔티티 관계로 설정되어 있으면 JPA는 이러한 관계를 변경 감지 대상으로 인지
해당 컬렉션에 엔티티를 추가하면 JPA는 해당 변경을 데이터베이스에 적용하기 위해 업데이트 쿼리를 생성
= MemberClct 엔티티 인스턴스를 저장한 후에 AddressEntity 인스턴스들이 해당 MemberClct 인스턴스를 참조하도록 갱신하는 작업이 발생
⭐ 식별자가 필요하고, 지속적으로 값을 추적, 변경해야 한다면 값 타입이 아닌 엔티티 사용
'Java > JPA' 카테고리의 다른 글
[JPA_Basic] 페이징 (0) | 2023.09.23 |
---|---|
[JPA_Basic] 프로젝션 - 여러 값 조회 (1) | 2023.09.22 |
[JPA_Basic] 값 타입과 불변 객체 (0) | 2023.09.14 |
[JPA_Basic] Cascade (0) | 2023.09.11 |
[JPA_Basic] N+1 문제 (0) | 2023.09.10 |