값 타입 - 값 타입 컬렉션(2)
"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."
www.inflearn.com/course/ORM-JPA-Basic#
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그
www.inflearn.com
이번엔 값 타입 컬렉션 에서의 데이터를 수정해보자.
기본적인 임베디드 타입의 경우 이전의 강의에서 들은 바와 같이 아래의 코드에서 처럼 완전히 싹 갈아 끼워야 한다.
- JpaMain.java
Address a = findMember.getHomeAddress();
// 아예 새로 만들어야 한다.
findMember.setHomAddress(new Address("new City", a.getStreet(), a.getZipcode()));
- hibernate SQL
Hibernate:
/* update // update query 전달
hellojpa.Member */ update
Member
set
city=?,
street=?,
ZIPCODE=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
String 과 같은 일반적인 값 타입의 컬렉션의 경우 아래와 같은 코드를 작성해 줄 수 있다.
- JpaMain.java
// 치킨 -> 한식 변경
findMember.getFavoriteFood().remove("치킨");
findMember.getFavoriteFood().add("한식");
// 아예 통째로 삭제하고 다시 넣어야 한다.(String 자체가 값 타입이기 때문에 아예 통째로 갈아끼워야 한다.)
- hibernate SQL
Hibernate: // 기존에 있던 데이터를 삭제한 다음
/* delete collection row hellojpa.Member.favoriteFood */ delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate: // 새로운 데이터를 넣는다.
/* insert collection
row hellojpa.Member.favoriteFood */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
그리고 임베디드 타입으로 이루어진 컬렉션 데이터를 수정할 경우 아래와 같이 코드를 작성해 줄 수 있다.
- Address.java
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
- JpaMain.java
// 주소를 바꿔보자. old1 -> new1
findMember.getAddressHistory().remove(new Address("old1", "street", "10000")); // 통째로 갈아끼운다.
// remove 에서 삭제하는 객체를 찾을 때 내부적으로 equals 메소드로 동작하기 때문에
// Address 도메인 클래스 자체에 equals, hashCode 메소드를 오버라이드 해서 넣어주어야 한다.(아니면 망함)
// 두 메소드가 제대로 들어가 있지 않으면 값이 지워지질 않는다.(컬렉션을 다룰 때 의미가 있다.)
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
- hibernate SQL
Hibernate:
/* delete collection hellojpa.Member.addressHistory */ delete
from
ADDRESS // ADDRESS 테이블에 있는 데이터들을 전부 날려버렸다??
where
MEMBER_ID=?
Hibernate: // 거기다가 코드로 삽입한 데이터는 하나인데, insert SQL 은 두번이 나왔다
/* insert collection
row hellojpa.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
위의 hibernate SQL 에서 insert 문이 두 번 나온 것을 볼 수 있다.
insert 문이 두번 나온 이유는 다음과 같다.
- 일단 첫번째 delete SQL 문에서 ADDRESS 컬렉션 테이블(addressHistory 객체) 안에 있는 데이터들이 모두 날아갔다.
그렇기 때문에 현재 ADDRESS 테이블은 텅 비어있는 상태이다.
- 그런데 테이블이 비워지기 전에 테이블에 데이터가 2개 존재했었다.(old1, old2)
- 위의 코드에서 삭제한 건 old1 이었기에 old2 데이터는 여전히 테이블에 남아있어야 한다는 소리이다.
- 그렇기 때문에 ADDRESS 테이블에 있던 데이터들이 전부 날아간 다음, 기존에 있던 데이터 old2 를 복구시켜 주기 위해 old2 데이터에 대한 insert SQL 하나, 그리고 새로 삽입되는 데이터인 newCity1 에 대한 insert SQL 하나, 총 2개의 insert SQL 문이 DB 에 전달되게 된 것이다.
* 값 타입 컬렉션의 제약 사항
- 값 타입은 Entity 와 다르게 식별자 개념이 없어서 값을 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 Entity 와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
-> 여기서 감을 잡아야 하는것?? -> 이거 쓰면 안된다.
첫번째로 문제는 임베디드 타입으로 생성되는 자바 컬렉션 테이블이다.
Hibernate:
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
ZIPCODE varchar(255)
)
위와 같이 컬럼들이 구성되어 있는 경우, 값이 변경되면 추적이 불가능해진다.
삽입되어 있는 데이터 별로 식별할 수 있는 식별자가 존재하지 않기 때문이다.
따로 해결법이 있긴 하지만 그마저도 위험하기 때문에 복잡하게 쓸 거면 아예 다르게 써야 한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다 : null 입력 X, 중복 저장 X
결과적으로 위의 hibernate SQL 에서 나와있는 4가지 컬럼으로 기본 키를 만들어야 한다.
(JPA 는 어떻게 될지 모르기 때문에 기본 키를 만들어주지 않은 것이다.)
실제로 쓰면 직접 create table 문에 4가지 컬럼을 묶어서 기본 키를 만들어 줘야 한다.
그래서 결론은?
실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 연관관계를 사용하는 걸 고려하는게 낫다.
Address 를 AddressEntity 로 만들어서 일대다 단방향 연관관계로 매핑해주자.
- AddressEntity.java
@Entity
@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);
}
// Getter & Setter
}
- Member.java
// 기존에 선언해 두었던 값 타입 컬렉션 객체는 주석 처리한다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
위와 같이 작성한 다음 JpaMain.java 에서 값 타입 컬렉션을 사용했던 코드를 위와 같은 도메인 클래스로 바꿔준 후 애플리케이션을 실행해보자.(System.out.println("===============START================"); 출력문 이전 코드를 실행해주자, 위의 출력문 이후에 나오는 코드들은 일단 주석 처리)
- hibernate SQL
Hibernate: // old1 insert
/* insert hellojpa.AddressEntity
*/ insert
into
ADDRESS
(city, street, ZIPCODE, id)
values
(?, ?, ?, ?)
Hibernate: // old2 insert
/* insert hellojpa.AddressEntity
*/ insert
into
ADDRESS
(city, street, ZIPCODE, id)
values
(?, ?, ?, ?)
Hibernate: // 일대다 단방향 매핑이기 때문에 update 쿼리가 나가는 건 어쩔 수 없다, 다른 테이블에서 외래 키를 관리하고 있기 때문
// 위의 내용이 기억나지 않는다, 일대다 매핑에 대해서 다시 한번 복습하자.
/* create one-to-many row hellojpa.Member.addressHistory */ update
ADDRESS
set
MEMBER_ID=?
where
id=?
Hibernate:
/* create one-to-many row hellojpa.Member.addressHistory */ update
ADDRESS
set
MEMBER_ID=?
where
id=?
* 강의 외적인 내용
그럼 여기서 Entity 로서 일대다 연관관계로 매핑된 AddressEntity 타입의 컬렉션 데이터를 수정해보자.
여기서 한 가지 참고 할 것이, member.getAddressHistory().add(new AddressEntity("old2", "street", "10000")); 와 같이 getAddressHistory() 메소드에서 remove() 메소드를 통해 컬렉션 내부에 있는 데이터를 삭제하면 어떻게 될까?
- JpaMain.java
findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "10000"));
정답은 "old1 데이터가 삭제되지 않는다." 이다.
remove() 메소드를 호출했는 데도 불구하고 왜 데이터가 지워지지 않는것인지는 아직 잘 모르겠다.
다만, 1시간 정도 시간을 투자해 본 결과 컬렉션에서 값을 변경하는데 성공할 수 있었다.
- AddressEntity.java
@Id @GeneratedValue
private int id;
-> JpaMain.java 에서 get 메소드를 사용하기 위해 어쩔 수 없이 id 값의 타입을 int 형으로 변경하였다.
- JpaMain.java
// AddressEntity 도메인 클래스로 일대다 단방향 매핑으로 컬렉션을 생성할 경우 데이터 수정
AddressEntity updateAddress = findMember.getAddressHistory().get(new AddressEntity("old1", "street", "10000").getId());
findMember.getAddressHistory().remove(updateAddress);
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "10000"));
(도대체 어째서 get 메소드를 통해서 데이터를 한 번 가져오면 정상적으로 기능이 잘 수행되는 걸까?)
- hibernate SQL
Hibernate: /* get() 메소드를 통한 select */
select
addresshis0_.MEMBER_ID as member_i5_0_0_,
addresshis0_.id as id1_0_0_,
addresshis0_.id as id1_0_1_,
addresshis0_.city as city2_0_1_,
addresshis0_.street as street3_0_1_,
addresshis0_.ZIPCODE as zipcode4_0_1_
from
ADDRESS addresshis0_
where
addresshis0_.MEMBER_ID=?
Hibernate:
call next value for hibernate_sequence
Hibernate: /* newCity1 데이터 AddressEntity 테이블에 삽입 */
/* insert hellojpa.AddressEntity
*/ insert
into
ADDRESS
(city, street, ZIPCODE, id)
values
(?, ?, ?, ?)
Hibernate: /* 컬렉션에서 old1 데이터를 제거한다.(update) */
/* delete one-to-many row hellojpa.Member.addressHistory */ update
ADDRESS
set
MEMBER_ID=null
where
MEMBER_ID=?
and id=?
Hibernate: /* 컬렉션에 newCity1 데이터를 추가한다.(update) */
/* create one-to-many row hellojpa.Member.addressHistory */ update
ADDRESS
set
MEMBER_ID=?
where
id=?
Hibernate: /* AddressEntity 테이블에서 최종적으로 old1 데이터를 삭제한다. */
/* delete hellojpa.AddressEntity */ delete
from
ADDRESS
where
id=?
꽤 복잡해보이지만 위의 hibernate SQL 을 보면 데이터의 삭제와 삽입이 잘 이루어졌음을 확인할 수 있다.
- h2 console