Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: hellojpa.Member column: city (should be mapped with insert="false" update="false")
이를 해결하기 위해 @AttributeOverride(s)를 사용한다.
따라서, 멤버 클래스에 다음과 같이 애너테이션 @AttributeOverrides을 추가한다.
그러면 상단의 homeAddress의 속성 city / street / zipcode는 이름 그대로 들어가고 workAddress는 아래에서 정한대로 column name이 들어간다.
@AttributeOverride의 name 안에는 Address에서 정의한 필드명이 그대로 들어가야한다.
컬렉션은 일대다라서 데이터베이스에는 하나의 테이블에 넣을 수 있는 방법이 없다. (그래서 별도 테이블 필요)
컬렉션을 저장하기 위한 별도의 테이블이 필요한다.
값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.
데이터베이스는 보통 value만 넣을 수 있다. 요즘에야 Json 지원하기도 한다.
컬렉션은 "일대다" 개념이다.
member의 주소 이력을 저장할 때, 별도의 테이블을 뽑아서 저장 가능하다.
테이블 FAVORITE_FOOD 의 기본 키: MEMBER_ID + FOOD_NAME
테이블 ADDRESS의 기본 키:MEMBER_ID + CITY + STREET + ZIPCODE 모두 묶어서 PK를 쓴다.
ADDRESS, FAVORITE_FOOD에 Long id를 기본 키로 넣으면?
값 타입이 아닌 엔티티가 되버린다.
Ex) 값 타입 저장
멤버
둘다 MEMBER_ID로 조인한다.
java
열기
@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의 fetch type이 LAZY로 기본 설정되어 있기 때문이다.
java
열기
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_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=?
address = old1
address = old2
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자
Ex) 값 타입 수정의 잘못된 예시
값 타입은 불변이어야한다.
java
열기
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
findMember.getHomeAddress().setCity("newCity");
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
그런데 값 타입은 부작용(side effect)이 생길 수 있어서 이런 식으로 수정하면 절대 안된다.
java
열기
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Ex) 값 타입 수정의 올바른 예시
Address 자체를 통으로 갈아 끼워야한다.
java
열기
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
값 타입은 추적도 안되니까 완전히 교체를 해줘야한다.
java
열기
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Ex) 값 타입 컬렉션 업데이트
java
열기
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
publicclassJpaMain{
publicstaticvoidmain(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "100-000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-000"));
member.getAddressHistory().add(new Address("old2", "street", "100-000"));
em.persist(member);
em.flush();
em.clear();
// DB에 데이터 있는 상태에서 조회한다.
System.out.println("================ START =================");
Member findMember = em.find(Member.class, member.getId());
// findMember.getHomeAddress().setCity("newCity");
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
컬렉션의 값만 변경해도 실제 데이터베이스 쿼리가 날아가고 JPA가 데이터를 바꿔준다.
MEMBER_ID, FOOD_NAME 찾아서 DELETE 한 다음에 데이터를 새로 집어 넣는다.
java
열기
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Hibernate:
/* delete collection row hellojpa.Member.favoriteFoods */ delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD(MEMBER_ID, FOOD_NAME)values(?, ?)
값 타입 컬렉션의 제약 사항
값 타입은 엔티티와 다르게 식별자 개념이 없다.
그래서 값을 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 현재 값을 모두 다시 저장한다.
값 타입 컬렉션을 매핑하는 테이블을 모든 컬럼을 묶어서 기본 키를 구성해야한다 => null 입력 X, 중복 저장
ex) addressHistory를 지우는 경우, 멤버 아이디와 관련한 정보를 DB에서 모두 지운다.
그 다음 컬렉션에 최종으로 남은 것들에 대해서만 INSERT 실행한다.
Ex) 주소 바꾸기 - 값 타입 컬렉션의 제약 사항
컬렉션은 대상을 찾을 때, 기본적으로 equals()를 쓰기 때문에 괄호 안에 완전히 같은 값을 넣어준다.
addressHistory를 DELETE 하는데 ADDRESS 테이블을 통으로 지운다. 멤버 아이디 기준으로
그러고나서 address 관련 INSERT가 두 번 날아간다.
old1을 remove() 하고나면 old2, newCity1은 남아 있을 것이다. 이 두 개를 테이블을 완전히 갈아끼운 것이다.
왜 INSERT 가 두 번 날아갈까?
java
열기
================ START =================
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.USERNAME as USERNAME5_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
select
favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.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=?
Hibernate:
/* update
hellojpa.Member */ update
Member
set
city=?,
street=?,
zipcode=?,
USERNAME=?
where
MEMBER_ID=?
Hibernate:
/* delete collection hellojpa.Member.addressHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row hellojpa.Member.addressHistory */insert
into
ADDRESS(MEMBER_ID, city, street, zipcode)values(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.Member.addressHistory */ insert
into
ADDRESS(MEMBER_ID, city, street, zipcode)values(?, ?, ?, ?)
Hibernate:
/* delete collection row hellojpa.Member.favoriteFoods */ delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert collection
row hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD(MEMBER_ID, FOOD_NAME)values(?, ?)
cf) addressHistory 에 다음 애너테이션 OrderColumn을 넣는 것도 위험하다.