JPA/JPA 기본

35. 값타입 컬렉션

sdafdq 2023. 10. 29. 23:11

값타입 컬렉션은 값타입을 컬렉션에 담아서 쓰는거다.

 

기본값타입이나 임베디드를 컬렉션에 담아서 쓰는 걸 말하는 건가..

 

 

지금까지 엔티티는 컬렉션에 넣어서 한 적은 있는데..

 

 

일단. RDB 테이블에서는 컬렉션을 할 수 있는 구조는 없다. (json 하면서 그런 것들에 대한 걸 지원하는 DB도 있긴 하지만)

그래서 저렇게 테이블을 따로 나눈 듯 하다.

그래서 그냥 개념적으로 보면 일대다랑 같다.

 

보면은 다 PK이다.

뭐 저렇게 식별자 같은 개념을 넣어서 걔를 PK로 쓰면 그거는 값타입이 아니라 엔티티라고..

그냥 이럴경우는 엔티티로 설계하면 되는 거 아닌가..?

 

 

여튼 그래서 값타입은 이렇게 이 값들을 테이블에 값들만 저장되고 이걸 묶어서 PK로 구성을 하면 된다고 하는데...

 

 

 

기본자료구조 하위에 있는 인터페이스(Set, List 등)는 엔티티에서 다 쓸 수 있는 모양이다.

 

코드

@Entity
public class Member  extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="MEMBER_ID")
    private Long id;
    private String name;

    @Embedded
    private Period period;

    @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_HISTORY", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();

    @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_ZIP"))
    })
    private Address workAddress;
}

값타입 컬렉션은 값을 하나 이상 저장할 때 사용.

@ElementCollection(이건 값타입의 컬렉션이라는) 과 @CollectionTable(콜렉션으로 쓸 테이블에 대한 정보)

 

RDB는 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

 

결국 일대다로 풀어서 별도의 테이블로 만드는 방법임.

Hibernate: 
    create table ADDRESS_HISTORY (
        MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )
Hibernate: 
    create table FAVORITE_FOOD (
        MEMBER_ID bigint not null,
        FOOD_NAME varchar(255)
    )

 

 

난 근데 의문이 들었다 사실.

이럴거면 그냥 엔티티로 쓰면 되지 않나..?

보통 뭐랄까 select박스? 그런거에 정말 간단한 거에 쓰고, 보통은

일대다를 쓴다고 한다.

 

일단은 사실 여기서 엔티티와 다른 건,

생명주기가 부모와 같다는 것이다.

 

뭐 근데 우리가 이전시간에 배웠었던 영속성 전이, 고아객체가 있다.

아 근데 그렇게 쓴다고 한다. 한번 엔티티로 감싸서.

이걸 뭐 값타입 승격? 이라고 하셨던 것 같다.

 

그리고, 이 값타입 컬렉션 기본동작이 정말 말도 안되는 게,

주인 엔티티와 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션에 있는 현재값을 모두 저장한다고 한다. (임베디드는 그럼. )

 

그러니까

Member member = new Member();
member.setName("member1");

member.getAddressHistory().add(new Address("old1", "street1", "zip1"));
member.getAddressHistory().add(new Address("old2", "street1", "zip1"));
member.getAddressHistory().add(new Address("old3", "street1", "zip1"));

em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

findMember.getAddressHistory().remove(new Address("old1", "street1", "zip1"));
findMember.getAddressHistory().add(new Address("newCity1", "street1", "zip1"));

이렇게 임베디드 넣어놨다가 저거 특정 저거 지우고 (지울때는 equals()로 필터하여 찾아서 지움. 그래서 equals() 오버라이드 하는 거 중요함.)

새롭게 임베디드 추가하면,

Hibernate: 
    /* one-shot delete for jpabook.jpashop.domain.Member.addressHistory */delete 
    from
        ADDRESS_HISTORY 
    where
        MEMBER_ID=?
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member.addressHistory */insert 
    into
        ADDRESS_HISTORY (MEMBER_ID,city,street,zipcode) 
    values
        (?,?,?,?)
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member.addressHistory */insert 
    into
        ADDRESS_HISTORY (MEMBER_ID,city,street,zipcode) 
    values
        (?,?,?,?)
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member.addressHistory */insert 
    into
        ADDRESS_HISTORY (MEMBER_ID,city,street,zipcode) 
    values
        (?,?,?,?)

값이 이렇게 나감.

그러니까, Member와 관련된 거 싹 다 지운다음에,

현재 그 콜렉션에 있는거 다시 다 집어넣는 거임.

 

왜냐하면 임베디드는 기본값이기 때문에 추적이 안되니까.

 

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

이런 건 아예 기본타입이라서 

Hibernate: 
    /* delete for jpabook.jpashop.domain.Member.favoriteFoods */delete 
    from
        FAVORITE_FOOD 
    where
        MEMBER_ID=? 
        and FOOD_NAME=?
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member.favoriteFoods */insert 
    into
        FAVORITE_FOOD (MEMBER_ID,FOOD_NAME) 
    values
        (?,?)

이렇게 따로 쿼리 나가서 찾을 수 있긴 한데..

 

애초에 그리고

Hibernate: 
    create table ADDRESS_HISTORY (
        MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )

