컬렉션 값 타입이란
값 타입을 컬렉션에 담아 사용하는것을 말한다.
값 타입 하나 이상을 저장할 때 사용한다.
예를들어, 내가 좋아하는 책 , 혹은 주소 이력을 보관한다고 하면 별도의 테이블을 생성해서 데이터를 관리해야한다.
개념적으로 보면 1:M 관계가 형성된다.
데이터베이스 컬렉션을 같은 테이블에 저장할 수 없다, 따라서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
@ElementCollection 과 @CollectionTable 어노테이션을 사용한다.
@ElementCollection
@CollectionTable(name = "FAVORITE_BOOK", //매핑 정보(컬렉션의 테이블명)
joinColumns = @JoinColumn(name = "USER_ID")) //USER_ID를 FK로 잡게된다.
@Column(name = "BOOK_NAME")
private Set<String> favoriteBooks = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "USER_ID"))
private List<Address> addressHistory = new ArrayList<>();
@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String username;
@ElementCollection
@CollectionTable(name = "FAVORITE_BOOK", //매핑 정보(컬렉션의 테이블명)
joinColumns = @JoinColumn(name = "USER_ID")) //USER_ID를 FK로 잡게된다.
@Column(name = "BOOK_NAME")
private Set<String> favoriteBooks = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "USER_ID"))
private List<Address> addressHistory = new ArrayList<>();
}
[실행 후 콘솔 결과] CREATE (ADDRESS / FAVORITE_BOOK)
create table ADDRESS ( -- @CollectionTable에 name속성으로 지정한 테이블명이 설정된다.
USER_ID bigint not null, -- @JoinColumn에 지정한 값이 추가되고 추후 FK로 설정된다.
CITY varchar(255),
STREETvarchar(255),
ZIPCODE varchar(255)
)
create table FAVORITE_BOOK ( -- @CollectionTable에 name속성으로 지정한 테이블명이 설정된다.
USER_ID bigint not null, -- @JoinColumn에 지정한 값이 추가되고 추후 FK로 설정된다.
BOOK_NAME varchar(255)
)
값 타입 저장 테스트
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
EntityManager em;
@Test
public void valueTypeCollectionTest() {
User user = new User();
user.setUsername("userA");
//HashSet에 add()
user.getFavoriteBooks().add("JPA");
user.getFavoriteBooks().add("SPRINGBOOT");
user.getFavoriteBooks().add("REACT");
user.getFavoriteBooks().add("TYPESCRIPT");
//List에 add()
user.getAddress().add(new Address("city", "street", "12345"));
user.getAddress().add(new Address("country", "load", "54321"));
em.persist(user);
}
}
[실행 후 콘솔 결과] - INSERT (ADDRESS / FAVORITE_BOOK)
Hibernate:
/* insert collection
row hellojpa.USER.addressHistory */ insert
into
ADDRESS
(USER_ID, city, street, zipcode)
values (1, "city", "street", "12345")
Hibernate:
/* insert collection
row hellojpa.USER.addressHistory */ insert
into
ADDRESS
(USER_ID, city, street, zipcode)
values (1, "city", "street", "54321")
Hibernate:
/* insert collection
row hellojpa.USER.favoriteBooks*/ insert
into
FAVORITE_BOOK
(USER_ID, BOOK_NAME
values (1, "JPA")
Hibernate:
/* insert collection
row hellojpa.USER.favoriteBooks*/ insert
into
FAVORITE_BOOK
(USER_ID, BOOK_NAME
values (1, "SPRINGBOOT")
Hibernate:
/* insert collection
row hellojpa.USER.favoriteBooks*/ insert
into
FAVORITE_BOOK
(USER_ID, BOOK_NAME
values (1, "REACT")
Hibernate:
/* insert collection
row hellojpa.USER.favoriteBooks*/ insert
into
FAVORITE_BOOK
(USER_ID, BOOK_NAME
values (1, "TYPESCRIPT")
값 타입은 선언한 엔터티에 종속적이게 되므로, 따로 저장하거나 조회하지 않고 엔터티를 기준으로 접근해서 저장,조회를 해야한다.
값 타입 컬렉션은 영속성 전에 (Cascade) + 고아객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 조회 테스트
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
EntityManager em;
@Test
public void valueTypeCollectionTest() {
User user = new User();
user.setUsername("userA");
//HashSet에 add()
user.getFavoriteBooks().add("JPA");
user.getFavoriteBooks().add("SPRINGBOOT");
user.getFavoriteBooks().add("REACT");
user.getFavoriteBooks().add("TYPESCRIPT");
//List에 add()
user.getAddressHistory().add(new Address("city", "street", "12345"));
user.getAddressHistory().add(new Address("country", "load", "54321"));
em.persist(user);
em.flush();
em.clear();
User findUser = em.find(User.class, user.getId());
System.out.println(findUser.getAddressHistory());
System.out.println(findUser.getFavoriteBooks());
}
}
[콘솔 결과 출력]
/* ...User Select 생략 ...*/
/* === === address Select === === */
select
addresshis0_.user_id as user_id1_0_0_,
addresshis0_.city as city2_0_0_,
addresshis0_.street as street3_0_0_,
addresshis0_.zipcode as zipcode4_0_0_
from
address addresshis0_
where
addresshis0_.user_id=1
address = Address(city=city, street=street, zipcode=12345)
address = Address(city=country, street=load, zipcode=54321)
/* === favorite_book Select === */
select
favoritebo0_.user_id as user_id1_1_0_,
favoritebo0_.book_name as book_nam2_1_0_
from
favorite_book favoritebo0_
where
favoritebo0_.user_id=1
favoriteFood = SPRINGBOOT
favoriteFood = JPA
favoriteFood = REACT favoriteFood = TYPESCRIPT
값 타입 수정 테스트
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
EntityManager em;
@Test
public void valueTypeCollectionTest() {
/* === 저장 === */
User user = new User();
user.setUsername("userA");
//HashSet에 add()
user.getFavoriteBooks().add("JPA");
user.getFavoriteBooks().add("SPRINGBOOT");
user.getFavoriteBooks().add("REACT");
user.getFavoriteBooks().add("TYPESCRIPT");
//List에 add()
user.getAddressHistory().add(new Address("city", "street", "12345"));
user.getAddressHistory().add(new Address("country", "load", "54321"));
em.persist(user);
em.flush();
em.clear();
/* === 조회 === */
User findUser = em.find(User.class, user.getId());
System.out.println(findUser.getAddressHistory()); //DELETE 쿼리 호출
System.out.println(findUser.getFavoriteBooks()); //INSERT 쿼리 호출
/* === 수정 === */
//[FavoriteBooks] JPA -> 나는 미남이다
findUser.getFavoriteBooks().remove("JPA");
findUser.getFavoriteBooks().add("나는 미남이다");
//[AddressHistory] city -> universe
findUser.getAddressHistory().remove(new Address("city", "street", "12345")); //DELETE 쿼리 호출
findUser.getAddressHistory().add(new Address("universe", "street", "12345")); //INSERT 쿼리 호출
}
}
위 코드를 실행하게 되면 수정 부분에서 DELETE한 뒤 INSERT로 새로 추가하게 된다.
컬렉션에서 remove를 할때는 들어있는 값과 동일한 객체를 매개변수로 넣어주면 해당 데이터를 찾아 지워준다.
값 타입 컬렉션에서 값을 수정할 때는 UPDATE쿼리가 아닌 DELETE 후 INSERT하는 방식으로 매커니즘으로 동작한다.
이때 전체를 DELETE한 뒤 원래 남아있던 데이터를 INSERT하고 새로운 데이터를 INSER하게된다.
결론적으로는 값이 변경이 되었다.
이것은 마치 CASCADE의 고아객체 제거기능이 작동된것 처럼 동작 한 것이다?
값 타입 컬렉션에 변경 사항이 발생하면 주인 엔터티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
(저장할때는 컬렉션에 남아 있는 데이터를 저장한다.)
값 타입은 엔터티와 다르게 식별자 개념이 없기 때문에 값을 변경하면 추적이 어렵다.
PK와 같은 ID 값이 존재하지 않기 때문에 중간에 변경되면 추적이 어렵기 때문에 사용하지 않는게 바람직하다.
@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "USER_ID"))
private List<Address> addressHistory = new ArrayList<>();
위와 같이 @OrderColumn 어노테이션으로 지정하게되면 자동 증가 컬럼이 추가되지만, 의도하지 않은대로 동작한다.
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다.
(null 불가, 중복 불가 - @ColumnDefinition?)
값 타입 컬렉션 대안
실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려한다.
일대다 관계를 위한 엔티티를 만들고, 값타입을 사용한다.
@Entity
@Getter @Setter
public class AddressHistory {
@Id @GeneratedValue
@Column(name = "address_history_id")
private Long id;
@Embedded
private Address address; // Address 엔터티를 매핑
public AddressHistory() {
}
public AddressHistory(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
@Entity
@Getter
@Setter
@ToString
public class FavoriteFood {
@Id
@GeneratedValue
@Column(name = "favorite_food_id")
private Long id;
private String bookName;
public FavoriteFood(String bookName) {
this.bookName = bookName;
}
public FavoriteFood() {
}
}
User엔터티에 아래와 같이 1:M 매핑을 걸어준다.
@Entity(name = "USERS")
@Setter
@Getter
@ToString
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String username;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "user_id")
private Set<FavoriteFood> favoriteBooks = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "user_id")
private List<AddressHistory> addressHistory = new ArrayList<>(); //AddressHistory로 변경
}
이때 cascade옵션과 orphanRemoval옵션을 설정해줘야한다.
(추가 정리 필요)
[CREATE 쿼리 발생]
create table address_history (
address_history_id bigint not null,
city varchar(255),
street varchar(255), zipcode varchar(255),
user_id bigint,
primary key (address_history_id)
)
create table favorite_food (
favorite_food_id bigint not null,
book_name varchar(255),
user_id bigint,
primary key (favorite_food_id)
)
[테스트 코드]
@SpringBootTest
@Transactional
public class JpaTest {
@PersistenceContext
EntityManager em;
@Test
public void valueTypeCollectionTest() {
/* === 저장 === */
User user = new User();
user.setUsername("userA");
//HashSet에 add()
user.getFavoriteBooks().add("JPA");
user.getFavoriteBooks().add("SPRINGBOOT");
user.getFavoriteBooks().add("REACT");
user.getFavoriteBooks().add("TYPESCRIPT");
//List에 add()
user.getAddressHistory().add(new AddressHistory("city", "street", "12345"));
user.getAddressHistory().add(new AddressHistory("country", "load", "54321"));
em.persist(user);
em.flush();
em.clear();
/* === 조회 === */
User findUser = em.find(User.class, user.getId());
System.out.println(findUser.getAddressHistory()); //DELETE 쿼리 호출
System.out.println(findUser.getFavoriteBooks()); //INSERT 쿼리 호출
/* === 수정 === */
//[FavoriteBooks] JPA -> 나는 미남이다
findUser.getFavoriteBooks().remove("JPA");
findUser.getFavoriteBooks().add("나는 미남이다");
//[AddressHistory] city -> universe
findUser.getAddressHistory().remove(new AddressHistory("city", "street", "12345")); //DELETE 쿼리 호출
findUser.getAddressHistory().add(new AddressHistory("universe", "street", "12345")); //INSERT 쿼리 호출
}
}
[UPDATE 쿼리 발생]
/* address_history */
update
address_history
set
user_id=1
where
address_history_id=2
update
address_history
set
user_id=1
where
address_history_id=3
/* favorite_food */
update
favorite_food
set
user_id=1
where
favorite_food_id=4
update
favorite_food
set
user_id=1
where
favorite_food_id=5
update
favorite_food
set
user_id=1
where
favorite_food_id=6
update
favorite_food
set
user_id=1
where
favorite_food_id=7
'Hibernate > JPA(EntityManager)' 카테고리의 다른 글
[JPA] JPA에서 Proxy 객체란? (0) | 2023.07.13 |
---|---|
[JPA] 상속 관계 매핑 @Inheritance , @Discriminator____ (0) | 2023.06.30 |
[JPA] 값 타입 객체간 비교 (equlas & hashcode) (0) | 2023.06.29 |
[JPA] 임베디드 내장 타입 @Embadded 와 불변 객체 (0) | 2023.06.29 |
[JPA] @ManyToMany와 한계점 극복 (0) | 2023.06.29 |