[JPA 기본] 값 타입 컬렉션

강신현·2022년 7월 25일
0
post-thumbnail

✅ @ElementCollection ✅ @CollectionTable
✅ 실무에서는 값 타입 컬렉션 대신에 일대다 관계를 고려한다. (대신 영속성 전이(Cascade) + 고아 객체 제거 사용)


값 타입 컬렉션

데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.

👉 따라서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

예제

1. 저장

영속성 전이 (Cascade) + 고아 객체 제거 기능을 필수로 가진다.
[JPA] 영속성 전이 (CASCADE), 고아 객체

  • Member
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;

@Column(name = "USERNAME")

@Embedded
private Address homeAddress;
    
@ElementCollection![](https://velog.velcdn.com/images/peanut_/post/32047e8e-000b-4f0a-bb3f-497788ae198e/image.png)

@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> faveriteFoods = new HashSet<>();

@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

  • Main
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

member.getFaveriteFoods().add("치킨");
member.getFaveriteFoods().add("족발");
member.getFaveriteFoods().add("피자");

member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));

em.persist(member);

2. 조회 및 수정

값 타입 컬렉션도 지연 로딩 전략 사용
[JPA] 즉시 로딩(EAGER) vs 지연 로딩(LAZY)

  • Main
// <저장>
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

member.getFaveriteFoods().add("치킨");
member.getFaveriteFoods().add("족발");
member.getFaveriteFoods().add("피자");

member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));

em.persist(member);

em.flush();
em.clear();
            
// <조회>
Member findMember = em.find(Member.class, member.getId());

// <수정>
// findMember.getHomeAddress().setCity("newCity); -> 불변 객체이므로 불가능
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));

findMember.getFaveriteFoods().remove("치킨");
findMember.getFaveriteFoods().add("한식");

- remove 주의 사항 (equals, hashCode 오버라이드)

remove()를 사용할때 equals, hashCode를 사용하는데, 삭제하고자 하는 값 타입 컬렉션에 맞게 equals와 hashCode가 오버라이드 되어 있지 않으면 삭제 되지 않는 상황이 생기기도 함

  • Main
// ...(위 코드와 동일)...
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

1. equals, hashCode를 override 하지 않고 진행

👉 old1이 삭제되지 않음

2. equals, hashCode를 override 하고 진행

  • Member에 아래 코드 추가
@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);

👉 old1이 정상적으로 삭제됨

제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없으므로 값이 변경되면 추적이 어렵다.
  • 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
    • 위 예제에서 쿼리를 확인해보면 old1 만 삭제하고 newCity1을 추가하는 것이 아니라, old1과 old2모두 삭제한 후 old2와 newCity1을 추가한 것을 알 수 있음
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. (null 입력X, 중복 저장X)

실무

  • 여러 불편함이 있기 때문에 실무에서는 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
  • 대신 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬 렉션처럼 사용한다.
  • 정말 단순한 경우, 값을 추적할 필요가 없을 경우에만 선택적으로 값 타입을 사용한다.

예제

  1. 여러 Address를 담을 AddressEntity를 따로 만든다.
  • AddressEntity
@Entity
public class AddressEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Address address;
}
  1. AddressEntity를 일대다 관계로 매핑한다.
  • Member
// 값 타입으로 매핑
// @ElementCollection
// @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
// private List<Address> addressHistory = new ArrayList<>();

// 엔티티로 매핑
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) // 일대다 관계로 매핑(영속성 전이(Cascade) + 고아 객체 제거)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
profile
땅콩의 모험 (server)

0개의 댓글