9.1 기본값 타입
JPA의 데이터 타입 분류
- 엔티티 타입(entity type)
- 엔티티 타입이란 @Entity로 정의하는 클래스 객체를 말한다.
- 데이터가 변해도 식별자로 지속해서 추적 가능하다
- ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능하다.
- 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적이 불가능하다
- ex) 숫자 100 => 200 바꾸면 완전히 다른 값으로 대체
- 기본값 타입: 자바 기본 타입(int, double) / 래퍼 클래스 / String
- 생명 주기를 엔티티에 의존한다.
- ex) 회원 삭제하면 이름, 나이도 함께 삭제
- 값 타입은 공유하면 안 된다. ex) 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안된다. (부수 효과)
- cf) 자바의 기본 타입은 절대 공유되지 않는다.
- 기본 타입은 항상 값을 복사한다.
- Integer같은 래퍼 클래스나 String같은 특수한 클래스는 공유 가능한 객체이지만 변경할 수 없다.
- 임베디드 타입(embedded type, 복합 값 타입): 새로운 클래스를 만들어서 사용하는 경우
- 컬렉션 값 타입 (collection value type): List 처럼 뭔가 담을 수 있는 형태
- 기본값 타입: 자바 기본 타입(int, double) / 래퍼 클래스 / String
9.2 임베디드 타입
임베디드 타입(Embedded type, 내장 타입, 복합 값 타입)
- 새로운 값 타입을 직접 정의 가능하다
- JPA는 임베디드 타입이다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
- 추적이 안 되고 변경하면 끝이다.
임베디드 타입 예시
- 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다.
임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시한다.
- @Embedded: 값 타입을 사용하는 곳에 표시한다.
- @Embeddable, @Embedded 둘 중 하나만 넣어도 되지만 둘 다 넣는 걸 권장한다.
- 기본 생성자가 필수
- 임베디드 타입의 장점
- 재사용성
- 높은 응집도
- Period.isWork() 처럼 해당 값 타입만 사용하는 의미있는 매소드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명 주기를 의존한다.
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게 (find-grained) 매핑하는게 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다. value type이 많이 튀어나온다.
임베디드 타입과 연관 관계
임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null 이다.
Ex) 임베디드 타입
- Member 클래스
- 여기에서 기간 / 주소 끼리 속성을 묶으려면?
- 아래처럼 따로 빼버린다.
- @Embeddable을 붙여서 값 타입임을 보여준다.
<hide/>
package hellojpa;
import java.time.LocalDateTime;
import javax.persistence.Embeddable;
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public LocalDateTime getStartDate() {
return startDate;
}
public void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
}
<hide/>
package hellojpa;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name= "MEMBER_ID")
private Long id;
@Column(name= "USERNAME") // DB에 넣고 싶은 값
private String userName; // 객체에 주는 값
//기간
@Embedded
private Period workPeriod;
// 주소
@Embedded
private Address homeAddress;
}
Note) 실행 결과 - embed 한 객체들이 모두 Member클래스의 필드로 들어간다.
Ex) 같은 엔티티가 중복되면?
<hide/>
@Embedded
private Address homeAddress;
@Embedded
private Address WORKAddress;
Note) 실행 결과 - MappingException
- Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: hellojpa.Member column: city (should be mapped with insert="false" update="false")
- 이를 해결하기 위해 @AttributeOverride(s)를 사용한다.
- 따라서, 멤버 클래스에 다음과 같이 애너테이션 @AttributeOverrides을 추가한다.
- 그러면 상단의 homeAddress의 속성 city / street / zipcode는 이름 그대로 들어가고 workAddress는 아래에서 정한대로 column name이 들어간다.
- @AttributeOverride의 name 안에는 Address에서 정의한 필드명이 그대로 들어가야한다.
<hide/>
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")), // DB 컬럼을 따로 매핑한다.
@AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
@AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE"))
})
private Address workAddress;
Note) 실행 결과
- create table member을 보면 새로 오버라이드 한 것과 기존에 쓴 것 모두 나온다.
Ex) 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null 이다.
<hide/>
@Embedded
private Period workPeriod = null;
9.3 값 타입과 불변 객체
값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
- 부작용(side effect) 발생 가능성 때문이다.
Ex) 값 타입 공유 참조 - 부작용
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Address address = new Address("city", "street", "200-000");
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.getHomeAddress().setCity("newCity");
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - 업데이트 쿼리 두 번 실행된다.
값 타입 복사
- 위의 예제에서 봤듯이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.
- 따라서, 값(인스턴스)을 복사해서 사용한다.
객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 객체의 공유 참조는 피할 수 없다.
- ex) 위 사진에서 a, b는 Address라는 같은 인스턴스를 가리키므로 둘다 "New"로 바뀐다.
불변 객체(immutable object)
- 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단 가능
- 값 타입은 불변 객체(immutable object, 생성 시점 이후로 절대 값을 변경할 수 없는 객체)로 설계해야한다.
- 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다. 또는 private으로 만든다.
- ex) Ingeger, String은 불변 객체
Ex) 그럼에도 불구하고 바꾸려면?
- 아래처럼 newAddress를 새로 넣어야 한다.
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Address address = new Address("city", "street", "200-000");
Member member = new Member();
member.setUserName("member");
member.setHomeAddress(address);
em.persist(member);
Address newAddress = new Address(address.getCity(), address.getStreet(),
address.getZipcode());
member.setHomeAddress(newAddress);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
9.4 값 타입의 비교
값 타입의 비교
- 값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
- 동일성 비교(identity) 비교: 인스턴스의 참조값을 비교한다. '=='
- 동등성 비교(equivalence) 비교: 인스턴스의 값을 비교, equals()
- 값 타입을 equals()을 사용해서 동등성 비교를 해야한다.
- 값 타입의 equals()를 적절하게 재정의한다. (주로 모든 필드사용)
- equals() 를 구현하면 그에 맞게 hashcode()도 꼭 재정의한다.
Ex) Address에 다음과 같이 두 메서드를 override한다.
<hide/>
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
@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) ;
}
Note) 실행 결과
9.5 값 타입 컬렉션
값 타입 컬렉션
- 값 타입을 컬렉션에 담아 쓰는 걸 말한다. 하나 이상 저장할 때 사용한다.
- @ElementCollection, @CollectionTable 사용한다.
- @ElementCollection
- @CollectionTable
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션은 일대다라서 데이터베이스에는 하나의 테이블에 넣을 수 있는 방법이 없다. (그래서 별도 테이블 필요)
- 컬렉션을 저장하기 위한 별도의 테이블이 필요한다.
- 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.
- 데이터베이스는 보통 value만 넣을 수 있다. 요즘에야 Json 지원하기도 한다.
- 컬렉션은 "일대다" 개념이다.
- member의 주소 이력을 저장할 때, 별도의 테이블을 뽑아서 저장 가능하다.
- 테이블 FAVORITE_FOOD 의 기본 키: MEMBER_ID + FOOD_NAME
- 테이블 ADDRESS의 기본 키: MEMBER_ID + CITY + STREET + ZIPCODE 모두 묶어서 PK를 쓴다.
- ADDRESS, FAVORITE_FOOD 에 Long id를 기본 키로 넣으면?
- 값 타입이 아닌 엔티티가 되버린다.
Ex) 값 타입 저장
- 멤버
- 둘다 MEMBER_ID로 조인한다.
<hide/>
@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<>();
Note) 실행 결과
- MEMBER에 주소 3가지 정보가 들어간다.
- address 테이블에도 멤버ID(외 래키)와 세가지 주소가 들어간다.
Ex) 값 타입 저장
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 멤버가 먼저 INSERT
- 주소는 3개 food는 2개 INSERT 된다.
- 값 타입을 따로 persist()하지 않았는데 member를 persist()하니까 값 타입 컬렉션도 함께 저장된다.
- 값 타입은 라이프 사이클이 따로 없다. 따라서 생명 주기가 member에 소속된다.
<hide/>
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(city, street, zipcode, USERNAME, MEMBER_ID)
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
(?, ?, ?, ?)
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
(?, ?)
Ex) 값 타입 조회 - LAZY (디폴트)
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 멤버 테이블만 가져오고 끝난다. => 컬렉션들은 "지연 로딩"
- Address는 멤버에 소속된 값 타입이라서 같이 들어온다.
- 멤버 외에 다른 테이블도 가져오도록 하려면?
- addressHistory 추가
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
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();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 이전 예제에 컬렉션 addressHistory 리스트를 추가했다.
- history 테이블 가져와서 주소 정보 찍는다.
- food 테이블 가져와서 food 정보를 찍는다.
- @ElementCollection의 fetch type이 LAZY로 기본 설정되어 있기 때문이다.
<hide/>
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_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_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자
Ex) 값 타입 수정의 잘못된 예시
- 값 타입은 불변이어야한다.
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
findMember.getHomeAddress().setCity("newCity");
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 그런데 값 타입은 부작용(side effect)이 생길 수 있어서 이런 식으로 수정하면 절대 안된다.
<hide/>
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Ex) 값 타입 수정의 올바른 예시
- Address 자체를 통으로 갈아 끼워야한다.
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 값 타입은 추적도 안되니까 완전히 교체를 해줘야한다.
<hide/>
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Ex) 값 타입 컬렉션 업데이트
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 컬렉션의 값만 변경해도 실제 데이터베이스 쿼리가 날아가고 JPA가 데이터를 바꿔준다.
- MEMBER_ID, FOOD_NAME 찾아서 DELETE 한 다음에 데이터를 새로 집어 넣는다.
<hide/>
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
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
(?, ?)
값 타입 컬렉션의 제약 사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 그래서 값을 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블을 모든 컬럼을 묶어서 기본 키를 구성해야한다 => null 입력 X, 중복 저장
- ex) addressHistory를 지우는 경우, 멤버 아이디와 관련한 정보를 DB에서 모두 지운다.
- 그 다음 컬렉션에 최종으로 남은 것들에 대해서만 INSERT 실행한다.
Ex) 주소 바꾸기 - 값 타입 컬렉션의 제약 사항
- 컬렉션은 대상을 찾을 때, 기본적으로 equals()를 쓰기 때문에 괄호 안에 완전히 같은 값을 넣어준다.
- 전에 오버라이드한 equals(), hashcode() 를 사용
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
findMember.getAddressHistory().remove(new Address("old1", "street", "100-000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "100-000"));
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 순서는 JPA가 알아서 고른다.
- addressHistory를 DELETE 하는데 ADDRESS 테이블을 통으로 지운다. 멤버 아이디 기준으로
- 그러고나서 address 관련 INSERT가 두 번 날아간다.
- old1을 remove() 하고나면 old2, newCity1은 남아 있을 것이다. 이 두 개를 테이블을 완전히 갈아끼운 것이다.
- 왜 INSERT 가 두 번 날아갈까?
<hide/>
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.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:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
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:
/* 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
(?, ?)
cf) addressHistory 에 다음 애너테이션 OrderColumn을 넣는 것도 위험하다.
@OrderColumn(name ="address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name ="MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
- 일댕다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용한다.
- 영속성 전이(cascade) + 고아 객체를 사용해서 값 타입 컬렉션 처럼 사용한다.
- ex) AddressEntity
Ex) 대안 - 값 타입을 엔티티로 승급
- Address는 값 타입, AddressEntity는 엔티티
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
- 다음과 같이 멤버의 addressHistory를 바꾼다.
<hide/>
// 기존 코드
// @ElementCollection
// @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name ="MEMBER_ID"))
// private List<Address> addressHistory = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new AddressEntity("old1", "street", "100-000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
// Address a = findMember.getHomeAddress();
// findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
//
// findMember.getFavoriteFoods().remove("치킨");
// findMember.getFavoriteFoods().add("한식");
// findMember.getAddressHistory().remove(new Address("old1", "street", "100-000"));
// findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "100-000"));
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 일대다 단방향 매핑은 update 쿼리가 나간다. (다른 테이블에 FK 가 있기 때문)
- 값 타입으로 쓸 때보다 훨씬 유용하다.
<hide/>
/* 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_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
- 데이터베이스를 보면 ADDRESS에 ID라는 개념이 생겼다.
- 자체적인 ID가 있다는 것은 값 타입이 아니라 엔티티라는 뜻이다. 수정 가능
- => 값 타입을 엔티티로 승급
엔티티와 값 타입 비교
- 엔티티 타입의 특징: 식별자 있음 / 생명 주기 관리 / 공유
- 값 타입의 특징: 식별자 없음 / 생명 주기를 엔티티에 의존 / 공유하지 않는게 안전 (또는 복사해서 사용) / 불변 객체로 만드는 게 안전하다.
9.6 실전 예제 6 - 값 타입 매핑
cf)
- equals(), hashcode()를 재정의하는 경우에 필드에 직접 접근하면 프록시일 때 문제가 생긴다.
- getter()로 접근해야 프록시인 경우에 문제가 안 생긴다.
Ex)
- jpa-shop
- Member
<hide/>
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entity
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
// private String city;
// private String street;
// private String zipcode;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
public Long getId() {return id;
}
public void setId(Long id) {this.id = id;
}
public String getName() {return name;
}
public void setName(String name) {this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
}
- Address
<hide/>
package jpabook.jpashop.domain;
import java.util.Objects;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public String getCity() {
return city;
}
private void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
private void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@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(getCity(), address.getCity()) && Objects.equals(
getStreet(), address.getStreet()) && Objects.equals(getZipcode(),
address.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
}
- Delivery
<hide/>
package jpabook.jpashop.domain;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class Delivery extends BaseEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
private DeliveryStatus status;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public DeliveryStatus getStatus() {
return status;
}
public void setStatus(DeliveryStatus status) {
this.status = status;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}
Note) 실행 결과
- 주소 관련된 필드 3개가 공통으로 들어간다.
- Address 클래스에 fullAddress() 와 같은 메서드를 추가하거나 @Column(length= "제한 길이")를 넣어서 쓸 수도 있다.
<hide/>
create table Delivery (
id bigint not null,
createdBy varchar(255),
createdDate timestamp,
lastModifiedBy varchar(255),
lastModifiedDate timestamp,
city varchar(255),
street varchar(255),
zipcode varchar(255),
status integer,
primary key (id)
)
====중간 생략 =====
create table Member (
MEMBER_ID bigint not null,
createdBy varchar(255),
createdDate timestamp,
lastModifiedBy varchar(255),
lastModifiedDate timestamp,
city varchar(255),
street varchar(255),
zipcode varchar(255),
name varchar(255),
primary key (MEMBER_ID)
)
출처 - https://www.inflearn.com/course/ORM-JPA-Basic
'Spring Framework > [인프런] Java ORM 표준 프로그래밍 - JPA' 카테고리의 다른 글
Chapter 11. 객체지향 쿼리 언어2 - 중급 문법 (0) | 2022.09.09 |
---|---|
Chapter 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2022.09.08 |
Chapter 08. 프록시와 연관 관계 관리 (0) | 2022.09.07 |
Chapter 07. 고급 매핑 (2) | 2022.09.06 |
Chapter 06. 다양한 연관관계 매핑 (0) | 2022.09.05 |