JPA의 데이터 타입 분류
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 데체
- 생명주기를 엔티티에 의존
임베디드 타입
새로운 값 타입을 직접 정의 할 수 있음
주로 기본 값 타입들을 모아서 만들어서 복합 값 타입이라고 함, int,String 과 같은 값 타입
이런식으로 데이터들을 합쳐서 추상화 시켜서 좀 더 이해하기 쉽다.
JPA에서는 이것들을 @Embeddable (= 값타입을 정의하는곳에 표시), @Embedded (= 값 타입을 사용하는곳에 표시) 을 사용해서 표시한다.
테이블 입장에서는 달라질 것이 없다. 매핑만 해줄 뿐이다.
이렇게 묶었을 때 해당 객체와 연관된 메소드를 가지게 할 수 있는 장점이 있다.
//기간 Period
private LocalDateTime startDate;
private LocalDateTime endDate;
@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;
}
}
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city",
column=@Column(name= "WORK_CITY"))})
private Address workAddress;
임베디트 타입은 엔티티의 값일 뿐임으로 매핑하는 테이블은 그대로이다.
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
@AttributeOveride 를 통해 중복해서 사용할 수 있다. 이 경우에는 별도의 column이 지정한 이름으로 추가적으로 생겨난다.
값 타입과 불변 객체
값타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야한다.
기본타입의 경우는 복사할떄 동시에 여러개가 변하는 경우가 있을 수 없다. 하지만 객체는 그것이 가능하다.
Address 같은 임베디드 타입이 여러 엔티티에서 공유하면 위험해진다.
update쿼리가 2번 나가면서 2 member 모두다 변경이 되어버렸다.
이걸 방지하기위해 새로운 new Address로 만들어서 주입하는 방법이 있다.
애초에 값 타입은 불변객체로 만들어야한다.
불변 객체 = 생성 시점 이후 절대 값을 변경할 수 없는 객체 , 생성자로만 값을 설정, setter를 안 만들면 된다.
값 타입 컬렉션
@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<>();
Set<String> 처럼 안에 들어간 객체가 속성이 여러개가 아니면 Column으로 이름 매핑이 가능하다
create table FAVORITE_FOOD (
MEMBER_ID bigint not null,
favoriteFoods varchar(255)
)
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
값 타입을 하나 이상을 저장할때 @ElementCollection , @CollectionTable을 통해 사용한다.
이런식으로 아예 새로운 테이블이 형성이 된다.
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("city1","street","10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1","street","10"));
member.getAddressHistory().add(new Address("old2","street2","102"));
em.persist(member);
tx.commit();
모두다 값타입들이기 때문에 member랑 lifeCycle을 같이 동반한다. 그래서 따로 persist 해주지 않아도 문제 없다.
서로 다 동일한 Member_id 로 들어간것을 확인할 수 있다.
값타입은 조회 될떄 지연로딩할 이유가 없으므로 그냥 같이 조회되지만 값타입 컬렉션은 지연로딩을 사용한다.
위에서도 언급했지만 값타입을 변경하고 싶을 때는 항상 그냥 새로운 인스턴스를 만들어서 그냥 갈아껴줘야한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
Hibernate:
/* delete collection row hellojpa.Member.favoriteFoods */ delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and favoriteFoods=?
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */ insert
into
FAVORITE_FOOD
(MEMBER_ID, favoriteFoods)
values
(?, ?)
알아서 delete 쿼리를 날린후에 insert 쿼리를 날린다.
하지만 단순 String이 아닌 Address 컬렉션의 경우는 다르다.
@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);
}
findMember.getAddressHistory().remove(new Address("old2","street2","102"));
findMember.getAddressHistory().add(new Address("newcity1","street2","102"));
지울려면 이런식으로 객체를 그대로 넣어줘야 제대로 지워진다. 이때 equals와 hashcode가 제대로 구현이 되어있지 않으면 사고가 난다. 하지만 쿼리가 이상하다.
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
(?, ?, ?, ?)
delete 1 번 insert 2번 쿼리가 나간것을 확인 -> 아예 테이블에서 주인 엔티티와 관련된 row들을 다 지우고 새로 갈아끼기 떄문에 성능이 떨어질 수 있다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재값을 모두 다시 저장한다.
Address를 추적하는 방법으로는 @OrderColumn을 써서 값 타입 컬렉션 테이블에 order column을 추가해서 구분할 수 있도록해주는데 이것도 위험하다.
Collection element의 순서 column이 추가 된것이다. 0,1,2 이런식으로 쭉 증가한다.
중간에 하나빠지면 null도 들어가고 아무튼 쓰지 말라했다.
값 타입 컬렉션의 대안은 = 일대다 관계를 고려해야한다. 현재는 Address table의 PK가 존재하지 않으므로 DB 운영이 어려운 상태이다. -> 4개의 column으로 pk를 내가 만들어줘야 한다.
@Entity
@Table(name = "ADDRESS")
public class AddressEntity
{
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity()
{
}
public AddressEntity(Address address)
{
this.address = address;
}
public AddressEntity(String city, String street, String zipcode)
{
address = new Address(city,street,zipcode);
}
// @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<>();
findMember.getAddressHistory().remove(new AddressEntity("old2","street2","102"));
findMember.getAddressHistory().add(new AddressEntity("newcity1","street2","102"));
기존의 값 컬렉션에서 아예 하나의 AddressEntity를 생성하여 일대다 단방향 매핑관계를 만들어주었다.
AddressEntity 의 테이블이름을 Address로 해주었고 그 결과 PK가 생성이 된 테이블이 생성되었다.
그렇다면 값 타입 컬렉션은 언제 쓰는가? 진짜 단순한 경우에만 써야한다.
실전 예제 - 6. 값 타입 매핑
@Embeddable
public class Address
{
private String city;
private String street;
private String zipcode;
private String fullAddress()
{
return getCity()+getStreet()+getZipcode();
}
public String getCity()
{
return city;
}
public String getStreet()
{
return street;
}
public String getZipcode()
{
return 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());
}
}
equals 메소드를 오버라이딩 할때 getter를 통해서 값을 참조하는것이 좋다. 그냥 직접필드를 참조하게 되면 프록시인 경우에는 비교가 불가능하기 때문이다. 그리고 이것이 안전한 방법이다.
또한 이렇게 값 타입을 사용하게되면 그 클래스안에 의미있는 메소드들을 만들어 줄 수 있다.
'WEB > JPA' 카테고리의 다른 글
김영한 (ORM 표준 JPA 프로그래밍 11) 객체지향 쿼리 언어 소개2 - 중급문법 (0) | 2021.03.20 |
---|---|
김영한 (ORM 표준 JPA 프로그래밍 10) 객체지향 쿼리 언어 소개 (0) | 2021.03.18 |
김영한 (ORM 표준 JPA 프로그래밍 8) 프록시와 연관관계 관리 (0) | 2021.03.14 |
김영한 (ORM 표준 JPA 프로그래밍 7) 고급 매핑 (0) | 2021.03.13 |
김영한 (ORM 표준 JPA 프로그래밍 6) 다양한 연관관계 매핑 (0) | 2021.03.08 |