값 타입5(값 타입 컬렉션)

Mina Park·2022년 9월 13일
0

1. 기본개념

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함
@Entity
public class MemberMapping extends BaseEntity {

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

    @Column(name = "USERNAME")
    private String username;

    //임베디드타입 Period
    @Embedded
    private Period workPeriod;

    //임베디드타입 Address
    @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", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();

    //한 엔티티에 안에서 같은 값 타입을 사용하면 컬럼명이 중복되므로 @AttributeOverrides, @AttributeOverride로 컬럼명 속성 재정의
    @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_ZIPCODE")),
    })
    private Address workAddress;

    @ManyToOne(fetch = FetchType.LAZY) //지연로딩 사용해서 프록시로 조회
    @JoinColumn(name = "TEAM_ID")
    private TeamMapping team;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProductList = new ArrayList<>();
    
    //...getter,setter
}
Hibernate: 
    
    create table ADDRESS (
       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)
    )

Hibernate: 
    
    create table MemberMapping (
       MEMBER_ID bigint not null,
        createdAt timestamp,
        createdBy varchar(255),
        updatedAt timestamp,
        updatedBy varchar(255),
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        WORK_CITY varchar(255),
        WORK_STREET varchar(255),
        WORK_ZIPCODE varchar(255),
        endDate timestamp,
        startDate timestamp,
        LOCKER_ID bigint,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )

2. 값 타입 컬렉션 사용 예제
1) 저장

  • 값 타입은 영속성 전이(cascade) & 고아 객체 제거 기능(orphanRemoval)을 필수로 가진다고 볼 수 있음
    • 해당 엔티티의 생명주기에 의존함(별도의 persist, update 등은 불필요)
 			MemberMapping member = new MemberMapping();
            member.setUsername("member1");

            //값 타입 하나만 저장
            member.setHomeAddress(new Address("homeCity", "street1", "1111111"));

            //값 타입 복수 저장
            member.getFavoriteFoods().add("마라탕");
            member.getFavoriteFoods().add("양꼬치");
            member.getFavoriteFoods().add("쌀국수");

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

            em.persist(member);
            //member 저장시 값타입 컬렉션도 함께 저장
            //값타입 컬렉션도 값타입처럼 해당 엔티티의 생명주기에 의존함(별도의 persist 등은 필요 X)
            
            tx.commit();

2) 조회

  • 값 타입 컬렉션도 지연 로딩 전략 사용
    • 해당 객체 조회시에만 DB 쿼리 실행
			MemberMapping member = new MemberMapping();
            member.setUsername("member1");

            //1) 저장
            //값 타입 하나만 저장
            member.setHomeAddress(new Address("homeCity", "street1", "1111111"));

            //값 타입 복수 저장
            member.getFavoriteFoods().add("마라탕");
            member.getFavoriteFoods().add("양꼬치");
            member.getFavoriteFoods().add("쌀국수");

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

            em.persist(member);
            //member 저장시 값타입 컬렉션도 함께 저장
            //값타입 컬렉션도 값타입처럼 해당 엔티티의 생명주기에 의존함(별도의 persist 등은 필요 X)

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

            //2) 조회
            //값타입 컬렉션은 기본값이 지연로딩 => 해당객체 조회시 쿼리 실행
            System.out.println("================= START =================");
            MemberMapping findMember = em.find(MemberMapping.class, member.getId());

            List<Address> addressHistory = findMember.getAddressHistory();
            for (Address address : addressHistory) {
                System.out.println("address = " + address.getCity());
            }

            Set<String> favoriteFoods = findMember.getFavoriteFoods();
            for (String favoriteFood : favoriteFoods) {
                System.out.println("favoriteFood = " + favoriteFood);
            }

            tx.commit();
================= START =================
Hibernate: 
    select
        membermapp0_.MEMBER_ID as member_i1_11_0_,
        membermapp0_.createdAt as createda2_11_0_,
        membermapp0_.createdBy as createdb3_11_0_,
        membermapp0_.updatedAt as updateda4_11_0_,
        membermapp0_.updatedBy as updatedb5_11_0_,
        membermapp0_.city as city6_11_0_,
        membermapp0_.street as street7_11_0_,
        membermapp0_.zipcode as zipcode8_11_0_,
        membermapp0_.LOCKER_ID as locker_15_11_0_,
        membermapp0_.TEAM_ID as team_id16_11_0_,
        membermapp0_.USERNAME as username9_11_0_,
        membermapp0_.WORK_CITY as work_ci10_11_0_,
        membermapp0_.WORK_STREET as work_st11_11_0_,
        membermapp0_.WORK_ZIPCODE as work_zi12_11_0_,
        membermapp0_.endDate as enddate13_11_0_,
        membermapp0_.startDate as startda14_11_0_,
        locker1_.id as id1_6_1_,
        locker1_.name as name2_6_1_ 
    from
        MemberMapping membermapp0_ 
    left outer join
        Locker locker1_ 
            on membermapp0_.LOCKER_ID=locker1_.id 
    where
        membermapp0_.MEMBER_ID=?
================= LAZY LOADING1 =================
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=?
address = old1
address = old2
================= LAZY LOADING2 =================
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=?
favoriteFood = 마라탕
favoriteFood = 양꼬치
favoriteFood = 쌀국수

