BE전문가 프로젝트
값 타입 컬렉션 본문
값 타입을 컬렉션에 담아서 사용하는 것을 의미한다.
@Entity
public class Member extends BaseEntity{
@Id
@GeneratedValue
private Long id;
@Column(name="USERNAME")
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="TEAM_ID")
private Team team;
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
@Embedded
private Period workPeriod;
@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<>();
}
실행시키면 위에 있는 테이블과 같은 테이블이 만들어진다.
값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- @ElementCollection, @CollectionTable 사용
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.(일대다 개념이기 때문에 한 테이블에 넣을 수는 없다.)
- 컬렉션을 저장하기 위한 별도의 테이블이 필요함
값 타입 저장 예제
try {
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
member1.getFavoriteFoods().add("치킨");
member1.getFavoriteFoods().add("피자");
member1.getFavoriteFoods().add("족발");
member1.getAddressHistory().add(new Address("old1", "street", "1000"));
member1.getAddressHistory().add(new Address("old2", "street", "10005"));
em.persist(member1);
Address copiedAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member1.setHomeAddress(copiedAddress);
tx.commit();
}
코드를 실행하면 ADDRESS테이블에는 주소 정보, FAVORITE_FOODS에는 음식 값이 각자 맞는 테이블에 입력된 것을 확인할 수 있다. persist시에 모든 값이 동시에 들어가는 것을 확인 할 수 있는데 이것은 라이프 사이클이 같다는 것을 의미한다. 그 이유는 값타입이기 때문이며 모든 라이프 사이클 즉 생명주기가 member에 소속된 것을 알 수 있다. String name도 값타입이고 List<Address>도 마찬가지로 값타입이다. 따라서 별도로 persist를 해줄 필요가 없다.
값 타입 조회 예제
값 타입 컬렉션도 지연 로딩 전략 사용
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
지연로딩을 사용하지 않는 다면 member테이블에 있는 정보만 조회가 된다. 따라서 지연로딩을 설정해주는 것이 좋다.
값 타입 수정 예제
Member findMember = em.find(Member.class, member1.getId());
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("home", a.getStreet(), a.getZipcode()));
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
값 타입 통으로 바꿔야 한다. set 역시 값 타입이기 때문에 삭제 후 생성해주어야 한다.
member에 lifeCycle모두 맏기는 것이다.
public class
JPAMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); //트랜잭션 시작
try {
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
member1.getFavoriteFoods().add("치킨");
member1.getFavoriteFoods().add("피자");
member1.getFavoriteFoods().add("족발");
member1.getAddressHistory().add(new Address("old1", "street", "1000"));
member1.getAddressHistory().add(new Address("old2", "street", "10005"));
em.persist(member1);
Address copiedAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member1.setHomeAddress(copiedAddress);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member1.getId());
findMember.getAddressHistory().remove(new Address("old1", "street", "1000"));
findMember.getAddressHistory().add(new Address("old4", "street", "1000"));
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
emf.close();
}
}
list에 담겨있는 address클래스 중 변수의 일부분을 수정하고 싶다면 위에 코드처럼 전체를 가져와 삭제후 add로 값을 넣어줘야 한다. 따라서 hashCode가 제대로 구현되어 있지 않으면 로직에 큰 문제가 생긴다.
위에 코드를 실행 후 콘솔창들 보면 old1이 포함되어 있는 데이터를 삭제 후 새로운 데이터 즉 old4의 데이터만 insert되기를 기대했지만 old2와 old4 둘다 insert가 되는 것을 확인할 수 있다. 그 이유는 값 타입 컬렉션의 제약사항에서 알아보자
값 타입 컬렉션의 제약 사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력X, 중복 저장X
따라서 사용하지 않는 것을 권장한다.
값 타입 컬렉션의 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
값 타입 컬렉션의 사용
추적할 필요도 없고 값이 변경되어도 update 쿼리를 보내지 않을 정도로 단순하게 사용한다면 사용이 가능하지만 되도록이면 Entity를 사용하는 것이 좋다.
엔티티 타입의 특징 | 값 타입의 특징 |
식별자O | 식별자X |
생명 주기 관리 | 생명 주기를 엔티티에 의존 |
공유 | 공유하지 않는 것이 안전(복사해서 사용) |
불변 객체로 만드는 것이 안전 |
값 타입은 정말 값 타입이라 판단될 때만 사용
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티
'JPA' 카테고리의 다른 글
JPQL(Java Persistence Query Language) (0) | 2022.11.06 |
---|---|
객체지향 쿼리 언어 (0) | 2022.11.06 |
값 타입의 비교 (0) | 2022.11.02 |
값 타입과 불변 객체 (0) | 2022.11.02 |
임베디드 타입(복합 값 타입) (0) | 2022.11.01 |