본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


JPA

JPA 공부 12 - 값 타입 collection 사용

반응형
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    //기간
    @Embedded
    private Period period;

    //주소
    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides(value = {
            @AttributeOverride(name = "zipcode",column = @Column(name = "work_zipcode")),
            @AttributeOverride(name = "street",column = @Column(name = "work_street"))
    })
    private Address workAddress;


    
    @OneToMany(cascade = CascadeType.ALL,orphanRemoval = true)
    @JoinColumn(name = "member_id")
    private List<AddressHistory> addressHistories = new ArrayList<>();

    //값타입 컬랙션을 사용하기 위한 방법이다.
    //테이블을 만들어낸다.
    //생명주기가 자동으로 관리된다.
    @ElementCollection
    @CollectionTable(name= "FAVORITE_FOOD",joinColumns = @JoinColumn(name = "member_id"))
    private List<String> favoriteFoods = new ArrayList<>();



    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;


}

 

JPA에서 엔티티에서 값으로만 사용되는 타입들을 값 타입이라 한다. 위의 코드를 예를 들면 private Team team;

 

필드를 제외한 모든 필드는 값 타입이라 할 수 있다.  

 

 

1.  값 타입 컬랙션 

 

DB에서는 컬랙션을 담는 필드 만들 수 없다  , String 이나 Integer Object  자바 객체들을 담는 컬랙션을 

DB로 표현하기위해서 JPA는 컬랙션에 대한 테이블을 만들어서 부모테이블과 연관 관계를 맺어 사용하게 한다.

 

@ElementCollection 

@CollectionTable(name = "테이블 이름",joinColumns = @JoinColumn(name="member_id"))

 

위의 두 어노테이션을 지정해준다 .   

@ElementCollection은 기본 컬랙션 타입임을 선언하고 

@CollectionTable은 만들어질 테이블의 이름과 외래 키를 정하는 어노테이션이다 . 

 

프로그램을 실행하면 

 

 

위와 같은 테이블이 생성된다 

 

위 방법의 문제는  해당 값을 삭제할 때 발생한다 . 

 

Member member = new Member();
member.setName("member1");
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("고기");

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

Member member1 = em.find(Member.class, member.getId());
//값 타입 리스트 테이블을 모두 지우고   다시 넣는다 .
member1.getFavoriteFoods().remove("피자");

 

favoriteFoods에 '치킨',피자','고기'을 추가하고 저장 후에 '피자'라는 값을 지워봤다. 

 

쿼리를 살펴보자 

 

 

 

? 멤버 favorit_food 테이블을 몽땅 지워버리고 값을 다시 넣었다 .

 

값 타입 컬랙션은 DB에서 테이블로 표현 된다고 했다 . 생성된 테이블은 PK값을 따로 갖고 있지 않다 . 

 

참조하는 member의 외래키 값만 가지고 있기 때문에 JPA에서는 값을 지우려해도 그 값의 고유키가 없어 찾을 방법이 없기 때문에 

 

해당 테이블을 모두 지워버리고 그 remove() 된 레코드를 뺀 테이블에 원래 있던 레코드들을 다시 집어넣는다 . 

 

위의 경우 3개가 들어 있었고 , 1개를 뻈으니 테이블이 지워진 후 남아있던 2개가 다시 채워진 것이다 . 

 

이 같은 문제는 값 타입이기 때문에 발생하는 문제다 처음에 말했듯이 값 타입은 스스로 생명주기를 갖지 못하고 , 값의 변경에 대한 추적이

 

불가능 하기 떄문이다. 

 

때문에 정말 간단한 데이터를 담는 것이 아닌 이상 이 방법으로는 사용하지 않는 것이 좋다 . 

 

만약 테이블에 100개가 들어있다고 하면 , 1개를 지울때 인서트문이 100개가 나갈 수 있다 . 

 

 

2.값 타입  컬렉션 문제 해결

값 타입 컬렉션의 문제를 해결하는 방법은 해당 값타입을 담을 엔티티를 만들어 랩핑하는 방법이 있다 . 

 

회원의 주소 이력을   담는 AdressHistory라는 엔티티를 만들었다 .

 

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "address")
public class AddressHistory {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address address;

    public AddressHistory(Address address) {
        this.address = address;
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(id, address);
    }
}

 

 

아이디 값과 임베디드 타입의 Address를 담고 있다 . 값 타입 컬랙션안의 요소들의 삭제를 위해선 비교를 위한 

Equals 코드와 Hash를 오버라이드 하는 것이 필수다 .(list.get.remove() <- 메서드는 equals메서드를 통해 값을 찾아냄

 

이제 Address 가 엔티티로 관리될 것이다. 

 

맨위의 Member 엔티티 클래스를 보면 알 수 있듯이 1대다 단방향으로 맵핑이 되어있다 . 1대다 단방향의 특성상  외래키를 AddressHistory에서 관리하기 떄문에 update 문이 생기는 건 피할 수 없다. 

 

엔티티 타입으로 랩핑 됬지만 값타입의 특성을 지키기 위해선 @OneToMany(cascade = CascadeType.ALL,orphanRemoval = true) 처럼   생명주기를 부모객체인 멤버에 맡기는 것이 좋다. 

 

결론은 값 타입은 진짜로 간단한 데이터에만 사용하던지 

 

사용해야한다면 엔티티로 랩핑하여 사용하는 것이 좋을 것 같다. 

 

반응형