3) 수정

        	MemberMapping member = new MemberMapping();
            member.setUsername("member1");

            //1) 저장
            //값 타입 하나만 저장
            member.setHomeAddress(new Address("homeCity", "street1", "1111111"));

            //값 타입 복수 저장
            member.getFavoriteFoods().add("마라탕");
            member.getFavoriteFoods().add("양꼬치");
            member.getFavoriteFoods().add("쌀국수");

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

            em.persist(member);
            //member 저장시 값타입 컬렉션도 함께 저장
            //값타입 컬렉션도 값타입처럼 해당 엔티티의 생명주기에 의존함(별도의 persist 등은 필요 X)

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

            //2) 조회
            //값타입 컬렉션은 기본값이 지연로딩 => 해당객체 조회시 쿼리 실행
            System.out.println("================= START =================");
            MemberMapping findMember = em.find(MemberMapping.class, member.getId());

            System.out.println("================= LAZY LOADING1 =================");
            List<Address> addressHistory = findMember.getAddressHistory();
            for (Address address : addressHistory) {
                System.out.println("address = " + address.getCity());
            }

            System.out.println("================= LAZY LOADING2 =================");
            Set<String> favoriteFoods = findMember.getFavoriteFoods();
            for (String favoriteFood : favoriteFoods) {
                System.out.println("favoriteFood = " + favoriteFood);
            }

            //3) 수정
            //3-1) 값타입 단일 수정
            //homeCity -> newCity
            //findMember.getHomeAddress().setCity("newCity");
            //에러 => 값타입은 immutable object이어야하므로 setter를 삭제 or private으로 설정
            //따라서 값타입은 객체 자체를 아예 교체해줘야 함
            Address oldAddress = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", oldAddress.getStreet(), oldAddress.getZipcode()));

            //3-2) 값타입 컬렉션 수정
            //마라탕 -> 마라샹궈
            //마찬가지로 object를 찾아서 통째로 교체해줘야 함
            findMember.getFavoriteFoods().remove("마라탕");
            findMember.getFavoriteFoods().add("마라샹궈");

            //old1 -> new1
            //equals를 통해서 이전에 들어간 object를 찾아서 삭제 후 교체
            //쿼리상 member_id에 해당되는 컬렉션을 전부 삭제 후 old2, new1를 새로 저장
            findMember.getAddressHistory().remove(new Address("old1", "street", "1111111"));
            findMember.getAddressHistory().add(new Address("new1", "street", "1111111"));

            tx.commit();

📌값 타입 컬렉션의 제약사항

  • 엔티티와 다르게 식별자 개념이 X
  • 값을 변경하면 추적이 X
  • 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제 후 값 타입 컬렉션에 있는 현재값을 모두 다시 저장 => 실무에서 사용 X !!!
    • 그럼 해결책은?
      • 값 타입 컬렉션을 맵핑하는 테이블은 모든 컬럼을 묶어서 하나의 기본키를 구성
        • null 입력 X, 중복 저장 X

📌 값 타입 컬렉션의 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려
    • 일대다 맵핑을 위한 엔티티를 새로 만들고, 이 안에서 값 타입을 사용
    • 영속성 전이 + 고아객체 제거 어노테이션을 사용해서 값 타입 컬렉션처럼 사용

📌그럼 값 타입 컬렉션은 언제 사용할까?

  • 정말 간단한 경우에 사용(추적이 불필요한 경우에만)
    • 대부분은 엔티티로 사용
    • 예)콤보박스/셀렉트박스
//값타입 컬렉션을 엔티티로 새로 생성(엔티티로 승격)
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;

    private Address address; //값 타입

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public AddressEntity() {
    }

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

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

//    private MemberMapping member;

    public Address() {
    }

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

    public String getCity() {
        return city;
    }

//    public void setCity(String city) {
//        this.city = city;
//    }

    public String getStreet() {
        return street;
    }

//    public void setStreet(String street) {
//        this.street = street;
//    }

    public String getZipcode() {
        return zipcode;
    }

//    public void setZipcode(String zipcode) {
//        this.zipcode = zipcode;
//    }


    @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);
    }
}
    //값타입 컬렉션
    //    @ElementCollection
    //    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    //    private List<Address> addressHistory = new ArrayList<>();


    //값타입 컬렉션 => 엔티티 생성해서 일대다 관계로 변경
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true  )
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
      		MemberMapping member = new MemberMapping();
            member.setUsername("member1");

            //1) 저장
            //값 타입 하나만 저장
            member.setHomeAddress(new Address("homeCity", "street1", "1111111"));

            //값 타입 복수 저장
            member.getFavoriteFoods().add("마라탕");
            member.getFavoriteFoods().add("양꼬치");
            member.getFavoriteFoods().add("쌀국수");

//            member.getAddressHistory().add(new Address("old1", "street", "1111111"));
//            member.getAddressHistory().add(new Address("old2", "street", "1111111"));\
            member.getAddressHistory().add(new AddressEntity("old1", "street", "1111111"));
            member.getAddressHistory().add(new AddressEntity("old2", "street", "1111111"));

            em.persist(member);
            tx.commit();
  • 자체 id가 생성

3. 엔티티 vs 값 타입 특징 비교

📌 <엔티티>

  • 식별자 O
  • 생명주기를 직접 관리
  • 공유

📌 <값 타입>

  • 식별자 X
  • 생명주기를 엔티티에 의존
  • 공유해서는 X(복사해서 사용)
  • 불변객체로 만들어야함

값 타입은 정말 단순한 경우에만 사용!!!
식별자가 필요하고, 지속해서 값을 변경 or 추적해야한다면 엔티티!!!

0개의 댓글