본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편
을 수강하며 기록한 필기 내용을 정리한 글입니다.
-> 인프런
-> 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의
@OneToMany
어노테이션과 함께 엔티티 컬렉션을 활용했었지만, 본 내용에서는 값 타입을 컬렉션에 담아서 활용하는 방안에 대해 다룸.@ElementCollection
, @CollectionTable
@Entity
public class Member {
...
@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
: 값 타입 컬렉션임을 설정@CollectionTable
: 값 타입 컬렉션 데이터를 저장할 테이블 관련 설정@Column(name = "FOOD_NAME")
: FAVORITE_FOOD 테이블의 컬럼은 어차피 하나이기 때문에 예외적으로 해당 컬럼에 대한 설정을 적용할 수 있음.@ElementCollection
, @CollectionTable
사용Member member = new Member();
member.setUsername("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);
Member member = new Member();
member.setUsername("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);
em.flush();
em.clear();
System.out.println("=============== START ==============="
Member findMember = em.find(Member.class, member.getId());
Member member = new Member();
member.setUsername("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);
em.flush();
em.clear();
System.out.println("=============== START ==============="
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println(address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String food : favoriteFoods) {
System.out.println(food);
}
=============== START ===============
Hibernate:
select
member0_.MEMBER_ID as member_i1_7_0_,
member0_.city as city2_7_0_,
member0_.street as street3_7_0_,
member0_.zipcode as zipcode4_7_0_,
member0_.USERNAME as username5_7_0_,
member0_.endDate as enddate6_7_0_,
member0_.startDate as startdat7_7_0_
from
Member member0_
where
member0_.MEMBER_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=?
old1
old2
Hibernate:
select
favoritefo0_.MEMBER_ID as member_i1_4_0_,
favoritefo0_.FOOD_NAME as food_nam2_4_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
족발
치킨
피자
@ElementCollection
어노테이션의 fetch
속성을 들여다봐도 default로 FetchType.LAZY
가 설정되어 있는 것을 확인할 수 있다....
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress.setCity("newCity"); // 절대 금지
Address old = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", old.getStreet(), old.getZipcode()));
tx.commit();
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));
remove()
메소드는 기본적으로 equals()
메소드를 기반으로 값 비교를 하기 때문에, Address 값 타입의 equals()
재정의는 필수이다.Address old = findMember.getAddressHistory().stream()
.filter(a -> a.getCity().equals("old1"))
.collect(Collectors.toList())
.get(0);
findMember.getAddressHistory().remove(old);
findMember.getAddressHistory().add(new Address("new1", old.getStreet(), old.getZipcode()));
delete old1 → insert new1
으로 나가야 할 것이다.@OrderColumn(name = "address_history_order")
이와 같이 @OrderColumn
어노테이션을 활용함⇒ ⭐️ 이렇게 쿼리를 어마무시하게 보내거나, 혹은 복잡하게 쓸 바에는 쓰지 않는 것이 좋다. ⭐️
영속성 전이 + 고아 객체 제거
추가해서 → 이를 “값 타입 컬렉션에서 엔티티로 승격시킨다”고 한다.AddressEntity
를 생성하고, 이 안에서 Address
값 타입을 사용하는 것이다.@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity(String city, String street, String zipcode) {
address = new Address(city, street, zipcode);
}
public AddressEntity() {
}
}
Address
값 타입 컬렉션을 쓰는게 아니라, 해당 AddressEntity
와 일대다 연관관계 매핑을 시키는 것이다.@JoinColumn
어노테이션으로 연관관계 주인을 설정하는 것은 상황에 따라 적절히 설정하면 된다.영속성 전이 + 고아 객체 제거
를 꼭 추가해서 주인 엔티티가 라이프 사이클을 가져가도록 설정하는 것이다....
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
AddressEntity
의 equlas()
메소드도 적절히 잘 설정해야 할 것이다.@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
AddressEntity that = (AddressEntity)o;
return Objects.equals(id, that.id) && Objects.equals(address, that.address);
}
@Override
public int hashCode() {
return Objects.hash(id, address);
}
equals()
메소드에서 기준으로 여기는 필드가 AddressEntity
의 id값과 address 필드가 된다.이렇게 문제가 보이는 값 타입 컬렉션은 언제 써야할까?
진짜 단순하고 간단할 때 쓰일 수 있다.
그렇지 않은 이상, 웬만하면 엔티티로 승격해서 쓰는게 낫다.
⭐️ 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티로 설정해야 한다. ⭐️