[자바 ORM 표준 JPA] JPA 임베디드 타입
[자바 ORM 표준 JPA] JPA 임베디드 타입
임베디드 타입
임베디드(복합 값) 타입#
- 새로운 값 타입을 정의할 수 있음
- JPA는 임베디드 타입(embedded type)이라 함
- 주로 기본 값 타임을 모아 만들어서 복합 값 타입이라고도 함
- int, String과 같은 값 타입 (엔티티 아님)
임베디드 타입#
회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
회원 엔티티는 이름, 근무 기간, 집 주소를 가진다.
- Period는 startDate와 endDate를 가지게 클래스로 만들어 값 타입을 만듭니다.
- Address는 city, street, zipCode를 묶어서 클래스로 만들어 값 타입을 만듭니다.
임베디드 타입 사용법#
- @Embeddable : 값을 타입을 정의하는 곳에서 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입 장점#
- 재사용
- 높은 응집도
- Period.isWork() 처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
임베디드 타입과 테이블 매핑#
임베디드 타입을 사용하던 안하던 회원테이블은 변화가 없습니다.
테이블은 데이터를 잘 관리하기 위한 것이고, 객체는 행위까지 고려해야 하기때문에 임베디드 타입으로 묶었을때 얻을 수 있는 이득이 많습니다.
Member.java
package relativemapping;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class Member {
public Member(){
}
@Id @GeneratedValue
private Long id;
private String name;
// 기간
private LocalDateTime startDate;
private LocalDateTime endDate;
// 주소
private String city;
private String street;
private String zipcode;
}
JpaMain.java - 애플리케이션 재시작
console
create table Member (
id bigint not null,
city varchar(255),
endDate timestamp,
name varchar(255),
startDate timestamp,
street varchar(255),
zipcode varchar(255),
primary key (id)
)
MEMBER 테이블이 생성된 것을 확인 할 수 있습니다. Member 엔티티에 속성들을 workPeriod와 homeAddress 임베디드 타입으로 변경해 보도록 하겠습니다.
Period.java
package relativemapping;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@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;
}
}
Address.java
package relativemapping;
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;
}
}
Member.java
package relativemapping;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class Member {
public Member(){
}
@Id @GeneratedValue
private Long id;
private String name;
// 기간
//private LocalDateTime startDate;
//private LocalDateTime endDate;
@Embedded
private Period workPeriod;
// 주소
//private String city;
//private String street;
//private String zipcode;
@Embedded
private Address homeAddress;
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 Period getWorkPeriod() {
return workPeriod;
}
public void setWorkPeriod(Period workPeriod) {
this.workPeriod = workPeriod;
}
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
}
@Embeddable과 @Embedded는 한쪽만 설정해 두어도 되지만, 양쪽다 추가하길 권장합니다.
JpaMain.java - 애플리케이션 재시작
console
create table Member (
id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
name varchar(255),
endDate timestamp,
startDate timestamp,
primary key (id)
)
Create SQL은 이전과 똑같습니다. 테이블 구조는 변함 없지만, Member 엔티티는 좀더 객체지향 적으로 사용할 수 있습니다.
예를 들면 현재시간이 workPeriod에 시작과 끝에 포함된다면, isIncumbent()를 통해 재직중인지 확인하는 메소드를 만들어 사용할 수 도 있습니다.
package relativemapping;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
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 isIncumbent(){
LocalDateTime today = LocalDateTime.now();
/*
System.out.println(this.startDate);
System.out.println(today);
System.out.println(this.startDate.isEqual(today)); // 주어진 시간과 같은지
System.out.println(this.startDate.isBefore(today)); // 주어진 시간보다 이전인지
System.out.println(this.startDate.isAfter(today)); // 주어진 시간보다 이후인지
System.out.println(this.endDate);
System.out.println(today);
System.out.println(this.endDate.isEqual(today)); // 주어진 시간과 같은지
System.out.println(this.endDate.isBefore(today)); // 주어진 시간보다 이전인지
System.out.println(this.endDate.isAfter(today)); // 주어진 시간보다 이후인지
*/
if( ! this.startDate.isAfter(today) && this.endDate.isAfter(today)){
return true;
}else{
return false;
}
}
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;
}
}
JpaMain - Member 추가
package relativemapping;
import org.hibernate.Hibernate;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
public class JpaMain {
//psvm 단축키로 생성 가능
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("relativemapping");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); // [트랜잭션] 시작
try{
Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(new Address("서울","영등포","00000"));
member1.setWorkPeriod(new Period(LocalDateTime.of(2021,01,01,1,12,0),LocalDateTime.of(2023,01,01,1,12,0)));
em.persist(member1);
System.out.println("isIncumbent "+member1.getWorkPeriod().isIncumbent());
tx.commit();
}catch (Exception e){
e.printStackTrace();
tx.rollback();
}finally {
em.close();
}
emf.close();
}
}
console
isIncumbent true
Hibernate:
/* insert relativemapping.Member
*/ insert
into
Member
(city, street, zipcode, name, endDate, startDate, id)
values
(?, ?, ?, ?, ?, ?, ?)
임베티드 타입의 장점#
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블은 아주 세밀하게(find-grained) 매핑하는 것이 가능
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
- 용어도 공통화 되고, 코드도 공통화 된다.
- 도메인의 언어로 맞출수 있는 언어들도 많이 나올 수 있음.
임베디드 타입과 연관관계#
JPA 표준 스펙에 있는 내용으로, Member엔티티가 Adress와 PhoneNumber라는 임베디드 값 타입을 가지고 있습니다.
Address는 Zipcode를 가질 수 있는데, 임베디드 타입은 임베디드 타입을 가질수 있습니다.
그리고 재미있는 사실이 PhoneNumber라는 임베디드 타입이 PhoneEntity를 가질 수 있습니다. PhoneNumber 입장에서 PhoneEntity의 외래키를 가지고 있으면 되므로 어렵운 개념은 아닙니다.
@AttributeOverride: 속성 재정의#
- 한 엔티티에서 같은 값 타입을 사용하면?
- 컬럼명이 중복됨
- @AttributeOverrides, @AttributeOverride를 사용해서 컬럼명 속성을 재정의
Member.java - Address workAddress 추가
package relativemapping;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class Member {
public Member(){
}
@Id @GeneratedValue
private Long id;
private String name;
// 기간
//private LocalDateTime startDate;
//private LocalDateTime endDate;
@Embedded
private Period workPeriod;
// 주소
//private String city;
//private String street;
//private String zipcode;
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress; // ** 추가
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 Period getWorkPeriod() {
return workPeriod;
}
public void setWorkPeriod(Period workPeriod) {
this.workPeriod = workPeriod;
}
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
}
JpaMain.java - 애플리케이션 재시작
console
Exception in thread "main" javax.persistence.PersistenceException: [PersistenceUnit: relativemapping] Unable to build Hibernate SessionFactory
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.persistenceException(EntityManagerFactoryBuilderImpl.java:1336)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1262)
at org.hibernate.jpa.HibernatePersistenceProvider.createEntityManagerFactory(HibernatePersistenceProvider.java:56)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:79)
at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:54)
at relativemapping.JpaMain.main(JpaMain.java:12)
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: relativemapping.Member column: city (should be mapped with insert="false" update="false")
at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:862)
at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(PersistentClass.java:880)
at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(PersistentClass.java:876)
at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:902)
at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:634)
at org.hibernate.mapping.RootClass.validate(RootClass.java:267)
at org.hibernate.boot.internal.MetadataImpl.validate(MetadataImpl.java:354)
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:465)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1259)
... 4 more
오류없이 속성을 재정의 하여 사용하고 싶을때, @AttributeOverrides 와 @AttributeOverride를 사용합니다.
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
})
private Address workAddress;
@AttributeOverrides({
@AttributeOverride(
name = "city"
, column = @Column(
name = "work_city"
)
)
})
JpaMain.java - 애플리케이션 재시작
console
Hibernate:
create table Member (
id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
name varchar(255),
work_city varchar(255),
work_street varchar(255),
work_zipcode varchar(255),
endDate timestamp,
startDate timestamp,
TEAM_ID bigint,
primary key (id)
)
임베디드 타입과 null#
임베디드 타입의 값이 null 이면 매핑한 컬럼 값은 모두 null