@Entity
로 정의하는 객체
데이터가 변해도 식별자로 지속해서 추적 가능
int
, Integer
, String
같은 단순히 값으로 사용하는 자바 기본 타입이나 객체
식별자가 없고 값만 있으므로 변경시 추적 불가
자바 기본 타입(int, double)
래퍼 클래스(Integer, Long), String
공통된 특성을 묶어낼 수 있는 것(기간, 주소 등)
@Embeddable
: 값 타입 정의하는 곳에 표시
@Embedded
: 값 타입 사용하는 곳에 표시
기본 생성자 필수
임베디드 타입을 사용하지 않은 경우
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
private String city;
private String street;
private String zipcode;
}
city, street, zipcode는 주소와 관련된 값이므로 임베디드 타입을 사용해 연관된 값을 하나로 묶어줄 수 있다.
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// Getter, Setter, Constructor(No, All)
...
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@Embedded
private Address homeAddress;
// Getter, Setter
...
}
이렇게 Address 클래스를 만들어 관련된 값을 묶어서 @Embeddable
을 사용하고, 이 타입을 사용하는 곳에선 @Embedded
를 사용해서 Address 클래스를 사용한다.
✏️ 임베디드 타입은 정말 말 그대로 단순히 "값"들을 하나로 묶은 것이라 엔티티와 달리 식별자가 없고, 생명주기를 자신을 소유하는 엔티티에 의존한다. 그렇기에 재사용성이 좋고 높은 응집도를 가진다.
하나의 엔티티 에서 같은 임베디드 타입을 여러개 사용해야한다면?
아래와 같이 하나의 값 타입을 여러개 사용할 때 단순히 변수 1개를 더 추가한다면 에러가 발생한다.
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
// 반복되는 컬럼있다는 메세지와 함께 에러 발생
Repeated column in mapping for entity: hellojpa.Member column:
city (should be mapped with insert="false" update="false")
그래서 @AttributueOverrides
와 @AttributeOverride
를 사용해 컬럼을 재정의해서 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@Embedded
private Address homeAddress;
@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;
// Getter, Setter
...
}
특징
값 타입 : 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념
따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다
실제 인스턴스인 값을 공유하는 것은 위험해서 복사해서 사용해야 한다
Address address = new Address("city", "street","zipcode");
Member member1 = new Member();
member1.setUsername("111");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("222");
member2.setHomeAddress(address);
em.persist(member2);
member1.getHomeAddress().setCity("busan");
tx.commit();
System.out.println("member1 : " + member1.getHomeAddress().getCity());
System.out.println("member2 : " + member2.getHomeAddress().getCity());
-----------------------
// 출력값
member1 : busan
member2 : busan
}
이렇게 임베디드 타입같은 값 타입을 여러 엔티티에서 공유하면 side Effect로 인해 부작용이 발생할 수 있다.
해결하려면?
Adrress의 Setter를 구현하지 않거나 private로 구현하면 된다.
인스턴스 달라도 값이 같으면 같은 것으로 봐야 함
int a = 2;
int b = 2;
System.out.println("a == b : "+ (a==b));
Address address1 = new Address("busan", "namgu", "zipcode");
Address address2 = new Address("busan", "namgu", "zipcode");
System.out.println("address1 == address2 : "+(address1==address2));
System.out.println("address1 equals address2 : "+(address1.equals(address2)));
--------------------------------------
// 출력
a == b : true
address1 == address2 : false
address1 equals address2 : true
동일성(identity) 비교 : 인스턴스 참조 값을 비교( == 사용, private 타입)
동등성(equivalence) 비교 : 인스턴스 값을 비교( equals() 사용, embedded 타입 등)
- 값 타입의 equals() 메소드를 적절하게 재정의해서 사용(왠만하면 자동으로 해주는 거 써라!)
- 💡 재정의 하지않고 equals()를 써서 비교하면 fasle가 나온다. 그 이유는 기본이 == 비교로 되어있기 때문이다.
- hasCode()도 같이 정의해줘야됨. 그래야 자바 Collection에서 효율적으로 사용가능
값 타입을 컬렉션에 담아 쓰는 것
값 타입을 하나 이상 저장할 때 사용
@ElementCollection
, @CollectionTable
어노테이션을 붙여 값 타입 컬렉션 사용 가능
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
// 값이 하나고 내가 정의한 것이 아니기 때문에 예외적으로 컬럼명 변경 허용
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressesHistory = new ArrayList<>();
public String getUsername() {
return username;
}
public Set<String> getFavoriteFoods() {
return favoriteFoods;
}
public List<Address> getAddressesHistory() {
return addressesHistory;
}
}
RDB에는 내부적으로 컬렉션을 담을 수 있는 구조가 없다. 그냥 값만 넣을 수 있는 구조이다
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("busan", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("헤이에그누들");
member.getFavoriteFoods().add("볶음짬뽕");
member.getAddressHistory().add(new Address("seoul", "street1", "10001"));
member.getAddressHistory().add(new Address("incheon", "street2", "10002"));
em.persist(member);
tx.commit();
Member만 저장했는데 값 타입 컬렉션에 대한 insert 쿼리가 날아갔다.
즉, 값 타입 컬렉션은 Member 객체의 라이프 사이클과 동일하게 적용된다.
값 타입은 별도로 persist나 update 할 필요 없이 Member에서 값을 변경하면 자동으로 처리해준다.
그렇기에 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
....
em.flush();
em.clear();
System.out.println("============ START ============");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
영속성 컨텍스트를 비운 후 Member 조회하면 컬렉션들은 같이 조회되지 않는다.
그 이유는 컬렉션 값 타입들은 지연 로딩 전략을 취하기 때문이다.
💡 @ElementCollection
의 fetch 기본값이 LAZY이다.
수정은 Member 객체에서 getter로 불러와서 remove 후 add 해주면 된다.
Hibernate:
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
@OrderColumn(name = "address_history_order)
를 사용해 update 쿼리 날아갈 수 있도록 가능(의도대로 동작하지 않을 때가 많음)@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
참고
임베디드 타입
정리가 잘 된 글이네요. 도움이 됐습니다.