@Entity
public class Member {
...
private String name;
private int age;
...
}
String, int 가 값 타입에 해당
값 타입은 엔티티에 의존하기 때문에 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다.
9.2 임베디드 타입(복합 값 타입)
JPA 에서는 새로운 값 타입을 직접 정의해서 사용하는 경우 임베디드 타입이라 지칭함
@Entity
public class Member {
...
@Embedded Period workPeriod; // 근무 기간
@Embedded Address homeAddress; // 집 주소
...
}
@Embeddable
public class Period {
@Temporal(TemporalType.Date)
java.util.Date startDate;
@Temporal(TemporalType.Date)
java.util.Date endDate;
}
@Embeddable
public class Address {
@Column(name="city") //매핑할 컬럼 정의
private String city;
private String street;
private String zipcode;
...
}
@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
기본 생성자가 필수
임베디드 타입의 관계를 UML 로 표현시 컴포지션(composition) 관계
임베디드 타입과 테이블 매핑
임베디드 타입은 엔티티의 값일 뿐이기 때문에 엔티티의 테이블에 매핑된다
@AttributeOverride : 속성 재정의
@Entity
public class Member {
...
@Embedded
Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
@AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
@AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
})
Address companyAddress;
...
}
@AttributeOverrides 는 엔티티에 설정 (임베디드 타입이 임베디드 타입을 가지고 있어도)
임베디드 타입과 null
임베디드 타입이 null 이면 매핑한 컬럼 값은 모두 null 이 된다
9.3 값 타입과 불변 객체
값 타입 공유 참조
member1.setHomeAddress(new Address("OldCity");
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
같은 인스턴스를 참조하기 때문에 회원1 과 회원2 모두 값이 변경된다(각각 UPDATE SQL 을 실행함)
이 경우 값을 복사해서 사용하면 된다
값 타입 복사
member1.setHomeAddress(new Address("OldCity");
Address address = member1.getHomeAddress();
// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(address);
값을 복사해서 사용시 공유 참조로 인해 발생하는 부작용을 피할 수 있다
setter 를 제거하여 공유 참조를 해도 값을 변경하지 못하도록 해야한다
9.4 값 타입의 비교
자바가 제공하는 객체 비교 2가지
동일성(Identity) 비교: 인스턴스의 참조 값을 비교, == 사용
동등성(Equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
값 타입의 경우 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다.
따라서 동등성 비교(a.equals(b))를 사용해야 한다.
9.5 값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 사용
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
...
}
관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없음
따라서 별도의 테이블을 구성하고 @CollectionTable 을 사용하여 매핑해야 한다
값 타입 컬렉션 사용 - 조회
값 타입 컬렉션의 조회시 기본 페치 전략은 LAZY 이다.
//SQL: SELECT ID, CITY, STREET, ZIPCODE FROM MEMBER WHERE ID = 1
Member member = em.find(Member.class, 1L); //1. member
//2. member.homeAddress
Address homeAddress = member.getHomeAddress();
//3. member.favoriteFoods
Set<String> favoriteFoods = member.getFavoriteFoods(); //LAZY
//SQL: SELECT MEMBER_ID, FOOD_NAME FROM FAVORITE_FOODS WHERE MEMBER_ID = 1
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
//4. member.addressHistory
List<Address> addressHistory = member.getAddressHistory(); //LAZY
//SQL: SELECT MEMBER_ID, CITY, STREET, ZIPCODE FROM ADDRESS WHERE MEMBER_ID = 1
addressHistory.get(0);
member: 회원과 임베디드 값 타입인 homeAddress 를 함께 조회
member.homeAddress: 1번에서 회원 조회시 같이 조회
member.favoriteFoods: LAZY 로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL 1번 호출
member.addressHistory: LAZY 로 설정해서 실제 컬렉션을 사용할 때 SELECT SQL 1번 호출
값 타입 컬렉션 사용 - 수정
Member member = em.find(Member.class, 1L);
//1. 임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시1", "123456"));
//2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
//3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "기존 주소", "123-123"));
addressHistory.add(new Address("새로운도시", "새로운 주소", "123-456"));
임베디드 값 타입 수정: Address 는 Member 테이블의 필드중 일부. 따라서 Member 테이블이 업데이트 된다
기본값 타입 컬렉션 수정: 수정의 대상이 되는 기본값을 제거한 뒤 새로 추가한다
임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 추가한다(equals, hashcode 반드시 구현해야 함)
값 타입 컬렉션 제약사항
식별자가 없는 값 타입의 경우 값이 변경되면 DB 에서 원본 데이터를 찾기가 어렵다.
이런 문제로 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면,
값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 DB 에 다시 저장한다
DELETE FROM ADDRESS WHERE MEMBER_ID=100
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100, ... )
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100, ... )
값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해야 한다
일대다 관계에 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
Address address;
}
값 타입?
9.1 기본값 타입
String
,int
가 값 타입에 해당9.2 임베디드 타입(복합 값 타입)
JPA 에서는 새로운 값 타입을 직접 정의해서 사용하는 경우 임베디드 타입이라 지칭함
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시임베디드 타입과 테이블 매핑
@AttributeOverride : 속성 재정의
임베디드 타입과 null
9.3 값 타입과 불변 객체
값 타입 공유 참조
값 타입 복사
9.4 값 타입의 비교
==
사용equals()
사용a.equals(b)
)를 사용해야 한다.9.5 값 타입 컬렉션
@ElementCollection
,@CollectionTable
사용@CollectionTable
을 사용하여 매핑해야 한다값 타입 컬렉션 사용 - 조회
값 타입 컬렉션 사용 - 수정
Address
는 Member 테이블의 필드중 일부. 따라서 Member 테이블이 업데이트 된다equals
,hashcode
반드시 구현해야 함)값 타입 컬렉션 제약사항
9.6 정리
엔티티 타입 vs 값 타입
엔티티 타입
값 타입
값 타입은 값 타입이라 판단될 때만 사용해야 한다. 식별자가 필요하고 지속해서 값을 추적하고 구분하고 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.
식별자 (id) 값이 없다. 값 타입일까?