1. 기본개념
@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) 저장
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) 조회
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();
3. 엔티티 vs 값 타입 특징 비교
📌 <엔티티>
📌 <값 타입>
값 타입은 정말 단순한 경우에만 사용!!!
식별자가 필요하고, 지속해서 값을 변경 or 추적해야한다면 엔티티!!!