[JPA] 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.(값 타입, 엔티티 타입)
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
JPA
의 데이터 타입은 2가지로 분류 됩니다.
- 엔티티 타입
@Entity
로 정의하는 객체- 데이터가 변해도 식별자로 지속해서 추적 가능
- 값 타입
int
,Integer
,String
,Long
처럼 단순히 값으로 사용하는java
기본 타입이나 객체- 식별자가 없고 값만 있으므로 변경시 추적이 불가함
값 타입과 불변 객체
값 타입에 대해서 본격적으로 들어가기에 앞서 개념적으로 정리해봅시다…!!
값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 등장한 개념 입니다.
값 타입은 등장 목적에 맞게 단순하고 안전하게 다룰 수 있어야 합니다.
값 타입 공유 참조
- 임베디드 타입을 여러 엔티티에 공유하면 위험
side-effect
발생
어떤 일이 발생하는지 코드를 작성하여 보여드리겠습니다.
User
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address homeAddress;
}
Address
@Embeddable
@Setter @Getter
@Access(AccessType.FIELD) // 필드에 직접 접근
@NoArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
private String detailAddress;
public Address(String city, String street, String zipcode, String detailAddress) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
this.detailAddress = detailAddress;
}
}
실험
tx.begin();
Address address = new Address("서울시", "광장시장로", "110", "신영상가 3층");
User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address);
em.persist(user1);
User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(address);
em.persist(user2);
// 의도: user1의 detail 주소만 변경
user1.getHomeAddress().setDetailAddress("민혁상가 2층");
tx.commit();
결과
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* update
com.study.purejpa.User */ update
User
set
city=?,
detailAddress=?,
street=?,
zipcode=?,
name=?
where
id=?
Hibernate:
/* update
com.study.purejpa.User */ update
User
set
city=?,
detailAddress=?,
street=?,
zipcode=?,
name=?
where
id=?
Update SQL
이 2개 발생하면서 용준이도 민혁상가2층
으로 변경되었습니다.
이렇게 값 타입의 실제 인스턴스인 값을 공유하는 것은
매우 매우 매우 위.험⚠️ 합니다.
그러면 어떻게 해야할까요?
바로 값을 복사(깊은 복사)해서 사용해야 합니다.
실험
tx.begin();
Address address = new Address("서울시", "광장시장로", "110", "신영상가 3층");
User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address);
em.persist(user1);
// 깊은 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode(), address.getDetailAddress());
User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(copyAddress);
em.persist(user2);
// 의도: user1의 detail 주소만 변경
user1.getHomeAddress().setDetailAddress("민혁상가 2층");
tx.commit();
결과
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* update
com.study.purejpa.User */ update
User
set
city=?,
detailAddress=?,
street=?,
zipcode=?,
name=?
where
id=?
의도한대로 민혁이만 민혁상가 2층
으로 변경되었습니다!!
정리
java
기본 타입(primitive type
)에 값을 대입하면 값을 복사함- 임베디드 타입처럼 직접 정의한 값 타입은 객체 타입
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
- 객체의 공유 참조는 피할 수 없음
- 임베디드 타입은 값을 복사(깊은 복사)해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있음
- 하지만 얇은 복사를 막을수 없기 때문에 컴파일러에서 확인할 수 있는 방법이 없음 🥲
기본 타입
int a = 10;
int b = a; // 기본 타입은 값을 복사(깊은 복사)
객체 타입
Address a1 = new Address("old");
Address a2 = a1; // 객체 타입은 참조를 전달(얇은 복사)
a2.setCity("New"); // a1의 city도 변경됨
객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다….
그러면 어떻게 하면 될까? 🤔
객체 타입을 수정할 수 없게 만들면 된다!
불변 객체
- 값 타입은 불변 객체(
immutable object
)로 설계해야함- 생성 시점 이후 절대 값을 변경할 수 없는 객체(불변 객체)
- 재할당은 가능하지만, 한번 할당하면 내부 데이터를 변경할 수 없는 객체
- 생성자로만 값을 설정하고 수정자(
setter
)를 만들지 않으면 됨Integer
,String
,Boolean
은java
가 제공하는 대표적인 불변 객체
Addresssetter
제거
@Embeddable
@Getter
@Access(AccessType.FIELD)
@NoArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
private String detailAddress;
public Address(String city, String street, String zipcode, String detailAddress) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
this.detailAddress = detailAddress;
}
}
tx.begin();
Address address1 = new Address("서울시",
"광장시장로",
"110",
"신영상가 3층");
User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address1);
em.persist(user1);
/**
* address1의 값을 변경하고
* 싶으면 새로운 address2를 만들어야함
*/
Address address2 = new Address(address1.getCity(),
address1.getStreet(),
address1.getZipcode(),
"민혁상가 2층");
User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(address2);
em.persist(user2);
tx.commit();
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
값 타입은 불변 객체로 작성하자! 👍
값 타입의 비교
값 타입을 비교하려면 어떻게 해야할까요? 🤔
- 동일성 비교(
==
)- 인스턴스의 참조 값을 비교
- 동등성 비교(
equals()
)- 인스턴스의 값을 비교
값 타입은 equals()
를 사용해서 동등성 비교를 해야 합니다.equals()
를 적절하게 재정의 해서 사용하면 됩니다.^^
(주로 모든 필드를 사용 합니다.)
// 임베디드 타입을 정의하는 곳에 표시
@Embeddable
@Getter
@Access(AccessType.FIELD)
@NoArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
private String detailAddress;
public Address(String city, String street, String zipcode, String detailAddress) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
this.detailAddress = detailAddress;
}
// 인텔리제이의 도움을 받아 생성(cmd + N)
@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()) && Objects.equals(getDetailAddress(),
address.getDetailAddress()
);
}
// 인텔리제이의 도움을 받아 생성(cmd + N)
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode(), getDetailAddress());
}
}
값 타입 분류
값 타입은 JPA
에서 3가지로 분류할 수 있습니다.
- 기본 값 타입
- 임베디드 타입(복합 값 타입)
JPA
에서 정의해서 사용
- 컬렉션 값 타입
JPA
에서 정의해서 사용
기본 값 타입
기본 값 타입은 프로그래밍시 자주 사용하는
자바 기본 타입(int
, double
) ,래퍼 클래스(Integer
, Long
), String
입니다.
기본 값 타입은 생명주기를 엔티티에 의존 합니다.
(회원 엔티티를 삭제하면 이름(String
), 나이(int
)가 함께 삭제)
그리고 값 타입은 공유하면 안됩니다.
(회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됩니다…)
참고java
의 int
, double
같은 기본 타입은 절대 절대 절대로 공유 되지 않습니다.
기본 타입은 항상 값을 복사해서 사용합니다.
int a = 100;
int b = a; // a의 값을 b에 복사
a = 20;
System.out.println(a); // 20
System.out.println(b); // 100
Integer
같은 래퍼 클래스나 String
같은 특수한 클래스는 공유 가능한 객체이지만 변경이 불가합니다.
Integer a = new Ingteger(10);
Integer b = a; // 공유 가능(클래스라서 참조값(주소값)이 복사됨)
a.setValue(100) // 변경이 불가함(이러한 메소드 존재하지 않음)
임베디드 타입(복합 값 타임)
임베디드 타입은 JPA
에서 정의해서 사용하는 타입 입니다.
새로운 값(주로 기본 값 타입)을 직접 모아서 정의합니다.
임베디드 타입은 엔티티의 값일 뿐 입니다.^^
(임베디드 타입의 값이 null
이면 매핑한 컬럼 모두 null
입니다.)
즉 임베디드 타입도 결국에는 값 타입 입니다.(추적이 불가 합니다.)JPA
입장에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같습니다.
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address address; // 집 주소
}
@Embeddable
@Access(AccessType.FIELD)
public class Address {
private String city;
private String street;
private String zipcode;
private String detailAddress;
public boolean isDetailAddress(String detailAddress) {
if(detailAddress.isBlank()) {
return false;
}
return true;
}
}
장점
- 재사용
- 높은 응집도
- 값 타입만 사용하는 의미있는 메소드 생성 가능
- 임베디드 타입을 소유한 엔티티에 생명주기를 의존함
참고
잘 설계한 ORM
애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다고 한다! ㅎㅎ
그런데 하나의 엔티티에서 같은 임베디드 타입을 사용하면?🤔
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address homeAddress;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address workAddress;
}
@AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의 하면 됩니다.
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@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")
),
@AttributeOverride(
name = "detailAddress",
column = @Column(name = "work_detailAddress")
),
})
private Address workAddress;
}
컬렉션 값 타입
컬렉션 값 타입이란 값 타입을 컬렉션에 담아서 사용하는 것을 의미 합니다.
- 값 타입을 하나 이상 저장할 때 사용
@ElementCollection
,@CollectionTable
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없음.
- 컬렉션 저장을 위한 별도의 테이블 필요
예제
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "user_id"))
private List<Address> addressHistory = new ArrayList<>();
}
Hibernate:
create table ADDRESS (
user_id bigint not null,
city varchar(255),
detailAddress varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
create table FAVORITE_FOOD (
user_id bigint not null,
FOOD_NAME varchar(255)
)
Hibernate:
create table User (
id bigint not null,
city varchar(255),
detailAddress varchar(255),
street varchar(255),
zipcode varchar(255),
name varchar(255),
primary key (id)
)
FAVORITE_FOOD
와 ADDRESS
테이블이 생성되는 create SQL
을 보면 PK
가 없는 것을 확인할 수 있습니다.
이처럼 값 타입 컬렉션에는 몇 가지 제약사항이 존재합니다.
값 타입 컬렉션 제약사항
- 값 타입은 엔티티와 다르게 식별자(PK)가 없음
- 값이 변경되면 추적이 어려움
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함
null
허용 🙅♂️, 중복 저장 🙅♂️
저장 예제
tx.begin();
User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));
user1.getFavoriteFoods().add("치킨");
user1.getFavoriteFoods().add("족발");
user1.getFavoriteFoods().add("피자");
user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));
em.persist(user1);
tx.commit();
결과
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.addressHistory */ insert
into
ADDRESS
(user_id, city, detailAddress, street, zipcode)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.addressHistory */ insert
into
ADDRESS
(user_id, city, detailAddress, street, zipcode)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
자세히 보면 em.persist(user1)
만 했는데도
자동으로FAVORITE_FOOD
, ADDRESS
테이블에 데이터가 들어갔습니다.
생명주기(life-cycle
)가 User
에 소속되었기 때문입니다.
조회할 때는 어떨까요? 🤔
조회 예제
tx.begin();
User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));
user1.getFavoriteFoods().add("치킨");
user1.getFavoriteFoods().add("족발");
user1.getFavoriteFoods().add("피자");
user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));
em.persist(user1);
em.flush();
em.clear();
// 조회
User findUser = em.find(User.class, user1.getId());
tx.commit();
결과
Hibernate:
select
user0_.id as id1_2_0_,
user0_.city as city2_2_0_,
user0_.detailAddress as detailad3_2_0_,
user0_.street as street4_2_0_,
user0_.zipcode as zipcode5_2_0_,
user0_.name as name6_2_0_
from
User user0_
where
user0_.id=?
엇! user
만 조회하는것을 확인할 수 있습니다.
값 타입 컬렉션은 지연 로딩을 사용하는 것 같은데요?
조회 지연로딩 예제
// 조회
User findUser = em.find(User.class, user1.getId());
// 값 타입 컬렉션 조회(지연로딩)
List<Address> addressHistory = findUser.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("city="+address.getCity());
}
// 값 타입 컬렉션 조회(지연로딩)
Set<String> favoriteFoods = findUser.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood="+favoriteFood);
}
결과
Hibernate:
select
addresshis0_.user_id as user_id1_0_0_,
addresshis0_.city as city2_0_0_,
addresshis0_.detailAddress as detailad3_0_0_,
addresshis0_.street as street4_0_0_,
addresshis0_.zipcode as zipcode5_0_0_
from
ADDRESS addresshis0_
where
addresshis0_.user_id=?
city=과천시
city=안산시
Hibernate:
select
favoritefo0_.user_id as user_id1_1_0_,
favoritefo0_.FOOD_NAME as food_nam2_1_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.user_id=?
favoriteFood=족발
favoriteFood=치킨
favoriteFood=피자
값 타입 컬렉션은 지연로딩 전략을 사용하는 것을 확인할 수 있습니다!!!
그런데 말 입니다..?
값 타입 컬렉션에 변경 사항이 발생하면? 🤔
수정 예제
tx.begin();
User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));
user1.getFavoriteFoods().add("치킨");
user1.getFavoriteFoods().add("족발");
user1.getFavoriteFoods().add("피자");
user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));
em.persist(user1);
em.flush();
em.clear();
// 조회
User findUser = em.find(User.class, user1.getId());
/**
* 변경사항이 생기면?
* 값 타입은 불변객체이기 때문에
* 새로운 인스턴스를 넣어야 합니다.
*/
// 민혁상가 1층 -> 민혁상가 5층
findUser.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 5층"));
// 치킨 -> 한식
findUser.getFavoriteFoods().remove("치킨");
findUser.getFavoriteFoods().add("한식");
// Address에 equals, hashCode 재정의 되어있어야함.
// 안산시 -> 시흥시
findUser.getAddressHistory().remove(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));
findUser.getAddressHistory().add(new Address("시흥시", "중앙로", "2448", "민혁상가3 3층"));
tx.commit();
결과
Hibernate:
/* update
com.study.purejpa.User */ update
User
set
city=?,
detailAddress=?,
street=?,
zipcode=?,
name=?
where
id=?
// ADDRESS를 통으로 삭제하고 시흥시와 과천시 Address를 삽입
Hibernate:
/* delete collection com.study.purejpa.User.addressHistory */ delete
from
ADDRESS
where
user_id=?
Hibernate:
/* insert collection
row com.study.purejpa.User.addressHistory */ insert
into
ADDRESS
(user_id, city, detailAddress, street, zipcode)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.addressHistory */ insert
into
ADDRESS
(user_id, city, detailAddress, street, zipcode)
values
(?, ?, ?, ?, ?)
//================================================================//
Hibernate:
/* delete collection row com.study.purejpa.User.favoriteFoods */ delete
from
FAVORITE_FOOD
where
user_id=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
여기서 신기한건 FAVORITE_FOOD
테이블의 치킨 -> 한식은 delete SQL
이후 insert SQL
이 발생했습니다.
그런데 ADDRESS
테이블은 ADDRESS
전체 데이터를 삭제하고 insert SQL
2개가 발생했습니다.
그 이유는 주인 엔티티와 연관된 모든 데이터를 삭제하고,
값 타입 컬렉션에 있는 현재 값을 모두 다시 저장해야 하기 때문입니다.
복잡하지요…?
값 타입 컬렉션 사용하지 마세요! 🙅♂️
✅ 값 타입 컬렉션 대신에 일대다(1:N) 관계를 사용하는 것을 권장드립니다.
AddressEntity
@Entity
@Getter
@Table(name = "address")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
}
User
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 임베디드 타입 사용하는 곳에 표시
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
// 일대다 관계 사용(컬렉션 값 타입 X)
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "user_id")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
값 타입 컬렉션 대신 일대다 관계 사용
tx.begin();
User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));
user1.getFavoriteFoods().add("치킨");
user1.getFavoriteFoods().add("족발");
user1.getFavoriteFoods().add("피자");
user1.getAddressHistory().add(new AddressEntity("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new AddressEntity("안산시", "중앙로", "2448", "민혁상가3 3층"));
em.persist(user1);
em.flush();
em.clear();
// 조회
User findUser = em.find(User.class, user1.getId());
/**
* 변경사항이 생기면?
* 값 타입은 불변객체이기 때문에
* 새로운 인스턴스를 넣어야 합니다.
*/
// 민혁상가 1층 -> 민혁상가 5층
findUser.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 5층"));
// 치킨 -> 한식
findUser.getFavoriteFoods().remove("치킨");
findUser.getFavoriteFoods().add("한식");
// 민혁상가3 3층 -> 뉴진스의 하입보이
AddressEntity addressEntity = findUser.getAddressHistory().get(1);
addressEntity.setAddress(new Address("서울시", "홍대로", "88", "뉴진스의 하입보이"));
tx.commit();
결과
Hibernate:
/* insert com.study.purejpa.User
*/ insert
into
User
(city, detailAddress, street, zipcode, name, id)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.study.purejpa.AddressEntity
*/ insert
into
address
(city, detailAddress, street, zipcode, id)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert com.study.purejpa.AddressEntity
*/ insert
into
address
(city, detailAddress, street, zipcode, id)
values
(?, ?, ?, ?, ?)
// 일대다(1:N) 연관관계 주인을 일(1)에 설정해서 발생
Hibernate:
/* create one-to-many row com.study.purejpa.User.addressHistory */ update
address
set
user_id=?
where
id=?
// 일대다(1:N) 연관관계 주인을 일(1)에 설정해서 발생
Hibernate:
/* create one-to-many row com.study.purejpa.User.addressHistory */ update
address
set
user_id=?
where
id=?
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
Hibernate:
select
user0_.id as id1_2_0_,
user0_.city as city2_2_0_,
user0_.detailAddress as detailad3_2_0_,
user0_.street as street4_2_0_,
user0_.zipcode as zipcode5_2_0_,
user0_.name as name6_2_0_
from
User user0_
where
user0_.id=?
Hibernate:
select
favoritefo0_.user_id as user_id1_1_0_,
favoritefo0_.FOOD_NAME as food_nam2_1_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.user_id=?
Hibernate:
select
addresshis0_.user_id as user_id6_0_0_,
addresshis0_.id as id1_0_0_,
addresshis0_.id as id1_0_1_,
addresshis0_.city as city2_0_1_,
addresshis0_.detailAddress as detailad3_0_1_,
addresshis0_.street as street4_0_1_,
addresshis0_.zipcode as zipcode5_0_1_
from
address addresshis0_
where
addresshis0_.user_id=?
// 민혁상가 1층 -> 민혁상가 5층
Hibernate:
/* update
com.study.purejpa.User */ update
User
set
city=?,
detailAddress=?,
street=?,
zipcode=?,
name=?
where
id=?
// 민혁상가3 3층 -> 뉴진스의 하입보이
Hibernate:
/* update
com.study.purejpa.AddressEntity */ update
address
set
city=?,
detailAddress=?,
street=?,
zipcode=?
where
id=?
// 치킨 -> 한식
Hibernate:
/* delete collection row com.study.purejpa.User.favoriteFoods */ delete
from
FAVORITE_FOOD
where
user_id=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row com.study.purejpa.User.favoriteFoods */ insert
into
FAVORITE_FOOD
(user_id, FOOD_NAME)
values
(?, ?)
정리
엔티티 타입의 특징
- 식별자 🙆♂️
- 생명 주기 관리
- 공유 🙆♂️
값 타입의 특징
- 식별자 🙅♂️
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전 👷🏼♂️
- 복사(깊은 복사)해서 사용
- 불변 객체로 만드는 것이 안전 👷🏼♂️
값 타입은 정말 값 타입이라 판단될 때만
사용하세요!
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 생성하면 안됩니다.
식별자가 반드시 필요하고, 지속적으로 값을 추적하고 변경해야 한다면..........
그것은 엔티티 타입 입니다.!!!!😁
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 조건절을 포함한 일대다 페이징 최적화 (0) | 2023.06.07 |
---|---|
[JPA] JPQL 기본 개념과 예제 (0) | 2023.02.28 |
[JPA] CASCADE(영속성 전이)와 고아객체 (0) | 2023.02.24 |
[JPA] JPA가 Entity를 판별하는 방법과 save()의 비밀(entityInformation.isNew(entity)) (0) | 2023.02.22 |
[JPA] 하이버네이트 프록시와 지연로딩(Lazy), 즉시로딩(Eager) (0) | 2023.02.22 |