목차
- 기본값 타입
- 임베디드 타입(복합 값 타입)
- 값 타입과 불변 객체
- 값 타입의 비교
- 값 타입 컬렉션
JPA의 데이터 타입 분류
- 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능 -> 내부 데이터가 변해도 추적이 가능하다
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
- 분류
- 기본값 타입
- 자바 기본 타입(int, double)
- 래퍼 클래스(Integer, Long)
- String
- 임베디드 타입(embedded type, 복합 값 타입)
- 컬렌션 값 타입(collection value type)
- 기본값 타입
기본 값 타입
- 예) String name, int age
- 생명주기를 엔티티에 의존 -> 회원을 삭제하면 이름, 나이 필드도 함께 삭제
- 값 타입은 공유하면 X -> 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨 ( Side Effect )
- 참고) 자바의 기본타입은 절대 공유 X
- int, double 같은 기본 타입(primitive type)은 절대 공유 X
- 기본 타입은 항상 값을 복사함
- Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경 X
임베디드 타입( embedded type, 복합 값 타입 )
- JPA는 임베디드 타입이라 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입 이라고도 함
- int, String과 같은 값 타입 -> 추적 X, 변경되면 끝
이해하기
- 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다
- 필드 속성 : startDate, endDate, city, street, zipcode
- 위 속성을 보면 공통적으로 묶을 수 있는 것들이 보인다.
- (startDate, endDate) -> "Period"
- (city, street, zipcode) -> "Address"
- 한번 생각해보자, 만약 내가 다른 사람에게 회원 정보에 대해 얘기를 한다면
- 필드들을 나열해서 말하는게 자연스러울까 묶어서 "이름, 근무 기간, 주소"값이 있다고 얘기하는게 자연스러울까?
- 후자처럼 추상화해서 표현하는게 더 자연스러울 것이다.
- JPA 에서는 이를 반영하여 위처럼 공통으로 묶어서 임베디드 타입으로 구현할 수 있다.
임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입의 장점
- 재사용
- 높은 응집도
- Period.isWork() 처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이다 ->임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- DB는 데이터 관리에 목적이 있기 때문에 한 통에 다 담기는 것이 맞다.
- 그러나, 객체는 데이터 뿐만 아니라 메서드(기능, 행위)를 가지고 있기 때문에, 공통 요소를 묶어서 임베디드 타입으로 관리하면 이점이 많다.
- 의미 있는 메소드
- 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음.
임베디드 타입과 연관관계
- Address는 Zipcode라는 임베디드 타입을 가질 수 있다.
- -> 임베디드 타입이 임베디드 타입을 가질 수 있음
- 임베디드 타입인 PhoneNumber는 엔티티인 PhoneEntity를 가질 수 있다.
- -> 임베디드 타입이 엔티티를 가질 수 있음
- 가능한 이유) : 엔티티에 대한 외래키만 가지면 되기 때문
- -> 임베디드 타입이 엔티티를 가질 수 있음
@AttributeOverride : 속성 재정의
- 한 엔티티에서 같은 값 타입을 사용하면?
- ex) : Member에서 Address가 2개 ( homeAddress, workAddress )
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
...
}
- 컬럼 명이 중복됨 -> 오류 발생 ( MappingException : Repeated column ~ )
- @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name = "WORK_CITY"),
@AttributeOverride(name="street", column=@Column(name = "WORK_street"),
@AttributeOverride(name="zipcode", column=@Column(name = "WORK_ZIPCODE")
})
private Address workAddress;
...
}
임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null
값 타입과 불변 객체
- 값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 만든 개념이다.
- 따라서, 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
- 부작용(side effect) 발생
코드로 이해하기
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("kim");
member2.setHomeAddress(address);
em.persist(member2);
member.getHomeAddress().setCity("new city");
tx.commit();
- 실행 결과, 분명 member의 Address에서 city값만 변경했는데 update 쿼리가 2개가 나간다
- DB를 확인해보면 member2의 city값 또한 동일하게 변경됐다
- 즉, 같은 Address를 공유해서 사용하면 이러한 side effect가 발생하기 때문에 절대 공유하면 안된다.
- 엔티티 간에 공유하고 싶은 값 타입은 엔티티로 만들어서 해결하자. ( 뒤에 설명 )
값 타입 복사
- 위와 같이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험한 행위이다.
- 대신 값(인스턴스)을 복사해서 사용
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);
//주소 객체 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("kim");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member.getHomeAddress().setCity("new city");
객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- -> 개발자가 실수로 직접 대입하는 것을 막을 방법이 없다.
- 객체의 공유 참조는 피할 수 없다.
불변 객체( immutable object )
- 객체 타입을 수정할 수 없게 만들어 부작용을 원천 차단
- 값 타입은 불변 객체(immutable object)로 설계해야함
- 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
- 안만들거나, set을 private으로 제한
- 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체
불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
- 불변인데도 불구하고, 객체의 필드 값을 변경하고 싶은 경우
- 객체를 통째로 교체하자.
Address address = new Address("old city", "street", "10000");
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(address);
em.persist(member);
Address newAddress = new Address("new City", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);
tx.commit();
값 타입의 비교
- 값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함
int a = 10;
int b = 10;
System.out.println("a == b : " + (a == b )); // true 반환
- 그런데 객체 타입은 값이 같아도 주소가 다르기 때문에 비교 시 false를 반환한다.
Address a = new Address("서울");
Address b = new Address("서울");
System.out.println(" a == b : " + ( a == b )); // a,b 주소 값이 다르기 때문에 false 반환
- 동일성(identity) 비교
- 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교
- 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함.
- 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용) + hash코드도 포함해서 정의
값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- 연관관계에서 엔티티를 컬렉션으로 가지고 있는것이 아니라,
- 좋아하는 음식, 주소변경 히스토리를 저장하는 값 타입을 컬렉션에 저장하고 싶을 때 사용
public class Member {
@Id
private Long id;
private Set<String> favoriteFoods;
private List<Address> addressHistory;
}
- @ElementCollection, @CollectionaTable(테이블명 지정) 사용
- 이러한 관계를 데이터베이스에서는 컬렉션을 같은 테이블에 저정할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요함
- 일대다 관계로 풀어서 별도의 테이블을 만들어서 조인이 가능하도록 해야한다.
코드로 이해하기 (Member)
- 값 타입 컬렉션 추가
- addressHistory는 임베디드 타입이므로 컬럼명 명시를 안해줘도 됨
- favorite은 예외적으로 컬럼명을 지정해준다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@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<>();
...
}
======== favorite 테이블 생성 로그, FK 설정(member id)========
Hibernate:
create table favorite_food (
member_id bigint not null,
food_name varchar(255)
)
Hibernate:
alter table favorite_food
add constraint FKetdr9m6ysgxkvs2f7vf15sac4
foreign key (member_id)
references Member
======== address 테이블 생성 로그, FK 설정(member id)========
Hibernate:
create table address (
member_id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
alter table address
add constraint FK6ncq4527mw0seu2b7wr19mqpg
foreign key (member_id)
references Member
컬렉션 값 타입 저장
- 실제 값 타입 컬렉션을 저장할 때 코드를 보면서 이해해보자.
- 이 코드에서 주목해야 할 점은 컬렉션에 값만 넣고 em.persist는 member만 해줬다는 점이다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("city1", "street1", "10001"));
member.getAddressHistory().add(new Address("city2", "street2", "10002"));
member.getAddressHistory().add(new Address("city3", "street3", "10003"));
em.persist(member);
tx.commit();
- 코드를 실행 해보면 컬렉션을 저장하는 테이블에 insert 쿼리가 6개가 나간다.
- "응?? 난 분명 em.persit 함수는 member만 사용했는데?? 심지어 cascade 설정도 안해줬는데 ??"
- 여기서 알 수있는 점은 컬렉션 값 타입이 엔티티의 생명 주기를 따라 간다는 것이다
- 값 타입이기 때문에 Member에 소속된 값 타입은 전부 Member의 생명 주기를 따라간다.
- 즉, 테이블만 따로 만들어서 관리하기 때문에 별도로 persist를 할 필요 없이 엔티티만 저장하면 되는 것이다.
- 일대다 관계에서 Cascade.ALL과 orphanRemoval == true랑 유사하다.
- 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다
- "응?? 난 분명 em.persit 함수는 member만 사용했는데?? 심지어 cascade 설정도 안해줬는데 ??"
ibernate:
/* insert hello.jpa.Member
*/ insert
into
Member
(id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, city, street, zipcode, locker_id, roleType, name, endDate, startDate)
values
(null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */
insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */
insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */
insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.favoriteFoods */
insert
into
favorite_food
(member_id, food_name)
values
(?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.favoriteFoods */
insert
into
favorite_food
(member_id, food_name)
values
(?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.favoriteFoods */
insert
into
favorite_food
(member_id, food_name)
values
(?, ?)
컬렉션 값 타입 조회
- 영속성 컨텍스트를 비워놓고(em.flush(), em.clear) Member를 조회해보자
- Member에 소속된 Embaded 타입인 city, street, zipcode는 검색이 되지만
- 컬렉션 값 타입들은 조회되지 않는다.
- "컬렉션 값 타입은 지연 로딩 전략을 사용한다"는 것을 알 수 있다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("city1", "street1", "10001"));
member.getAddressHistory().add(new Address("city2", "street2", "10002"));
member.getAddressHistory().add(new Address("city3", "street3", "10003"));
em.persist(member);
em.flush();
em.clear();
System.out.println("================== START ==================");
em.find(Member.class, member.getId());
================== START ==================
Hibernate:
select
member0_.id as id1_7_0_,
member0_.createdBy as createdB2_7_0_,
member0_.createdDate as createdD3_7_0_,
member0_.lastModifiedBy as lastModi4_7_0_,
member0_.lastModifiedDate as lastModi5_7_0_,
member0_.age as age6_7_0_,
member0_.description as descript7_7_0_,
member0_.city as city8_7_0_,
member0_.street as street9_7_0_,
member0_.zipcode as zipcode10_7_0_,
member0_.locker_id as locker_15_7_0_,
member0_.roleType as roleTyp11_7_0_,
member0_.team_id as team_id16_7_0_,
member0_.name as name12_7_0_,
member0_.endDate as endDate13_7_0_,
member0_.startDate as startDa14_7_0_,
locker1_.id as id1_6_1_,
locker1_.name as name2_6_1_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
where
member0_.id=?
- 컬렉션 값 타입들을 터치해서 실제로 지연 로딩 전략을 사용하는지 알아보자.
- 컬렉션 값 타입의 필드들을 터치하자마자 지연 로딩 전략을 사용한다.
System.out.println("================== START ==================");
em.find(Member.class, member.getId());
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
tx.commit();
================== START ==================
Hibernate:
select
member0_.id as id1_7_0_,
member0_.createdBy as createdB2_7_0_,
member0_.createdDate as createdD3_7_0_,
member0_.lastModifiedBy as lastModi4_7_0_,
member0_.lastModifiedDate as lastModi5_7_0_,
member0_.age as age6_7_0_,
member0_.description as descript7_7_0_,
member0_.city as city8_7_0_,
member0_.street as street9_7_0_,
member0_.zipcode as zipcode10_7_0_,
member0_.locker_id as locker_15_7_0_,
member0_.roleType as roleTyp11_7_0_,
member0_.team_id as team_id16_7_0_,
member0_.name as name12_7_0_,
member0_.endDate as endDate13_7_0_,
member0_.startDate as startDa14_7_0_,
locker1_.id as id1_6_1_,
locker1_.name as name2_6_1_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
where
member0_.id=?
Hibernate:
select
addresshis0_.member_id as member_i1_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_.member_id=?
address = city1
address = city2
address = city3
Hibernate:
select
favoritefo0_.member_id as member_i1_4_0_,
favoritefo0_.food_name as food_nam2_4_0_
from
favorite_food favoritefo0_
where
favoritefo0_.member_id=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자
컬렉션 값 타입 수정
- 한번 더 강조하자면 "값 타입은 절대 불변해야 한다."
- 따라서, 수정해야 한다면 객체를 통째로 바꿔야한다.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());
findMember.setHomeAddress(new Address("newCity", "newStreet", "20001"));
tx.commit();
- 컬렉션 값 타입 수정1 - String
- Set<String> 타입인 좋아하는 음식을 수정하려면, String 자체가 불변 객체 이므로,
- 삭제하고, 다시 리스트에 넣어준다.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("피자");
tx.commit();
- 발생한 쿼리를 확인 해보자
- 멤버를 SELECT 하고,
- 지연로딩으로 favoriteFood를 SELECT 하고,
- favoriteFood에서 치킨을 DELETE하고
- favoriteFood에 한식을 INSERT한다.
- 값 타입의 변경을 캐치해서 JPA에서 알아서 삭제, 추가 쿼리를 실행한다.
- 영속성 전이가 되는 것 처럼 동작한다. 값 타입 컬렉션들은 소속 엔티티에 의존 관계를 다 맡긴다.
================== START ==================
Hibernate:
select
member0_.id as id1_7_0_,
member0_.createdBy as createdB2_7_0_,
member0_.createdDate as createdD3_7_0_,
member0_.lastModifiedBy as lastModi4_7_0_,
member0_.lastModifiedDate as lastModi5_7_0_,
member0_.age as age6_7_0_,
member0_.description as descript7_7_0_,
member0_.city as city8_7_0_,
member0_.street as street9_7_0_,
member0_.zipcode as zipcode10_7_0_,
member0_.locker_id as locker_15_7_0_,
member0_.roleType as roleTyp11_7_0_,
member0_.team_id as team_id16_7_0_,
member0_.name as name12_7_0_,
member0_.endDate as endDate13_7_0_,
member0_.startDate as startDa14_7_0_,
locker1_.id as id1_6_1_,
locker1_.name as name2_6_1_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
where
member0_.id=?
Hibernate:
select
favoritefo0_.member_id as member_i1_4_0_,
favoritefo0_.food_name as food_nam2_4_0_
from
favorite_food favoritefo0_
where
favoritefo0_.member_id=?
Hibernate:
/* delete collection row hello.jpa.Member.favoriteFoods */ delete
from
favorite_food
where
member_id=?
and food_name=?
Hibernate:
/* insert collection
row hello.jpa.Member.favoriteFoods */
insert
into
favorite_food
(member_id, food_name)
values
(?, ?)
- 컬렉션 값 타입 수정2 - Address 객체
- List<Address> 타입의 AddressHistory의 값을 바꿔보자.
- 이때, equals, hashCode가 재정의 되어 있어야 한다.
- List<Address> 타입의 AddressHistory의 값을 바꿔보자.
System.out.println("================== START ==================");
Member findMember = em.find(Member.class, member.getId());
findMember.getAddressHistory().remove(new Address("city1", "street1", "10001"));
findMember.getAddressHistory().add(new Address("new city1", "street1", "10001"));
tx.commit();
- 발생한 쿼리를 살펴보자
- Member SELECT,
- 지연 로딩으로 AddressHistory SELECT하고
- Address(AddressHistory) 테이블중 Member 소속인 row들 전체 DELETE.
- 응?? 난 분명 city1만 삭제한다고 했는데??
- 그리고, Address 3개를 새로 INSERT 한다.
- 이 부분에 대해서 아래 제약사항에서 자세히 알아보자.
================== START ==================
Hibernate:
select
member0_.id as id1_7_0_,
member0_.createdBy as createdB2_7_0_,
member0_.createdDate as createdD3_7_0_,
member0_.lastModifiedBy as lastModi4_7_0_,
member0_.lastModifiedDate as lastModi5_7_0_,
member0_.age as age6_7_0_,
member0_.description as descript7_7_0_,
member0_.city as city8_7_0_,
member0_.street as street9_7_0_,
member0_.zipcode as zipcode10_7_0_,
member0_.locker_id as locker_15_7_0_,
member0_.roleType as roleTyp11_7_0_,
member0_.team_id as team_id16_7_0_,
member0_.name as name12_7_0_,
member0_.endDate as endDate13_7_0_,
member0_.startDate as startDa14_7_0_,
locker1_.id as id1_6_1_,
locker1_.name as name2_6_1_
from
Member member0_
left outer join
Locker locker1_
on member0_.locker_id=locker1_.id
where
member0_.id=?
Hibernate:
select
addresshis0_.member_id as member_i1_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_.member_id=?
Hibernate:
/* delete collection hello.jpa.Member.addressHistory */
delete
from
address
where
member_id=?
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */ insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */ insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hello.jpa.Member.addressHistory */ insert
into
address
(member_id, city, street, zipcode)
values
(?, ?, ?, ?)
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션 생성하는 테이블을 확인 해보면 ID(PK)가 따로 존재 하지 않는다.
- 즉, 뭘 수정해야 할지를 알 방법이 없다.
Hibernate:
create table address (
member_id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
- 그래서 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 물론 어노테이션을 통한 해결 방법이 있지만, 복잡하게 사용해야 한다
- 실무에서 쓰면 안된다는 거다..
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.
- null 입력 X, 중복 저장 X
값 타입 컬렉션 제안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 낫다.
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용하자.
- 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용하자
코드로 이해하기
- Member 클래스에서 AddressHistory 엔티티로 대체
- @OneToMany와 @JoinColumn 으로 일대다 단방향 매핑을 한다.
- 값 타입을 엔티티로 승격하지 않는 한, 일대다 값 타입 컬렉션은 양방향 매핑을 못한다.
- AddressEntity에서 내부적으로 Address 값 타입을 포함한다.
- @OneToMany와 @JoinColumn 으로 일대다 단방향 매핑을 한다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@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<>();
}
@Entity
@Getter
@Setter
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Address address;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
코드로 이해하기(Main)
- AddressEntity를 넣는다.
- @OneToMany와 @JoinColumn 으로 일대다 단방향 매핑을 했으니까,
- 키를 관리하는 Member 테이블의 반대인 AddressEntity(Address 테이블)에 Update 쿼리가 나가는 것은 이미 학습했다. 참조
- 이제는 ADDRESS 테이블에 ID라는 개념이 생겼다. 그리고 Member ID를 FK로 가지고 있다.
- 자체적인 아이디가 있다는 것은 엔티티라는 얘기고, 이제는 특정 row를 찾아서 내부의 값을 수정할 수 있다.
- 실무에서는 이런식으로 값 타입 컬렉션은 엔티티로 승격 시켜서 많이 사용한다.
Member member = new Member();
member.setUsername("park");
member.setHomeAddress(new Address("city", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("city1", "street1", "10001"));
member.getAddressHistory().add(new AddressEntity("city2", "street2", "10002"));
member.getAddressHistory().add(new AddressEntity("city3", "street3", "10003"));
em.persist(member);
em.flush();
em.clear();
Hibernate:
/* insert hello.jpa.Member
*/ insert
into
Member
(id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, city, street, zipcode, locker_id, roleType, name, endDate, startDate)
values
(null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert hello.jpa.AddressEntity
*/ insert
into
ADDRESS
(id, city, street, zipcode)
values
(null, ?, ?, ?)
Hibernate:
/* insert hello.jpa.AddressEntity
*/ insert
into
ADDRESS
(id, city, street, zipcode)
values
(null, ?, ?, ?)
Hibernate:
/* insert hello.jpa.AddressEntity
*/ insert
into
ADDRESS
(id, city, street, zipcode)
values
(null, ?, ?, ?)
Hibernate:
/* create one-to-many row hello.jpa.Member.addressHistory */ update
ADDRESS
set
member_id=?
where
id=?
Hibernate:
/* create one-to-many row hello.jpa.Member.addressHistory */ update
ADDRESS
set
member_id=?
where
id=?
Hibernate:
/* create one-to-many row hello.jpa.Member.addressHistory */ update
ADDRESS
set
member_id=?
where
id=?
정리
- 엔티티 타입의 특징
- 식별자 O
- 생명 주기 관리
- 공유
- 값 타입의 특징
- 식별자 X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
- 값 타입은 정말 값 타입이라 판단될 때만 사용
- 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨
- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티로 승격화해서 사용하자.
Reference
자바 ORM 표준 JPA 프로그래밍
'JPA' 카테고리의 다른 글
프록시 (0) | 2022.04.07 |
---|---|
고급 매핑 (0) | 2022.04.05 |
다양한 연관관계 매핑 (0) | 2022.03.31 |
연관관계 매핑 기초 (0) | 2022.03.29 |
엔티티 매핑 (0) | 2022.03.24 |