PK가 아님. 하긴 기본값이라.

 

근데 또 이것도 뭐 @OrderColumn해서 순서주는 컬럼 추가하도록 할 수 있음.

근데 이럴거면.. 차라리 엔티티를 쓰지..

그리고 이것도 위험하다고 함.

 

이렇게 쓸거면, 그냥 엔티티를 쓰면 된다.

 

차라리 일대다 관계를 써라.

https://qwefdg3.tistory.com/731

옛날 연관관계 배울 때,

다대일 단방향 많이쓰고,

일대다 관계는 권장하지 않지만 가끔 쓴다고 했다.

일대일도 가끔 쓰고.

다대다는 쓰면 안되고.

 

일대다를 쓰지 않는 이유는 엔티티가 가리키는 테이블에 외래키가 있는 것이 아니라 한번 거쳐가야 하기 때문.

테이블의 외래키 주인은 무조건 다대일의 다 쪽이다.

 

그 가끔쓰는 일대다 중 하나의 케이스가 이거인 것 같다.

 

일단 그러면, 일대다로 승격시킨 코드

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address address;

    public AddressEntity() {
    }

    public AddressEntity(String city, String street, String zipcode) {
        this.address = new Address(city, street, zipcode);
    }
}

새로 엔티티를 만들어 씀.

엔티티가 저 임베디드를 감싼다.

 

@Entity
public class Member  extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="MEMBER_ID")
    private Long id;
    private String name;

    @Embedded
    private Period period;

    @Embedded
    private Address homeAddress;

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

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();

    @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_ZIP"))
    })
    private Address workAddress;
}
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

이 부분.

cascade = ALL해서 부모의 생명주기와 맞추고,

orphanRemoval = true해서 고아제거 참으로 해 준다.

기준컬럼(외래키)은 MEMBER_ID, 이 엔티티의 id로 해준다.

 

Member member = new Member();
member.setName("member1");

member.getAddressHistory().add(new AddressEntity("old1", "street1", "zip1"));
member.getAddressHistory().add(new AddressEntity("old2", "street1", "zip1"));
member.getAddressHistory().add(new AddressEntity("old3", "street1", "zip1"));

em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

findMember.getAddressHistory().remove(0);
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street1", "zip1"));

tx.commit();

이렇게.

물론 일대다 이기 때문에 

Hibernate: 
    /* insert for
        jpabook.jpashop.domain.AddressEntity */insert 
    into
        ADDRESS (city,street,zipcode,id) 
    values
        (?,?,?,?)
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.AddressEntity */insert 
    into
        ADDRESS (city,street,zipcode,id) 
    values
        (?,?,?,?)
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.AddressEntity */insert 
    into
        ADDRESS (city,street,zipcode,id) 
    values
        (?,?,?,?)
Hibernate: 
    update
        ADDRESS 
    set
        MEMBER_ID=? 
    where
        id=?
Hibernate: 
    update
        ADDRESS 
    set
        MEMBER_ID=? 
    where
        id=?
Hibernate: 
    update
        ADDRESS 
    set
        MEMBER_ID=? 
    where
        id=?

이렇게 Address를 추가하면 update 쿼리가 추가로 나간다.

먼저 나를 insert 후 내 member_id를 할당 받은 후, 

 

address들에게 추가로 내 member_id를 update 해줘야 한다.

내가 순간적으로 그러면 member를 insert 먼저 하고 address를 나중에 MEMBER_ID에 값을 주면서 하면 되지 않나? 생각해 봤는데,

영속성 컨텍스트 기준으로 관리해서 그러는 건가?

https://www.inflearn.com/questions/1061109

 

여튼 이렇게 하면 되고, 그리고 수정할 때 index 0번을 찾아서 지우는 데, 사실 인덱스로 지울 수 있을만한 비즈니스 상황을 잘 모르겠다. city, zip코드 이런 거 선택해서 걔중에서 선별해서 getId() 이런 식으로 해서 인덱스를 가져와서 한다..? 이런 건가. 여튼 그런 것 보다는,

 

findMember.getAddressHistory().remove(new AddressEntity("old1", "street1", "zip1"));
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street1", "zip1"));

이렇게..

콜렉션.remove()는 인덱스도, equals()도 되니까..

 

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    AddressEntity that = (AddressEntity) o;
    Address thatAddress  = ((AddressEntity) o).getAddress();
    if(o == null) return false;
    if(this.address.equals(thatAddress)) return true;
    return Objects.equals(id, that.id) && Objects.equals(address, that.address);
}

AddressEntity에 equals() 를 추가로 정의해 줬다.

인텔리제이가 자동으로 만들어 주는 equals()에

@Override
public boolean equals(Object o) {
    Address thatAddress  = ((AddressEntity) o).getAddress();
    if(o == null) return false;
    if(this.address.equals(thatAddress)) return true;
}

이정도 더했다.

 

Address의 equals()는 만들어 놨으니 그걸 활용했다.

 

 

'JPA > JPA 기본' 카테고리의 다른 글

37. JPQL  (0) 2023.10.30
36. 값타입 실전  (0) 2023.10.30
34. 값타입 비교  (0) 2023.10.29
33. 값타입과 불변 객체  (0) 2023.10.29
32. 임베디드 타입  (0) 2023.10.29