기본값 타입
- JPA 데이터 타입은 2가지로 분류된다.
- 엔티티 타입
- 값 타입
1. JPA의 데이터 타입 분류
- 엔티티 타입
- 값 타입
🔍 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적할 수 있다.
- 예) 회원 엔티티의 나이, 키 값을 변경해도 식별자로 인식할 수 있다.
🔍 값 타입
- 값타입은 int / Integer / String처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체다.
- 식별자가 없고 값만 있으면 변경시 추적이 불가능하다.
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체된다.
- 값 타입은 절대 공유가 되어선 안된다.
- 회원 이름 변경시 다름 회원의 이름도 함께 변경되면 안된다.
- 값 타입의 생명 주기는 엔티티에 의존한다.
- 회원이 삭제되면 이름, 나이 필드도 함께 삭제된다.
💡 값 타입 분류
- 기본 값 타입
- 자바 기본 타입 (int, double)
- 래퍼 클래스 (Integer, Long)
- String
- 임베디드 타입 (embedded type, 복합 값 타입)
- 컬렉션 값 타입 (collection value type)
✔ 기본값 타입
기본 타입은 공유가 되지 않는다. 공유되지 않고 값이 복사가 된다. 그래서 기본 값은 사용했을 때 사이드 이펙트가 일어나지 않아서 안전하다.
int a = 5;
int b = 1;
b = a;
a = 10;
System.out.println(a); // 10
System.out.println(b); // 5
- b = a 에서 b의 값을 a 값으로 바꿨다. 그리고 a의 값을 10으로 바꿨다.
- 출력해보면 a는 10이다. b는 5다.
- b = a 에서 a의 값이 복사되어 b의 값이 된 것이다. a가 공유된 것이 아니다.
- 그러므로 a 값을 자유롭게 바꿔도 b 값은 변경이 일어나지 않는다.
- 기본 값은 안전하게 쓸 수 있다.
특수한 클래스(래퍼 클래스, String 클래스)도 공유가 가능하면서 값이 바뀌지 않는다.
String a = "abc";
String b = "ABC";
b = a;
a = "xyz";
System.out.println(a); // xyz
System.out.println(b); // abc
- String 클래스 / 래퍼 클래스는 객체이므로 공유가 가능하다.
- 하지만 기본 타입(primitive type)처럼 a 값을 바꿔도 b의 값이 바뀌지 않는다.
- 그래서 안전하게 쓸 수 있다.
임베디드 타입 (embedded type)
- 새로운 값 타입을 직접 정의할 수 있다.
- 내장타입 또는 임베디드 타입 또는 복합 값 타입 로 불린다.
- 임베디드 타입은 int / String 처럼 값 타입이다. 엔티티가 아니다.
- 기본 생성자가 필수로 존재해야 한다.
- 임베디드 타입의 값이 null이면 매핑한 칼럼 값도 모두 null이다.
1. 임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 사용한다.
- 주로 임베디트 타입의 해당 클래스에 쓰인다.
- @Embedded : 값 타입을 사용하는 곳에 사용한다.
- 주로 엔티티 내에서 쓰인다.
- @Embeddable 애노테이션을 사용할 경우 @Embedded 애노테이션을 생략할 수 있으나 둘 다 같이 명시하는 것을 권장한다.
💡 예시
멤버 엔티티의 속성들이다.
속성들을 분류할 수 있다. startDate 와 endDate 속성은 기간의 의미를 담고있는 Period 임베디드로 생성할 수 있다.
city / street / zipcode 속성은 주소를 의미하는 Adddress 임베디드로 생성할 수 있다.
위 사진처럼 저렇게 임베디드를 2개로 생성할 수 있다.
📌 MemberV2 엔티티
package hellojpa.domain2;
import javax.persistence.*;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 기간 Period
@Embedded
private Period workPeriod;
// 주소 Address
@Embedded
private Address homeAddress;
}
- 임베디드 타입에 @Embedded 애노테이션을 붙인다.
📌 Address 임베디드
package hellojpa.domain2;
import javax.persistence.Embeddable;
@Embeddable
@Getter @Setter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
- 임베디드 타입에 기본 생성자는 필수다.
- 엠베디드 타입 클래스에 @Embeddable 애노테이션을 붙인다.
📌 Period 임베디드 타입
package hellojpa.domain2;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
@Getter @Setter
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {
}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public boolean isWork() {
...
}
}
- 임베디드 타입에 기본 생성자는 필수다.
- 엠베디드 타입 클래스에 @Embeddable 애노테이션을 붙인다.
- isWork() 메서드 처럼 임베디드 타입 클래스 내에서 필드값을 통해 의미있는 메서드를 만들 수 있다.
📌 메인 메서드
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
import hellojpa.domain2.Period;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.time.LocalDateTime;
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 {
MemberV2 member = new MemberV2();
member.setUsername("hello");
member.setHomeAddress(new Address("city", "street", "10000"));
LocalDateTime startDate = LocalDateTime.of(2022, 3, 20, 12, 00, 00);
LocalDateTime endDate = LocalDateTime.now();
member.setWorkPeriod(new Period(startDate, endDate));
em.persist(member);
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 임베디드 타입을 사용해도 사용안한 것 처럼 정상적으로 테이블이 생성된다.
2. 임베디드 타입 장점
- 재사용성이 높다.
- 높은 응집도
- 예를 들어서 Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.
3. 임베디드 타입과 테이블 매핑
- 임베디드 타입은 그저 엔티티의 값이다.
- 임베디드 타입은 사용하기 전과 후 매핑하는 테이블은 같다.
- 임베디드를 통해 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스 수가 더 많다.
4. 임베디드 타입과 연관관계
- 임베디드 타입이 다른 임베디드 타입을 가질 수 있다.
- 임베디드 타입이 엔티티를 가질 수 있다.
- 엔티티의 외래키를 가지고 있으면 되기 때문이다.
package hellojpa.domain2;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
// 임베디드가 엔티티 소유
private Item item;
// @Column 사용
@Column(name="CODE")
private Code code;
}
이런식으로 임베디드가 엔티티를 소유할 수 있다.
엔티티 타입 클래스 안에서 @Column도 사용할 수 있다.
5. 임베디드 타입 내 제약 생성
package hellojpa.domain;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class AddressEmd {
@Column(length = 10)
private String city;
@Column(length = 20)
private String street;
@Column(length = 5)
private String zipcode;
public void isValid() {
....
}
}
- @Column 애노테이션을 통해 임베디드 타입의 각 속성마다 제약을 걸수 있다.
- 검증 조건을 체크하는 isValid() 메서드 처럼 의미있는 메서드를 만들 수 있다.
- 제약 조건이나 검증 조건이 있을 경우 임베디트로 통합적으로 관리해주는게 좋다.
6. @AttributeOverride : 속성 재정의
한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복된다.
📌 MemberV2 엔티티
package hellojpa.domain2;
import javax.persistence.*;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 회사 주소 Address
@Embedded
private Address workAddress;
// 집주소 Address
@Embedded
private Address homeAddress;
}
- 이렇게 workAddress 와 homeAddress 처럼 집 주소와 회사 주소를 모두 포함하고 싶을 때 문제가 있다.
- 바로 집 주소와 회사 주소의 임베디드 타입이 같다는 것이다. 그러면 칼럼명이 중복된다.
- 그래서 Repeated column in mapping for entity 예외가 발생한다.
- 이럴 떄 @AttributeOverride / @AttributeOverrides 애노테이션을 사용하여 속성 재정의를 해야한다.
✔ @AttributeOverrides 사용하여 해결하기
@AttributeOverride : 하나의 칼럼의 속성을 재정의할 경우 사용
@AttributeOverrides : 여러 칼럼의 속성을 재정의할 경우 사용
package hellojpa.domain2;
import javax.persistence.*;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 회사 주소 Address
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "w_city")),
@AttributeOverride(name = "street", column = @Column(name = "w_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "w_zipcode"))
})
private Address workAddress;
// 집주소 Address
@Embedded
private Address homeAddress;
}
- 속성을 재정의하여 컬렴 명 중복을 피할 수 있다.
값 타입과 불변 객체
- 값 타입은 복잡한 객체 세상을 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
1. 값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
- 주소의 city 값을 NewCity로 바꿔버리면 공유하고 있는 회원1, 회원2의 주소의 city값이 모두 바뀌는 부작용(Side Effect)이 발생한다.
🔍 공유 객체의 값 변경 전
📌 MemberV2 엔티티
package hellojpa.domain2;
import javax.persistence.*;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 집 주소 Address
@Embedded
private Address homeAddress;
}
📌 메인 메서드
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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", "10000");
MemberV2 member1 = new MemberV2();
member1.setUsername("member1");
member1.setHomeAddress(address);
MemberV2 member2 = new MemberV2();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member1);
em.persist(member2);
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- member1.setHomeAddress(address) 와 member2.setHomeAddress(address) 를 통해 member1과 member2는 서로 같은 주소 타입 객체를 공유하고 있다.
🔍 공유 객체의 값 변경 후
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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", "10000");
MemberV2 member1 = new MemberV2();
member1.setUsername("member1");
member1.setHomeAddress(address);
MemberV2 member2 = new MemberV2();
member2.setUsername("member2");
member2.setHomeAddress(address);
// member1의 주소 타입의 도시 값 수정
member1.getHomeAddress().setCity("NewCity");
em.persist(member1);
em.persist(member2);
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- member1의 주소 타입의 city 값을 바꿨더니 member2의 주소 타입의 city값도 같이 변경되었다.
- 이처럼 공유 객체를 사용하면 Side Effect가 발생할 우려가 있기 때문에 공유 객체를 사용하면 안된다.
2. 값 타입 복사
- 공유 객체를 사용하지 말고 값을 복사해서 사용해야 한다.
📌 메인 메서드
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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 m1Address = new Address("city", "street", "10000");
// 값 복사
Address m2Address = new Address(
m1Address.getCity(),
m1Address.getStreet(),
m1Address.getZipcode());
MemberV2 member1 = new MemberV2();
member1.setUsername("member1");
member1.setHomeAddress(m1Address);
MemberV2 member2 = new MemberV2();
member2.setUsername("member2");
member2.setHomeAddress(m2Address);
// member1의 주소 타입의 도시 값 수정
member1.getHomeAddress().setCity("NewCity");
em.persist(member1);
em.persist(member2);
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 값을 공유하지 않고 복사했기 때문에 member1의 도시 값을 바꿔도 member2는 영향을 받지 않았다.
3. 객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive type)이 아니라 객체 타입이라는 것이다.
- 자바 기본 타입(primitive type)에 값을 대입하면 공유되는 것이 아니라 값을 복사한다.
- 기본 타입은 '=' 으로 값을 복사한다.
- 그러나 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다. 즉, 객체의 공유 참조는 피할 수 없다.
- 객체 타입은 '=' 으로 객체 참조를 전달한다.
4. 불변객체
- 불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체다.
- 값 타입의 모든 객체를 모두 불변 객체로 만들어 부작용(=Side Effect)을 막아야한다.
- 만약에 불변 객체의 값을 변경하고 싶다면 생성자롤 다시 만들어서 새로운 불변객체를 생성해야 한다.
🔍 불변 객체 만드는 방법
- 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않거나 private로 만들면 된다.
- Integer, String은 자바가 제공하는 대표적인 불변 객체다.
값 타입의 비교
- 값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
- 값 타입은 equals() 메서드를 사용해서 동등성 비교를 해야한다.
- 실무에서 값 타입을 비교하는 경우가 별로 없지만 비교할 경우가 생기면 equlas()와 hashCode()를 재정의해서 사용해야 한다.
1. 동일성(identity) 비교
- 인스턴스의 참조 값을 비교한다.
- == 사용
2. 동등성(equivalence) 비교
- 인스턴스의 값 비교
- equals() 사용
- equals() 메서드를 재정의 해야한다.
- hashCode() 메서드도 같이 재정의 한다.
📌 Address 임베디드 타입
package hellojpa.domain2;
import javax.persistence.Embeddable;
import java.util.Objects;
@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;
}
@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);
}
}
- equals() 메서드와 hashCode() 메서드를 오버라이딩할 때 필드 값(city, street, zipcode)을 그대로 가져와서 쓸 수도 있지만 만약에 프록시 객체일 경우 필드값을 getXXX로 가져와야한다.
- equals() 와 hashCode()를 재정의할 때 저기에 체크박스를 눌러서 getter로 값을 가져와서 비교하는 것이 제일 안전하다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressEmd addressEmd = (AddressEmd) o;
return Objects.equals(getCity(), addressEmd.getCity())
&& Objects.equals(getStreet(), addressEmd.getStreet())
&& Objects.equals(getZipcode(), addressEmd.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
값 타입 컬렉션
- 결론 : 값 타입은 실무에서 권장하지 않는다.
- 컬렉션을 값 타입으로 사용하는 것을 값 타입 컬렉션이라고 한다.
- 관계형 데이터베이스는 기본적으로 컬렉션을 지원하지 않는다.
- 컬렉션은 일대다 개념이기 때문에 데이터베이스에 컬렉션과 엔티티를 같은 테이블에 저장할 수 없다.
- 그래서 컬렉션을 일대다 개념으로 풀어내야 한다.
- 컬렉션을 별도의 테이블로 만들어야 한다.
- 컬렉션을 별도의 테이블로 만들 때 엔티티의 id값과 컬렉션의 모든 요소를 묶어서 하나의 pk값으로 만들어야 한다.
- 참고로 컬렉션은 값 타입이기 때문에 pk 값을 id 값으로 식별자를 넣게 되면 값 타입이 아니라 엔티티가 되어 버린다.
- 값 타입은 하나 이상 저장할 때 사용할 수 있다.
- @ElementCollection / @CollectionTable 애노테이션을 사용한다.
- 값 타입 컬렉션은 영속성 전이(Casecade) + 고아 객체 제거 기능을 필수로 가지고 있다.
1. 값 타입 컬렉션 사용
🔍 값 타입 컬렉션 생성
package hellojpa.domain2;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@Embedded
private Address homeAddress;
@ElementCollection // 값 타입 컬렉션
@CollectionTable(name = "FAVORITE_FOOD", // 테이블명 지정
joinColumns= @JoinColumn(name = "MEMBER_ID") ) // 외래 키 지정
@Column(name = "FOOD_NAME") // 컬렉션 내부 값 타입이 String 이므로 @Column 을 사용할 수 있다.
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID") )
private List<Address> addressesHistory = new ArrayList<>();
}
Hibernate:
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
create table FAVORITE_FOOD (
MEMBER_ID bigint not null,
FOOD_NAME varchar(255)
)
Hibernate:
create table MemberV2 (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
USERNAME varchar(255),
TEAM_ID bigint,
primary key (MEMBER_ID)
)
- 값 타입 컬렉션의 테이블을 별도로 생성해야 한다.
🔍 저장 예제
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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 {
MemberV2 member = new MemberV2();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressesHistory().add(new Address("old1", "street", "10000"));
member.getAddressesHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
Hibernate:
/* insert hellojpa.domain2.MemberV2
*/ insert
into
MemberV2
(city, street, zipcode, TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
- 컬렉션도 값 타입이기 때문에 컬렉션의 생명 주기는 엔티티에 의존하게 된다.
- 따라서 컬렉션을 따로 영속화 시키지 않아도 엔티티가 영속화 될 때 같이 영속화 된다.
- 그리고 만약에 컬렉션 값을 변경하고 싶다면 엔티티를 통해서 값을 변경해야 한다.
🔍 조회 예제
엔티티 조회하기
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Set;
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 {
MemberV2 member = new MemberV2();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressesHistory().add(new Address("old1", "street", "10000"));
member.getAddressesHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===========START=========");
MemberV2 findMember = em.find(MemberV2.class, member.getId());
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
===========START=========
Hibernate:
select
memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
memberv2x0_.city as city2_8_0_,
memberv2x0_.street as street3_8_0_,
memberv2x0_.zipcode as zipcode4_8_0_,
memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
memberv2x0_.USERNAME as USERNAME5_8_0_,
team1_.TEAM_ID as TEAM_ID1_12_1_,
team1_.name as name2_12_1_
from
MemberV2 memberv2x0_
left outer join
Team team1_
on memberv2x0_.TEAM_ID=team1_.TEAM_ID
where
memberv2x0_.MEMBER_ID=?
- 컬렉션은 기본값이 지연로딩이다.
- 엔티티만 조회했을 때는 컬렉션이 조회되지 않는다.
컬렉션 값 조회하기
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Set;
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 {
MemberV2 member = new MemberV2();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressesHistory().add(new Address("old1", "street", "10000"));
member.getAddressesHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===========START=========");
MemberV2 findMember = em.find(MemberV2.class, member.getId());
List<Address> addressesHistory = findMember.getAddressesHistory();
for (Address address : addressesHistory) {
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) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
Hibernate:
select
memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
memberv2x0_.city as city2_8_0_,
memberv2x0_.street as street3_8_0_,
memberv2x0_.zipcode as zipcode4_8_0_,
memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
memberv2x0_.USERNAME as USERNAME5_8_0_,
team1_.TEAM_ID as TEAM_ID1_12_1_,
team1_.name as name2_12_1_
from
MemberV2 memberv2x0_
left outer join
Team team1_
on memberv2x0_.TEAM_ID=team1_.TEAM_ID
where
memberv2x0_.MEMBER_ID=?
Hibernate:
select
addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
addressesh0_.city as city2_0_0_,
addressesh0_.street as street3_0_0_,
addressesh0_.zipcode as zipcode4_0_0_
from
ADDRESS addressesh0_
where
addressesh0_.MEMBER_ID=?
address = old1
address = old2
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_5_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_5_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자
3월 31, 2022 5:33:33 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]
Process finished with exit code 0
- 컬렉션을 조회할 때 컬렉션 테이블을 조회한다.
🔍 수정 예제
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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 {
MemberV2 member = new MemberV2();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressesHistory().add(new Address("old1", "street", "10000"));
member.getAddressesHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===========START=========");
MemberV2 findMember = em.find(MemberV2.class, member.getId());
// 값 타입 컬레션 수정하기
findMember.getAddressesHistory().remove(new Address("old1", "street", "10000")); // equals(), hashCode() 재정의 된 상태이어야 한다.
findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
===========START=========
Hibernate:
select
memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
memberv2x0_.city as city2_8_0_,
memberv2x0_.street as street3_8_0_,
memberv2x0_.zipcode as zipcode4_8_0_,
memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
memberv2x0_.USERNAME as USERNAME5_8_0_,
team1_.TEAM_ID as TEAM_ID1_12_1_,
team1_.name as name2_12_1_
from
MemberV2 memberv2x0_
left outer join
Team team1_
on memberv2x0_.TEAM_ID=team1_.TEAM_ID
where
memberv2x0_.MEMBER_ID=?
Hibernate:
select
addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
addressesh0_.city as city2_0_0_,
addressesh0_.street as street3_0_0_,
addressesh0_.zipcode as zipcode4_0_0_
from
ADDRESS addressesh0_
where
addressesh0_.MEMBER_ID=?
Hibernate:
/* delete collection hellojpa.domain2.MemberV2.addressesHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
- 값 컬렉션 1개를 추가 했는데 Insert 구문이 2번 나간다.
- 왜냐하면 값 타입 컬렉션에서 변경 사항이 발생하면, 주인 ㅇ엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하기 때문이다.
- 이런 이유로 실무에선 값 타입 컬렉션을 사용하면 안된다.
<참고>
package hellojpa;
import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
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 {
MemberV2 member = new MemberV2();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressesHistory().add(new Address("old1", "street", "10000"));
member.getAddressesHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===========START=========");
MemberV2 findMember = em.find(MemberV2.class, member.getId());
/* homeCity → newCity 값을 바꾸는 방법 */
// findMember.getHomeAddress().setCity("newCity"); // 값 타입은 불변 객체이기 때문에 이렇게 값을 수정하면 안된다.
/ Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); // 새로운 값 타입으로 교체
/* 치킨 → 한식 */
// 값 타입 컬렉션을 수정할 때는 삭제하고 다시 추가해야한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("치킨");
findMember.getAddressesHistory().remove(new Address("old1", "street", "10000")); // equals(), hashCode() 재정의 된 상태이어야 한다.
findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));
tx.commit();
} catch (Exception e) {
System.out.println("e.getMessage=" +e.getMessage());
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
===========START=========
Hibernate:
select
memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
memberv2x0_.city as city2_8_0_,
memberv2x0_.street as street3_8_0_,
memberv2x0_.zipcode as zipcode4_8_0_,
memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
memberv2x0_.USERNAME as USERNAME5_8_0_,
team1_.TEAM_ID as TEAM_ID1_12_1_,
team1_.name as name2_12_1_
from
MemberV2 memberv2x0_
left outer join
Team team1_
on memberv2x0_.TEAM_ID=team1_.TEAM_ID
where
memberv2x0_.MEMBER_ID=?
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_5_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_5_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
Hibernate:
select
addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
addressesh0_.city as city2_0_0_,
addressesh0_.street as street3_0_0_,
addressesh0_.zipcode as zipcode4_0_0_
from
ADDRESS addressesh0_
where
addressesh0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.domain2.MemberV2 */ update
MemberV2
set
city=?,
street=?,
zipcode=?,
TEAM_ID=?,
USERNAME=?
where
MEMBER_ID=?
Hibernate:
/* delete collection hellojpa.domain2.MemberV2.addressesHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.domain2.MemberV2.addressesHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
2. 값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 식별자가 없기에 값을 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모두 칼럼을 묶어서 기본 키를 구성해야 한다.
- null이면안된다.
- 중복이면 안된다.
3. 값 타입 컬렉션 대안
- 실무에서는 값 타입 컬렉션을 엔티티로 만들어 일대다 관계로 사용하는 것이 좋다.
- 엔티티로 만들 때 영속성 전이 + 고아 객체 제거 기능을 사용해서 값 타입 컬렉션 처럼 사용할 수 있다.
📌 AddressEntity 엔티티
package hellojpa.domain2;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Getter @Setter
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
- 값 컬렉션을 엔티티로 만들어서 사용한다.
- 엔티티이므로 기본 생성자를 필수로 정의해야 한다.
📌 MemberV2
package hellojpa.domain2;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static javax.persistence.CascadeType.*;
@Entity
@Getter @Setter
public class MemberV2 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@Embedded
private Address homeAddress;
// 일대다 단방향 관계로 풀어낸다.
@OneToMany(cascade = ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressesHistory = new ArrayList<>();
}
- 참고로 일대다 단방향 관계일 경우 update 쿼리문이 생성된다.
4. 정리
- 값 타입 컬렉션을 쓰기 보다는 엔티티로 만들어서 사용할 것을 권장한다.
- 엔티티 타입 특징
- 식별자를 가진다.
- 생명 주기를 관리한다.
- 공유할 수 있다.
- 값 타입의 특징
- 식별자가 없다
- 생명 주기를 엔티티에 의존한다.
- 공유하지 않는 것이 안전하다. (값을 복사해서 사용할 것)
- 불변 객체로 만드는 것이 안전하다.
- 값 타입은 정말 값 타입이라 판단될 때만 사용한다.
- 정말 단순한 경우에만 사용
- 예) 좋아하는 음식 메뉴를 다중 선택으로 고르는 것 처럼 심플한 자료들
- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 값 타입이 아닌 엔티티로 만들어야 한다.
👀 참고 자료
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
https://catsbi.oopy.io/e1315057-ba06-426a-ab07-9b68af0fcae8
값 타입(1/2)
기본값 타입
catsbi.oopy.io
'[JPA] > JPA 프로그래밍 - 기본편' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어2 - 중급 문법 (0) | 2022.04.03 |
---|---|
[JPA] 객체지향 쿼리 언어1 - 기본 문법 (0) | 2022.03.31 |
[JPA] 프록시와 연관관계 관리 (0) | 2022.03.30 |
[JPA] 고급 매핑 (0) | 2022.03.29 |
[JPA] 다양한 연관관계 매핑 (0) | 2022.03.29 |