[자바 ORM 표준 JPA 프로그래밍 - 기본편] 09강. 값 타입
09강. 값 타입
🚨 주의
- 값 타입 컬렉션 가능하면 사용하지 말기
- 값 타입은 모두 불변으로 만들기
1. 기본값 타입
JPA의 데이터 타입 분류
타입 | 상세 |
---|---|
엔티티 타입 | - @Entity 로 정의하는 객체- 데이터가 변해도 식별자로 지속해서 추적 가능 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능 |
값 타입 | - int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체 - 식별자가 없고 값만 있으므로 변경시 추적 불가 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체 |
값 타입 분류
분류 | 상세 |
---|---|
기본값 타입 | • 자바 기본 타입(int, double) • 래퍼 클래스(Integer, Long) • String |
임베디드 타입 (embedded type, 복합 값 타입) | |
컬렉션 값 타입 (collection value type) |
- 기본값 타입 : 자바에서 기본적으로 제공하는 타입
ex. String name, int age- ⭐️ 생명주기를 엔티티에 의존 ex. 회원을 삭제하면 이름, 나이 필드도 함께 삭제됨
- 값 타입은 공유하면 X ex. 회원 이름 변경시 다른 회원의 이름도 함께 변경되므로
🚨 자바의 기본 타입(primitive type)은 절대 공유X
- int, double 같은 기본 타입(primitive type)은 절대 공유 X
- 기본 타입은 항상 값을 복사함
- Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경X
-
임베디드 타입, 컬렉션 값 타입 : JPA에서 정의해서 사용해야 함
ex. 좌표를 (x, y) 묶어서 사용하고 싶을 때 : position class 만들어서 사용
2. 임베디드 타입 (복합 값 타입) ⭐️
임베디드 타입
- 새로운 값 타입을 직접 정의할 수 있음
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
- ⭐️ int, String과 같은 값 타입
- 엔티티가 아님. 추적 안되고 변경하면 끝
ex. 회원 엔티티 : 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가짐
유사한 것끼리 묶어보면
-
시간 : 근무 시작일, 근무 종료일 -> Period Type
-
주소 : 주소 도시, 주소 번지, 주소 우편번호 -> Address Type
=> Period, Address가 임베디드 타입이 됨
임베디드 타입 사용법
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시- 기본 생성자 필수
임베디드 타입의 장점
- 재사용
- 높은 응집도
-
해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
ex. Period의 startDate, endDate를 이용해서 현재 일하고 있는지의 여부를 판단하는
Period.isWork()
메소드 생성 - 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존함
- 임베디드 타입도 값 타입 !!
임베디드 타입과 테이블 매핑
- Embedded Type에
@Embeddable
, Embedded Type을 포함하는 쪽에@Embedded
- 임베디드 타입을 쓰거나 쓰지 않거나 DB 테이블은 똑같음. 매핑만 해주면 됨
ex. 임베디드 타입 사용 X
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
}
실행 결과 (commit)
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
endDate timestamp,
startDate timestamp,
street varchar(255),
USERNAME varchar(255),
zipcode varchar(255),
TEAM_ID bigint,
primary key (MEMBER_ID)
)
임베디드 타입을 사용하려면
// Member.java
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
를
// Period.java
package hellojpa;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
// 기본 생성자 필수
public Period() {}
// getter, setter 생략
}
// Address.java
package hellojpa;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 기본 생성자 필수
public Address() { }
// getter, setter 생략
}
// Member.java
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
똑같이 Member table을 생성
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
USERNAME varchar(255),
endDate timestamp,
startDate timestamp,
TEAM_ID bigint,
primary key (MEMBER_ID)
)
-
Embedded Type 안에 다른 메소드 만들어서 사용 가능
-
이용 예제
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; // 기본 생성자 필수 public Member() {} @Column(name = "USERNAME") private String username; // Period @Embedded private Period workPeriod; // Address @Embedded private Address homeAddress; // getter, setter 생략 }
@Embeddable public class Address { private String city; private String street; private String zipcode; // 기본 생성자 필수 public Address() {} public Address(String city, String street, String zipcode) { this.city = city; this.street = street; this.zipcode = zipcode; } // getter, setter 생략 }
@Embeddable public class Period { private LocalDateTime startDate; private LocalDateTime endDate; public Period() {} public Period(LocalDateTime startDate, LocalDateTime endDate) { this.startDate = startDate; this.endDate = endDate; } // getter, setter 생략 }
// JpaMain Member member = new Member(); member.setUsername("hello"); member.setHomeAddress(new Address("city", "street", "zipcode")); member.setWorkPeriod(new Period()); em.persist(member); tx.commit();
실행 결과
Hibernate: /* insert hellojpa.Member */ insert into Member (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) values (?, ?, ?, ?, ?, ?, ?)
DB
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같음
-
객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
ex. 좌표값을 클래스로 -> 이 클래스를 이용해서 다양한 메서드를 만들어서 사용할 수 있음
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
임베디드 타입과 연관관계
- 임베디드 타입은 임베디드 타입을 포함할 수 있음
- 임베디드 타입은 엔티티를 포함할 수 있음
- 임베디드 타입에서 엔티티의 FK만 들고 있으면 됨
@AttributeOverride
: 속성 재정의
-
한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복됨
->
@AttributeOverrides
,@AttributeOverride
를 사용해서 컬럼명 속성을 재정의
ex. Member에 homeAddress, workAddress
@AttributeOverride
하지 않으면
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
// 기본 생성자 필수
public Member() {}
@Column(name = "USERNAME")
private String username;
// Period
@Embedded
private Period workPeriod;
// Address
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
// getter, setter (생략)
}
-
이 상태로 실행하면 에러 발생 :
Repeated column in mapping for entity
->
@AttributeOverrides
,@AttributeOverride
를 이용@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; // 기본 생성자 필수 public Member() {} @Column(name = "USERNAME") private String username; // Period @Embedded private Period workPeriod; // Address @Embedded private Address homeAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name="city", column=@Column(name="WORK_CITY")), // DB Column을 따로 매핑 @AttributeOverride(name="street", column=@Column(name="WORK_STREET")), @AttributeOverride(name="zipcode", column=@Column(name="WORK_ZIPCODE")) }) private Address workAddress; // getter, setter }
실행 결과
Hibernate: create table Member ( MEMBER_ID bigint not null, city varchar(255), street varchar(255), zipcode varchar(255), USERNAME varchar(255), WORK_CITY varchar(255), WORK_STREET varchar(255), WORK_ZIPCODE varchar(255), endDate timestamp, startDate timestamp, TEAM_ID bigint, primary key (MEMBER_ID) )
임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null
3. 값 타입과 불변 객체
값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 수 있어 위험함
ex.
- 회원1과 회원2 모두 주소의 city 값이 OldCity
- 그런데 이때 city 값을 NewCity로 바꾸면 회원1, 회원의 값이 모두 NewCity로 바뀜
ex.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
// 기본 생성자 필수
public Member() {}
@Column(name = "USERNAME")
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
// getter, setter 생략
}
// JpaMain
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
tx.commit();
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
이때, member1의 City 값만 바꾸려고 했는데
// JpaMain
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
// member1의 City 값만 바꾸려고 이렇게 코딩했는데..
member1.getHomeAddress().setCity("newCity");
tx.commit();
실행결과 : Update 쿼리 두 번 나가서 member1, member2의 city가 모두 바뀜
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
💡 만약 값을 공유하는 것이 의도였다면 -> 값 타입이 아닌 엔티티를 사용해야 함
값 타입 복사
- 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험
-
대신 값(인스턴스)를 복사해서 사용
ex. 회원1의 address 값을 getter로 가져와서 회원2의 address를 새로 만들어줌
// JpaMain
Address address1 = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address1);
em.persist(member1);
Address copyAddress = new Address(address1.getCity(), address1.getStreet(), address1.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member1.getHomeAddress().setCity("newCity");
tx.commit();
실행 결과 : member1만 업데이트
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?,
endDate=?,
startDate=?
where
MEMBER_ID=?
객체 타입의 한계
그런데,, 복사 안하고 쓰는 것을 막을 수 없다
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있음
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive type)이 아니라 객체 타입
- 자바 기본 타입에 값을 대입하면 값을 복사
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
- 객체의 공유 참조는 피할 수 없음
💡 Java 기본 타입 vs. 객체 타입
기본 타입(primitive type)
int a = 10; int b = a;//기본 타입은 값을 복사 b = 4;
객체 타입
Address a = new Address(“Old”); Address b = a; // 객체 타입은 참조를 전달 b. setCity(“New”) // a, b가 같은 Address 인스턴스를 가리킴 -> a, b 모두 값이 바뀜
불변 객체
- 값 타입을 불변 객체(immutable object)로 설계해서 부작용 가능성을 차단
💡 불변 객체
- 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성 방법 : 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
- 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체
ex. 기존 Address에서 setter만 삭제(getter만 만듦)하거나 setter는 private으로 만들기
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 기본 생성자 필수
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// getter 생략
}
수정하려면 Address 객체를 새로 만들어서 넣어줘야 함
// JpaMain
Address address1 = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address1);
em.persist(member1);
//member1.getHomeAddress().setCity("newCity"); // setter를 만들지 않아서 setCity으로 수정하는 것 자체가 불가능
// 수정하려면 새로 만들어서 address 자체를 수정해주어야 함
Address newAddress = new Address("NewCity", address1.getStreet(), address1.getZipcode());
member1.setHomeAddress(newAddress);
tx.commit();
4. 값 타입의 비교
값 타입의 비교
-
값 타입: 인스턴스가 달라도 값이 같으면 같은 것으로 봐야 함
// Java primitive type int a = 10; int b = 10; // a == b -> true // Java 객체 타입 Address a = new Address(“서울시”) Address b = new Address(“서울시”) // a == b -> false
-
a.equals(b)를 사용해서 동등성 비교
-> 값 타입의 equals() 메소드를 적절하게 재정의(override)(주로 모든 필드 사용)
🚨 주의
- equals를 바로 사용하면 안되고 override를 해주고 사용해야함
- equals 구현하면 반드시 hashCode도 override
💡 동일성 비교 vs. 동등성 비교
비교 상세 동일성(identity) 비교 인스턴스의 참조 값을 비교, == 사용 동등성(equivalence) 비교 인스턴스의 값을 비교, equals() 사용 @Embeddable public class Address { private String city; private String street; private String zipcode; // 기본 생성자 필수 public Address() {} public Address(String city, String street, String zipcode) { this.city = city; this.street = street; this.zipcode = zipcode; } // getter 생략 @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); } }
public class ValueMain { public static void main(String[] args) { int a = 10; int b = 10; System.out.println("a == b : " + (a == b)); Address address1 = new Address("city", "street", "10000"); Address address2 = new Address("city", "street", "10000"); System.out.println("address1 == address2 : " + (address1 == address2)); System.out.println("address1 equals address2 : " + (address1.equals(address2))); } }
a == b : true address1 == address2 : false address1 equals address2 : true
🚨 equals, hashCode Override 할 때, 반드시 getter 사용
- getter 사용하지 않으면 proxy일 때 접근이 안됨
5. 값 타입 컬렉션 ⭐️
값 타입 컬렉션
- 값 타입을 컬렉션에 담아서 쓰는 것
- 값 타입을 하나 이상 저장할 때 사용
@ElementCollection
,@CollectionTable
사용- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없음 -> 컬렉션을 저장하기 위한 별도의 테이블이 필요
- 데이터베이스는 다 value로 값만 넣음
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
// 기본 생성자 필수
public Member() {}
@Column(name = "USERNAME")
private String username;
// Period
@Embedded
private Period workPeriod;
// Address
@Embedded
private Address homeAddress;
// collections를 값으로 가짐
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")) // 매핑되는 테이블 지정, join되는 컬럼 지정(외래키 지정)
@Column(name = "FOOD_NAME") // Member table에 들어갈 컬럼명 지정
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
// getter, setter 생략
}
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
USERNAME varchar(255),
endDate timestamp,
startDate timestamp,
TEAM_ID bigint,
primary key (MEMBER_ID)
)
Hibernate:
create table FAVORITE_FOOD (
MEMBER_ID bigint not null,
FOOD_NAME varchar(255)
)
Hibernate:
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
값 타입 컬렉션 사용
-
값 타입 저장 예제
// JpaMain Member member = new Member(); member.setUsername("member1"); member.setHomeAddress(new Address("homeCity ", "street", "zipcode")); member.getFavoriteFoods().add("치킨"); member.getFavoriteFoods().add("족발"); member.getFavoriteFoods().add("피자"); member.getAddressHistory().add(new Address("old1", "street", "zipcode")); member.getAddressHistory().add(new Address("old2", "street", "zipcode")); em.persist(member); tx.commit();
실행 결과 : member만 persist 했는데, 값 타입 컬렉션도 자동으로 같이 persist
Hibernate: /* insert hellojpa.Member */ insert into Member (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) values (?, ?, ?, ?, ?, ?, ?) Hibernate: // addressHistory에 insert 2회 /* 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: // favoriteFoods에 insert 2회 /* insert collection row hellojpa.Member.favoriteFoods */ insert into FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) values (?, ?) Hibernate: /* insert collection row hellojpa.Member.favoriteFoods */ insert into FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) values (?, ?) Hibernate: /* insert collection row hellojpa.Member.favoriteFoods */ insert into FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) values (?, ?)
-
값 타입 조회 예제
// JpaMain // 생략 : 위에 insert한 테이블과 동일 em.flush(); em.clear(); // DB에는 넣고 깔끔하게 다시 시작 Member findMember = em.find(Member.class, member.getId()); tx.commit();
실행 결과 : member만 조회됨. 지연로딩 때문
Embedded Type인 homeAddress는 조회됨
Hibernate: select member0_.MEMBER_ID as MEMBER_I1_6_0_, member0_.city as city2_6_0_, // homeAddress는 조회됨 member0_.street as street3_6_0_, member0_.zipcode as zipcode4_6_0_, member0_.USERNAME as USERNAME5_6_0_, member0_.endDate as endDate6_6_0_, member0_.startDate as startDat7_6_0_ from Member member0_ where member0_.MEMBER_ID=?
- 값 타입 컬렉션도 지연 로딩 전략 사용
-
값 타입 컬렉션도 불러오려면 따로 조회해야 함
// JpaMain 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();
실행 결과
Hibernate: select member0_.MEMBER_ID as MEMBER_I1_6_0_, member0_.city as city2_6_0_, member0_.street as street3_6_0_, member0_.zipcode as zipcode4_6_0_, member0_.USERNAME as USERNAME5_6_0_, member0_.endDate as endDate6_6_0_, member0_.startDate as startDat7_6_0_ from Member member0_ where member0_.MEMBER_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 = old1 address = old2 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 = 피자
-
⭐️ 값 타입 수정 예제
// JpaMain Member findMember = em.find(Member.class, member.getId()); // 값 타입 수정 // homeCity -> newCity // findMember.getHomeAddress().setCity("newCity"); // 안됨!! 값타입은 immutable 해야 함 // 새로 Address를 만들어서 넣어주기 Address a = findMember.getHomeAddress(); findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); // 값 타입 컬렉션 수정 // 치킨 -> 한식 : remove 하고 새로 add 해줘야 함 findMember.getFavoriteFoods().remove("치킨"); findMember.getFavoriteFoods().add("한식"); tx.commit();
실행 결과
Hibernate: select member0_.MEMBER_ID as MEMBER_I1_6_0_, member0_.city as city2_6_0_, member0_.street as street3_6_0_, member0_.zipcode as zipcode4_6_0_, member0_.USERNAME as USERNAME5_6_0_, member0_.endDate as endDate6_6_0_, member0_.startDate as startDat7_6_0_ from Member member0_ where member0_.MEMBER_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: /* update hellojpa.Member */ update Member set city=?, street=?, zipcode=?, USERNAME=?, endDate=?, startDate=? where MEMBER_ID=? Hibernate: /* delete collection row hellojpa.Member.favoriteFoods */ delete from FAVORITE_FOOD where MEMBER_ID=? and FOOD_NAME=? Hibernate: /* insert collection row hellojpa.Member.favoriteFoods */ insert into FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) values (?, ?)
예제. 값 타입 컬렉션 수정
// JpaMain Member findMember = em.find(Member.class, member.getId()); // 만약 Address의 city 중 old1만 newCity으로 바꾸고, old2는 유지하고 싶다면 // 마찬가지로 지우고 다시 넣어줘야 함 // remove는 equals로 작동 -> equals, hashCode 제대로 override 해두지 않으면 꼬임!! findMember.getAddressHistory().remove(new Address("old1", "street", "10000")); findMember.getAddressHistory().add(new Address("newCity", "street", "10000")); tx.commit();
실행 결과 : member_id에 해당하는 address를 모두 삭제하고, 수정하는 값, 유지하는 값 모두 다시 insert
Hibernate: select member0_.MEMBER_ID as MEMBER_I1_6_0_, member0_.city as city2_6_0_, member0_.street as street3_6_0_, member0_.zipcode as zipcode4_6_0_, member0_.USERNAME as USERNAME5_6_0_, member0_.endDate as endDate6_6_0_, member0_.startDate as startDat7_6_0_ from Member member0_ where member0_.MEMBER_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 hellojpa.Member.addressHistory */ delete from ADDRESS where MEMBER_ID=? Hibernate: /* 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: /* insert collection row hellojpa.Member.addressHistory */ insert into ADDRESS (MEMBER_ID, city, street, zipcode) values (?, ?, ?, ?)
-
참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없으므로 값은 변경하면 추적이 어려움
- ⭐️ 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장
- 따라서 쓰지 말자 ^^
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함 : null 입력X, 중복 저장X
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
ex. AddressEntity
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
// 기본 생성자 필수
public Member() {}
@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<>();
// 값 타입이 아닌 Entity로 매핑
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
// getter, setter 생략
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// 기본 생성자 필수
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// getter, setter 생략
@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);
}
}
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
// 생성자
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
public AddressEntity(Address address) { this.address = address; }
// getter, setter 생략
}
// JpaMain
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity ", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new AddressEntity("old1", "street", "zipcode"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "zipcode"));
em.persist(member);
em.flush();
em.clear(); // DB에는 넣고 깔끔하게 다시 시작
System.out.println("=========== START ==========");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
실행 결과
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(city, street, zipcode, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert hellojpa.AddressEntity
*/ insert
into
ADDRESS
(city, street, zipcode, id)
values
(?, ?, ?, ?)
Hibernate:
/* insert hellojpa.AddressEntity
*/ insert
into
ADDRESS
(city, street, zipcode, id)
values
(?, ?, ?, ?)
Hibernate:
/* 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=?
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
=========== START ==========
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_6_0_,
member0_.city as city2_6_0_,
member0_.street as street3_6_0_,
member0_.zipcode as zipcode4_6_0_,
member0_.USERNAME as USERNAME5_6_0_
from
Member member0_
where
member0_.MEMBER_ID=?
- Address에 ID가 있음 : 값 타입이 아니라 자체로 Entity
- MEMBER_ID라는 FK로 연결
Leave a comment