자바 ORM 표준 JPA 프로그래밍 : 교보문고
자바 ORM 표준 JPA 프로그래밍 - 기본편 : 인프런
@Entity
로 정의하는 객체
식별자가 존재하기 때문에 지속적 추적이 가능
필드에 할당되는
값
으로만 사용하는 자바 기본타입이나 객체
식별자가 존재하지 않아서 추적이 불가능하다
다음 3가지로 분류 할 수 있다.
int one = 1 ;
int two = one ;
two = 2 ;
System.out.println((one == two) ? "같다" : "다르다");
System.out.println("one : "+one);
System.out.println("two : "+two);
----
다르다
one : 1
two : 2
JAVA 의 기본 타입은
=
연산자를 통해 복사가 일어나
one
과two
가 각각 다른 값이며
two
를 수정하더라도one
에 아무런 영향이 미치지 않는다.
public static class TObject
{
public TObject(){}
public TObject(int field1, int field2)
{ this.field1 = field1; this.field2 = field2;}
private int field1;
private int field2;
...getter setter 생략
TObject tObject1 = new TObject(1,1);
TObject tObject2 = tObject1;
tObject2.setField2(2);
System.out.println((tObject1 == tObject2 ) ? "같다" : "다르다");
System.out.println((tObject1.getField2() == tObject2.getField2()) ? "같다" : "다르다");
System.out.println("tObject1.getField2() : "+tObject1.getField2());
System.out.println("tObject2.getField2() : "+tObject2.getField2());
---
같다
같다
tObject1.getField2() : 2
tObject2.getField2() : 2
primitive 타입 이외
Object
클래스를 상속받는 객체 타입은=
연산시
primitive 타입과 달리참조
를 사용한다.
때문에TObject tObject2 = tObject1;
에서tObject1
의 복제가 아닌
tObject1
의 메모리 위치 값이tObject2
에 참조가 되어
System.out.println((tObject1 == tObject2 ) ? "같다" : "다르다");
가같다
라고 출력되고,tObject2
의 값을 수정함에도tObject1
의 값이 수정 된 것을 볼 수 있다.
사용자가 정의한 객체 타입을 JPA 에서는
Embedde Type
이라고 한다.
가만보면 이런 값이 왜 JPA 에서 필요하나 싶은데 다음을 보자.
@Entity
public class Member extends BaseEntity
{
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 기간 Period
private LocalDateTime startDate;
private LocalDateTime endDate;
// 주소 Address
private String city;
private String street;
private String zipcode;
다음 처럼
Member
엔티티의 필드들 도 다음 처럼 분류 할 수 있다.
다음과 같이 설정할 경우 재사용성이 떨어지고 눈에 보이는 method 를 정의하기 힘들다.
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Embeddable
public class Address
{
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member extends BaseEntity
{
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 기간 Period
@Embedded
private Period period;
// 주소 Address
@Embedded
private Address address;
Address
,Period
는 Entity 객체 내부에서 사용할Embedded
타입 객체로
class 명 위에@Embeddable
을 선언하고@Embeddable
객체를 필드로 사용할
Entity의 필드에@Embedded
를 선언하여 사용한다.JPA
는 Entity 필드에 주입 시 기본 생성자로 객체를 먼저 생성후Reflection API
를 사용하여 값을 Mapping 하기 때문에Embedded Type
을 정의할 때는 기본 생성자가 반드시 정의 되어야 한다.
Summary
1. Embedded Type 으로 정의한 Class 에는@Embeddable
을 붙인다.
2. Embedded Type 을 사용할 Entity 의 필드에는@Embedded
을 붙인다. (생략 가능 )
3.기본 생성자
가 정의 되어야 한다.
create table Member (
id bigint not null,
createDate timestamp,
createUser varchar(255),
modifyDate timestamp,
modifyUser varchar(255),
name varchar(255),
city varchar(255),
street varchar(255),
zipcode varchar(255),
endDate timestamp,
startDate timestamp,
TEAM_ID bigint,
primary key (id)
)
Hibernate:
Embedded Type
으로 정의된 객체의 필드들이 Entity 에 포함되어 Table Schema 에 정의 되는 것을 볼 수 있다.
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
public class Address
{
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member extends BaseEntity
{
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 기간 Period
private Period period;
// 주소 Address
private Address address;
@Entity
public class Member extends BaseEntity
{
...
// 집 주소
@Embedded
private Address homeaddress;
// 회사 주소
@Embedded
private Address workAddress;
다음과 같이
Embedded Type
인Address
객체를 여러번 사용하는 경우가 발생할 수 있다. 위와 같이 중복해서 사용하면 다음의 예외가 발생한다.Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: org.example.entity.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:347) at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:466) at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:939)
@Entity
public class Member extends BaseEntity
{
...
// 주소 Address
@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")),
})
private Address workAddress;
create table Member (
id bigint not null,
createDate timestamp,
createUser varchar(255),
modifyDate timestamp,
modifyUser varchar(255),
name varchar(255),
city varchar(255),
street varchar(255),
ZIPCODE varchar(255),
endDate timestamp,
startDate timestamp,
WORK_CITY varchar(255),
WORK_STREET varchar(255),
WORK_ZIPCODE varchar(255),
TEAM_ID bigint,
primary key (id)
)
Hibernate:
@AttributeOverrides
를 선언하고 안에서 다른 Column 으로 대처하면
DB Schema 에서 다른 컬럼 명으로 들어가서 정상적으로 Table 이 정의되는 것을 볼 수 있다.
Address address = new Address("city","street","10000");
Member member1 = new Member();
member1.setName("member1");
member1.setHomeaddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setName("member2");
member2.setHomeaddress(address);
em.persist(member2);
member1.getHomeaddress().setCity("newCity");
Hibernate:
/* update
org.example.entity.Member */ update
Member
set
createDate=?,
createUser=?,
modifyDate=?,
modifyUser=?,
name=?,
city=?,
street=?,
ZIPCODE=?,
endDate=?,
startDate=?,
TEAM_ID=?,
WORK_CITY=?,
WORK_STREET=?,
WORK_ZIPCODE=?
where
id=?
Hibernate:
/* update
org.example.entity.Member */ update
Member
set
createDate=?,
createUser=?,
modifyDate=?,
modifyUser=?,
name=?,
city=?,
street=?,
ZIPCODE=?,
endDate=?,
startDate=?,
TEAM_ID=?,
WORK_CITY=?,
WORK_STREET=?,
WORK_ZIPCODE=?
where
id=?
Embedded Type
은 객체 모델이기 때문에 복제가 아닌 참조를 사용한다.
memeber1
의 주소만 변경하더라도, 필드 값이 참조를 이용하는Embedded Type
이기 때문에member2
의 주소 값 역시 변경되고Update
Query 가 2번 발생하는 것을 확인할 수 있다.
Embedded Type
객체를 선언해서 사용 Address address = new Address("city","street","10000");
Member member1 = new Member();
member1.setName("member1");
member1.setHomeaddress(address);
em.persist(member1);
Address address2 = new Address(address.getCity(),address.getStreet(),address.getZipcode());
Member member2 = new Member();
member2.setName("member2");
member2.setHomeaddress(address2);
em.persist(member2);
// Member 1 의 City 를 변경
member1.getHomeaddress().setCity("newCity");
@Embeddable
public class Address
{
public Address(){}
public Address(String city, String street, String zipcode)
{
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
private String city;
private String street;
@Column(name = "ZIPCODE")
private String zipcode;
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipcode() {
return zipcode;
}
}
setter
를 없에서 생성자로만 객체가 생성되게 만들면 같은 객체를 참조하지 않아
문제를 원천 차단할 수 있다.
int one = 1;
int il = 1;
System.out.println("one : "+one);
System.out.println("il : "+il);
System.out.println((one == il) ? "같다.":"다르다.");
one : 1
il : 1
같다.
Address address1 = new Address("city","street","1000");
Address address2 = new Address("city","street","1000");
System.out.println("address1 : "+address1);
System.out.println("address2 : "+address2);
System.out.println((address1 == address2) ? "같다.":"다르다.");
address1 : org.example.entity.Address@3d494fbf
address2 : org.example.entity.Address@1ddc4ec2
다르다.
==
연산 시 참조 값을 비교하기 때문에 같은 값을 할당했더라고 해서 같지 않다.
JPA 에서 값 타입 비교 시equal
메소드를 사용해야한다. 사용할 때는 반드시 재정의를 하고 사용해야한다.
System.out.println((address1.equals(address2)) ? " (address1.equals(address2)) 같다.":" (address1.equals(address2)) 다르다.");
(address1.equals(address2)) 다르다.
Object 의
equals
의 기본값이==
를 사용하기 때문에 반드시 재정의가 필요하다.
public boolean equals(Object obj) { return (this == obj); }
equals 재정의
@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);
}
#### main
```java
System.out.println((address1.equals(address2)) ? " (address1.equals(address2)) 같다.":" (address1.equals(address2)) 다르다.");
(address1.equals(address2)) 같다.
DB
에서는 Collection 을 저장하는 구조가 존재하지
않게 때문에연관관계
ENTITY
를 설정한다. 엔티티가 아닌 값타입이 아닌 경우 어떻게 표현해야할까?
@Entity
public class Member extends BaseEntity
{
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 기간 Period
@Embedded
private Period period;
// 주소 Address
@Embedded
private Address homeaddress;
@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> addressHisotry = new ArrayList<>();
값 타입을 복수로 저장할 때 사용한다.
JPA 에서 복수로 값 타입을 할당하기 위해서 테이블을 생성한다.
@ElementCollection
: Collection 객체임을 JPA 에게 선언하며,
Entity 가 아닌 Embedded
값 타입에 대한 테이블을 1 : N
연관관계로 생성한다.
Member : ADDRESS = 1 : N
@CollectionTable
: Collection 테이블의 속성 값을 할당한다. Table 의 이름이나, 외래키 이름등을 설정
Member member = new Member();
member.setName("member1");
//FOOD INSERT
member.getFavoriteFoods().add("삼겹살");
member.getFavoriteFoods().add("짜파게티");
member.getFavoriteFoods().add("스파게티");
// ADDRESS INSERT
member.getAddressHisotry().add(new Address("o1","o1","o1"));
member.getAddressHisotry().add(new Address("o2","o2","o2"));
Hibernate:
/* insert org.example.entity.Member
*/ insert
into
Member
(createDate, createUser, modifyDate, modifyUser, name, city, street, ZIPCODE, endDate, startDate, TEAM_ID, id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.addressHisotry */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.addressHisotry */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
같은 생명주기로 관리되어
Member
와 함께 관리 된다.CASCADE.ALL
Member findMember = em.find(Member.class, member.getId());
System.out.println("ADDRESS");
List<Address> findAddresses = findMember.getAddressHisotry();
for (Address findAddress : findAddresses) {
System.out.println(findAddress.getCity());
}
System.out.println("::::::::::::::::::");
Set<String> findFavoriteFoods = findMember.getFavoriteFoods();
for (String findFavoriteFood : findFavoriteFoods) {
System.out.println(findFavoriteFood);
}
System.out.println("::::::::::::::::::");
Hibernate:
select
member0_.id as id1_3_0_,
member0_.createDate as createDa2_3_0_,
member0_.createUser as createUs3_3_0_,
member0_.modifyDate as modifyDa4_3_0_,
member0_.modifyUser as modifyUs5_3_0_,
member0_.name as name6_3_0_,
member0_.city as city7_3_0_,
member0_.street as street8_3_0_,
member0_.ZIPCODE as ZIPCODE9_3_0_,
member0_.endDate as endDate10_3_0_,
member0_.startDate as startDa11_3_0_,
member0_.TEAM_ID as TEAM_ID12_3_0_
from
Member member0_
where
member0_.id=?
ADDRESS
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=?
o1
o2
::::::::::::::::::
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=?
짜파게티
스파게티
삼겹살
Member findMember = em.find(Member.class, member.getId());
이때Member
필드들만 조회 되었다.
Collection
인ADDRESS
,FAVORITE_FOOD
은 해당 Collection 을 사용할 때
조회 됨으로지연 로딩
임을 알 수 있다.
System.out.println("::::::::::::::::::");
Member findMember = em.find(Member.class, member.getId());
Address address = findMember.getHomeaddress();
findMember.setHomeaddress(new Address("nc",address.getStreet(), address.getZipcode()));
//
findMember.getFavoriteFoods().remove("삼겹살");
findMember.getFavoriteFoods().add("보쌈");
Hibernate:
select
member0_.id as id1_3_0_,
member0_.createDate as createDa2_3_0_,
member0_.createUser as createUs3_3_0_,
member0_.modifyDate as modifyDa4_3_0_,
member0_.modifyUser as modifyUs5_3_0_,
member0_.name as name6_3_0_,
member0_.city as city7_3_0_,
member0_.street as street8_3_0_,
member0_.ZIPCODE as ZIPCODE9_3_0_,
member0_.endDate as endDate10_3_0_,
member0_.startDate as startDa11_3_0_,
member0_.TEAM_ID as TEAM_ID12_3_0_
from
Member member0_
where
member0_.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
org.example.entity.Member */ update
Member
set
createDate=?,
createUser=?,
modifyDate=?,
modifyUser=?,
name=?,
city=?,
street=?,
ZIPCODE=?,
endDate=?,
startDate=?,
TEAM_ID=?
where
id=?
Hibernate:
/* delete collection row org.example.entity.Member.favoriteFoods */ delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row org.example.entity.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
값 타입은 추적이 되지 않기 때문에 수정하려는 값을 삭제하고 수정 값을 넣어준다.
// remove 는 기본적으로 equals 메소드를 기반으로 동작
findMember.getAddressHisotry().remove(new Address("o1","o1","o1"));
findMember.getAddressHisotry().add(new Address("n1","o1","o1"));
Embedded Type
역시 값 타입이라 수정할 값을 삭제하고 값을 추가해야한다.
Collection
타입의remove
는equals
로 동작하기 때문에
수정 해야 할Embedded Type
을 삭제하고 새로 생성한다.
Hibernate:
select
member0_.id as id1_3_0_,
member0_.createDate as createDa2_3_0_,
member0_.createUser as createUs3_3_0_,
member0_.modifyDate as modifyDa4_3_0_,
member0_.modifyUser as modifyUs5_3_0_,
member0_.name as name6_3_0_,
member0_.city as city7_3_0_,
member0_.street as street8_3_0_,
member0_.ZIPCODE as ZIPCODE9_3_0_,
member0_.endDate as endDate10_3_0_,
member0_.startDate as startDa11_3_0_,
member0_.TEAM_ID as TEAM_ID12_3_0_
from
Member member0_
where
member0_.id=?
Hibernate:
select
addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
addresshis0_.city as city2_0_0_,
addresshis0_.street as street3_0_0_,
addresshis0_.ZIPCODE as ZIPCODE4_0_0_
from
ADDRESS addresshis0_
where
addresshis0_.MEMBER_ID=?
Hibernate:
/* delete collection org.example.entity.Member.addressHisotry */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row org.example.entity.Member.addressHisotry */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row org.example.entity.Member.addressHisotry */ insert
into
ADDRESS
(MEMBER_ID, city, street, ZIPCODE)
values
(?, ?, ?, ?)
INSERT Query 가 두번 일어났다. 이는
Embedded
값 타입 Collection 에 값이 변경되면 현재 값을 모두 다시 저장하기 때문에 두번의 INSERT 가 발생했다.
값 타입은 Entity 가 존재하지 않기 때문에 추적이 어렵고 그로 인한 수정이 더욱 어렵다.
따라서값 타입 Collection
을 사용하는 것 보다는값 타입 Collection
필드를 가진Entitiy
를 정의하여 사용하는 편이 좋다.
@Entity
@Table(name = "ADDRESS_HISTORY")
public class AddressEntity
{
@Id @GeneratedValue
private Long id;
@Embedded
private Address address;
@Entity 를 제외한 나머지 타입을 값 타입이라고 하며
유동적인 설계가 가능하지만참조
특성으로 인해 불변객체나 수정불가능하게 끔 고려해야하며
공유되어선 안된다. 동등성 검증을 위해 값 타입에서는Equals
메소드를 재정의해야한다.
특히 Collection 객체는 가급적 사용하지 않는 것이 좋다.