자바 ORM 표준 JPA 프로그래밍 공부 기록
예) String name
, int age
생명주기를 엔티티에 의존한다.
따라서 엔티티를 제거하면 name
, age
도 제거된다.
값 타입은 공유하면 안 된다.
기본 타입은 항상 값을 복사함.
Integer
같은 래퍼 클래스나 String
같은 특수한 클래스에 경우 객체지만 자바 에서 기본 타입처럼 사용할 수 있게 지원. 이들은 객체지만 변경 X
새로운 값 타입을 직접 정의해서 사용하는 것
=> 직접 정의한 임베디드 타입도 값 타입이다
@Embedded
: 값 타입을 사용하는 곳 @Embeddable
: 값 타입을 정의하는 곳@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String userName;
@Embedded
private Period workPriod;
@Embedded
private Address homeAddress;
...
@Embeddable
public class Address {
//집 주소 표현
private String city;
private String street;
private String zipcode;
...
}
@Embeddable
public class Period {
//근무기간
private LocalDate startDate;
private LocalDate endDate;
...
@AttributeOverride
: 속성 재정의임베디드 타입에 정의한 매핑 정보를 재정의하려면 @AttributeOverride
를 사용하면 된다. 예를 들어 Member에게 주소가 2개 필요하다면
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
private Address companyAddress;
...
위 상황에서 문제는 테이블에 매핑하는 컬럼명이 중복되는 것이다. (각 Address 안의 컬럼 명이 동일)
이 때는 @AttributeOverride
을 사용해서 매핑 정보를 재정의한다.
@Entity
public class Member {
...
@Embedded
private 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"))
})
private Address companyAddress;
...
임베디드 타입은 여러 엔티티에서 공유하면 위험하다
Address address = new Address("TEST","TEST","TEST");
Member m1 = new Member();
m1.setUserName("TEST1");
m1.setHomeAddress(address);
em.persist(m1);
Member m2 = new Member();
m2.setUserName("TEST2");
m2.setHomeAddress(address);
em.persist(m2);
m1.getHomeAddress().setCity("NewCity");
member1의 주소만 "NewCity"로 변경되길 기대했지만 멤버2의 주소도 같이 변경되었다. 둘이 같은 Address를 참조하기 때문이다. 이러한 공유 참조로 인해 발생하는 버그는 찾기 어렵다. 이런 부작용을 막기 위해서는 값을 복사해서 사용하면 된다.
Address address = new Address("TEST","TEST","TEST");
Member m1 = new Member();
m1.setUserName("TEST1");
m1.setHomeAddress(address);
em.persist(m1);
Address newAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member m2 = new Member();
m2.setUserName("TEST2");
m2.setHomeAddress(newAddress);
em.persist(m2);
m1.getHomeAddress().setCity("NewCity");
기존 address
를 복사하여 newAddress
를 새로 만들어서 위 과정을 반복하면 의도한대로 동작한다.
객체 대입 시 마다 인스턴스를 복사해서 대입하면 공유 참조
를 피할 수 있다. 그러나 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 수 없다. 자바는 그저 기본타입이면 값을 복사해서 넘기고 객체면 참조를 넘긴다.
객체의 공유 참조
는 피할수가 없다.
=> setter
사용을 하지 말아야 한다.
객체를 불변하게 만들면 부작용을 차단할 수 있다.
값 타입은 가능하면 불변 객체로 설계한다.
=> 생성자만 값을 설정하고 setter
생성 X
• 동일성 비교: 인스턴스의 참조 값을 비교, ==
• 동등성 비교: 인스턴스의 값을 비교, equals()
값 타입은 인스턴스가 달라도 그 안이 같으면 같은 것으로 봐야한다. 따라서 값 타입을 비교할 때는 equals()
를 사용해서 동등 비교를 해야한다.
@Embeddable
public class Address {
//집 주소 표현
private String city;
private String street;
private String 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
어노테이션을 사용영속성 전이(Cascade)
+ 고아 객체 제거
기능을 필수로 가진다고 볼 수 있다LAZY
가 기본이다.Member m = new Member();
m.setUserName("TEST");
m.setHomeAddress(new Address("TEST","TE","ST"));
m.getFavoriteFoods().add("Chicken");
m.getFavoriteFoods().add("Pizza");
m.getAddressHistory().add(new Address("TEST2","TE2","ST2"));
m.getAddressHistory().add(new Address("TEST3","TE3","ST3"));
em.persist(m);
em.flush();
em.clear();
System.out.println("=======================================");
Member findMember = em.find(Member.class, m.getId());
출력
=======================================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_6_0_,
member0_.city as city2_6_0_,
member0_.street as street3_6_0_,
member0_.zipcode as zipcode4_6_0_,
member0_.userName as userName5_6_0_
from
Member member0_
where
member0_.MEMBER_ID=?
지연 로딩이 적용된 것을 확인할 수 있다.
System.out.println("=======================================");
Member findMember = em.find(Member.class, m.getId());
System.out.println("=======================================");
List<Address> addressHistory = findMember.getAddressHistory();
for(Address address : addressHistory){
System.out.println(address.getCity());
}
System.out.println("=======================================");
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for(String food : favoriteFoods){
System.out.println(food);
}
출력 확인
=======================================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_6_0_,
member0_.city as city2_6_0_,
member0_.street as street3_6_0_,
member0_.zipcode as zipcode4_6_0_,
member0_.userName as userName5_6_0_
from
Member member0_
where
member0_.MEMBER_ID=?
=======================================
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=?
TEST2
TEST3
=======================================
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_
from
FAVORITE_FOODS favoritefo0_
where
favoritefo0_.MEMBER_ID=?
Pizza
Chicken
위와 같이 실제 컬렉션을 사용할 때 SELECT SQL을 한번 씩 호출하는 것을 볼 수 있다.
값 타입은 엔티티와 다르게 식별자라는 개념이 없으므로 변경하면 추적이 안된다.
따라서 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
===================== homeAddress
//값타입은 완전히 교체
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
Hibernate:
/* update
com.jpa.db.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
userName=?
where
MEMBER_ID=?
===================== favoriteFoods
//치킨=>한식
Set<String> favFoods = findMember.getFavoriteFoods();
findMember.getFavoriteFoods().remove("Chicken");
findMember.getFavoriteFoods().add("Korean Food");
Hibernate:
/* delete collection row com.jpa.db.Member.favoriteFoods */ delete
from
FAVORITE_FOODS
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row com.jpa.db.Member.favoriteFoods */ insert
into
FAVORITE_FOODS
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
===================== addressHistory
findMember.getAddressHistory().remove(new Address("TEST2","TE2","ST2"));
//equals를 제대로 구현해야 지워진다.
findMember.getAddressHistory().add(new Address("newCity2","TE2","ST2"));
Hibernate:
/* delete collection com.jpa.db.Member.addressHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row com.jpa.db.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row com.jpa.db.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
// 2번 인서트 ??
=> 현재 값 타입 컬렉션에 데이터가 2개 있어서 2번 INSERT 되었다.
=> 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
❓❓ 근데 왜 Set<String>
의 경우에는 1번 delete하고 1번 insert가 될까? Set이라서?
List<String>
으로 변경하면 1번 delete하고 2번 insert가 일어남.
그리고 List<Address>
를 Set<Address>
로 변경할 경우에는 또 1번 delete했다가 2번 insert가 일어난다.. 아무리 찾아봐도 모르겠다
아무튼 값 타입 컬렉션에 데이터가 많다면 성능에 문제가 생기므로 실무에서는 사용하면 ... 안 된다.
값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
em.persist
로 영속화 한다em.remove
로 제거한다