JPA의 데이터 타입
JPA의 데이터 타입은 크게 엔티티 타입과 값 타입으로 나눌 수 있다.
엔티티 타입과 값 타입의 가장 큰 차이는 식별자의 유무이다.
엔티티는 식별자를 통해 구별할 수 있지만 값은 할 수 없다.
기본 값
기본 값은 엔티티에 생명주기를 의존한다.
Member가 삭제되면 age나 name이 삭제되기 때문이다.
임베디드(embedded)
임베디드 타입은 사용자가 JPA에서 새로운 값 타입을 직접 정의한 것이다.
임베디드 타입은 여러 곳에 사용할 수 있는 재사용성이 좋다.
member도 주소가 필요하고, group도 주소가 필요할 때
member에 주소를 모두 쓰고, group에도 주소를 모두 쓰는 것보다
주소 임베디드 타입을 만든다면 매우 객체지향적이고 깔끔한 코드가 될 것이다.
임베디드 타입을 포함한 모든 값 타입은 해당 엔티티에 생명주기를 의존한다.
예를 들어, 주소 라는 값 타입을 사용하고 싶다.
(주소 안에는 우편번호, 거리 이름, 도시 이름이 있다)
해당 임베디드 클래스에서는
@Embeddable
엔티티에서는 임베디드를 나타내는
@Embedded
private Address homeAddress;
어노테이션을 사용한다.
하지만 주의해야할 점이 있다.
하나의 엔티티 클래스에서 같은 임베디드 타입을 사용하면 컬럼 명이 중복된다.
Repeated column in mapping for entity를 마주치게 된다.
따라서 전에 책을 통해 배웠던 @AttributeOverrides를 사용하면 된다.
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city",
column = @Column(name = "WORK_CITY")),
@AttributeOverride(name = "street",
column = @Column(name = "WORK_STREET")),
@AttributeOverride(name = "zipcode",
column = @Column(name = "WORDK_ZIPCODE")),
})
private Address workAddress;
결과가 이렇게 나옴을 알 수 있다!
만약 임베디드 타입의 값이 null이면 매핑한 컬럼도 모두 null이다.
즉, 주소가 null 이면 거리 이름, 우편번호, 도시가 모두 null이 된다!
값 타입과 불변 객체
객체를 사용하다 보면 변경에서 부작용이 발생하게 된다.
member1의 주소를 바꿨는데, member2의 주소도 같이 바뀌는 것을 알 수 있다.
Address address = new Address("city", "street", "zipcode");
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
이 방법은 새로운 객체를 만들어 넣어줘야한다.
값 타입 컬렉션
@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<>();
해당 연관관계는 일대다 연관관계에서, 연관관계 주인이 현재 일에 있다.
값 컬렉션을 사용하기 위해 @ElementCollection 어노테이션을 사용한다.
하지만 관계형 DB는 컬렉션 같은 테이블을 저장할 수 없다.
따라서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
이 때, @CollectionTable을 사용하여 테이블을 만든 뒤에 매핑을 한다.
FAVORITE_FOOD 같은 경우에는 값이 한 개(String)이기 때문에 @Column 을 사용할 수 있다.
만약 Address의 이름을 바꾸고 싶다면 @AttirubteOverride를 사용해야한다.
즉, @Column 을 사용할 수 없다.
값 타입 컬렉션 사용
처음에 값 타입 컬렉션에 값을 저장해본다.
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
얘네는 따로따로 persist하지 않고 member만 persist해도 저장이 된다.
ADDRESS, FAVORITE_FOOD는 Member와 다른 테이블인데도 불구하고 생명주기가 같다.
왜?
값 타입 컬렉션도 값 타입의 종류로 본인 스스로 라이프 사이클이 없다.
마치 cascade.all + orphanremoval = true 기능이 있는 것이다.
이제 조회해본다.
조회를 하면 멤버만 가져오는 것으로 보아 컬렉션들은 모두 지연로딩인 것을 알 수 있다.
System.out.println("=========ADDRESS 조회==============");
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
System.out.println("=========FOOD 조회===========");
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
@ElementCollection의 내부를 보니 fetch의 default 전략이 LAZY임을 알 수 있다.
그렇다면 수정은 어떻게 해야하까?
임베디드 값 타입을 수정
findMember.getHomeAddress().setCity("newCity");
앞서 배우다시피, 위와 같이 코드를 작성하게 되면 안된다.
새로운 address 객체를 생성하여 넣어줘야한다.
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
기본값 타입 컬렉션 수정
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
임베디드 값 타입 컬렉션 수정
처음에는 Address 클래스에 equals와 hashcode를 오버라이딩하지 않은 체로 삭제를 할 때,
제대로 삭제되지 않는다.
현재 2개의 데이터가 있었는데 삭제를 하고나서도 여전히 데이터의 개수는 2개이다.
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
List<Address> addressHistory = findMember.getAddressHistory();
System.out.println("addressHistory.size() = " + addressHistory.size());
하지만 equals와 hashcode를 오버라이딩 하고 나서 위 코드를 실행하게 되면 개수가 줄어든다.
하지만 삭제를 하면서 신기한 점이 있다.
Member findMember = em.find(Member.class, member.getId());
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
위 쿼리를 보면 insert가 두 개 나가는 것을 알 수 있다.
왜 이렇게 나갈까?
컬렉션이 아닌 단일 값 타입은 값이 변경되어도 엔티티를 DB에서 찾고 수정하면 추적하기 쉽다.
그러나 값 타입 컬렉션은 별도의 테이블에 보관된다. 그래서 해당 테이블의 값 타입의 값이 변경되면 DB에 있는 원본 데이터를 찾기 어렵다. 즉, address는 삭제하면 id가 있는게 아니라 추적이 어렵게 된다. 또한, 중간에 city나 street, zipcode 중에 중간에 수정되면 알 수 없다.
그래서 AddressEntity를 새로 생성한다.
그리고 Member와 일대다 연관관계를 맺는다. 물론 연관관계의 주인은 Member이다.
이 때, cascade.all과 orphanremoval = true 옵션을 사용한다면 값 타입 컬렉션처럼 사용할 수 있다.
Reference
자바 ORM 표준 JPA 프로그래밍
'Spring 강의 > JPA - 기본편' 카테고리의 다른 글
객체지향 쿼리 언어2 (0) | 2022.01.05 |
---|---|
객체지향 쿼리 언어1 (1) ~ 프로젝션 (0) | 2021.12.14 |
프록시와 연관관계 관리(2) 즉시 로딩과 지연 로딩 ~ 영속성 전이 (0) | 2021.12.10 |
프록시와 연관관계 관리 (1) 프록시 (0) | 2021.12.10 |
고급 매핑 (0) | 2021.12.07 |