반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'자바 ORM 표준 JPA 프로그래밍 - 기본편'
Intro
JPA의 데이터 타입을 분류하면 엔티티 타입과 값 타입으로 구분할 수 있다.
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자를 통해 지속해서 추적할 수 있다.
- 예) 회원 엔티티의 키나 나이 등의 값을 변경해도 식별자로 인식 가능
값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 숫자나 문자 같은 속성만 있어 변경 시 추적이 불가능하다.
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
값 타입은 다음 3가지로 나눌 수 있다.
- 기본값 타입 : 자바 기본 타입, 래퍼 클래스, String
- 자바에서 제공하는 기본 데이터 타입
- 임베디드 타입(Embedded type, 복합 값 타입) : 좌표, 포지션 등
- JPA에서 사용자가 직접 정의한 값 타입
- 컬렉션 값 타입(Collection value type) : 기본값 타입이나 임베디드 타입을 저장할 수 있는 것
- 하나 이상의 값 타입을 저장할 때 사용
값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고, @ElementCollection, @CollectionTable 어노테이션을 사용하여 값 타입 컬렉션을 구현할 수 있다.
- 데이터베이스는 컬렉션과 같은 데이터를 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
값 타입 컬렉션 사용
위 이미지와 같이 데이터베이스 테이블을 만들기 위해 값 타입 컬렉션을 정의한 코드이다.
// Member 클래스
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@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<>();
...
}
// Address 클래스
@Embeddable
public class Address {
@Column(name = "city")
private String city;
private String street;
private String zipcode;
...
}
- favoriteFoods와 addressHistory에 @ElementCollection 어노테이션을 지정한다.
- favoriteFoods는 String 타입의 Set 컬렉션
- 관계형 데이터베이스는 컬렉션을 포함할 수 없음
- @CollectionTable 어노테이션을 통해 추가할 테이블을 매핑해야 한다.
- joinColumns 속성을 통해 MEMBER_ID와 조인할 수 있도록 한다.
- 값으로 사용되는 컬럼이 하나면 @Column을 통해 컬럼명을 지정할 수 있다.
- addressHistoy는 Address 타입의 List 컬렉션
- @CollectionTable 어노테이션을 통해 별도의 테이블을 매핑해야 한다.
- joinColumns 속성을 통해 MEMBER_ID와 조인할 수 있도록 한다.
값 타입 컬렉션 저장
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("보쌈");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
- em.persist(member) 하나만 실행해도, FAVORITE_FOOD와 ADDRESS 테이블에 값이 저장된다.
- 값 타입 컬렉션들은 다른 테이블임에도 불구하고 생명주기가 Member 엔티티에 속하게 된다.
- em.persist(member) 한 번 호출에 7번의 INSERT SQL이 실행된다.
- member를 저장하기 위한 INSERT SQL 1번
- homeAddress는 컬렉션이 아닌 임베디드 값 타입이므로 member를 저장하기 위한 SQL에 포함
- favoriteFoods를 저장하기 위한 INSERT SQL 4번
- addressHistory를 저장하기 위한 INSERT SQL 2번
값 타입 컬렉션 조회
값 타입 컬렉션은 조회할 때 페치 전략을 선택할 수 있다.(기본 값 LAZY)
System.out.println("===============START===============");
Member findMember = em.find(Member.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);
}
출력
===============START===============
Hibernate:
select
member0_.MEMBER_ID as member_i1_7_0_,
member0_.city as city2_7_0_,
member0_.street as street3_7_0_,
member0_.zipcode as zipcode4_7_0_,
member0_.USERNAME as username5_7_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_4_0_,
favoritefo0_.FOOD_NAME as food_nam2_4_0_
from
FAVORITE_FOOD favoritefo0_
where
favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 보쌈
favoriteFood = 치킨
favoriteFood = 피자
- 값 타입 컬렉션의 조회는 기본이 지연 로딩(LAZY) 전략이다.
- em.find(Member.class, member.getId()); 실행 시 값 타입 컬렉션을 바로 조회하지 않는다.
- addressHistory 조회 시 해당 값 타입 컬렉션 테이블 조회 SQL 발생
- favoriteFoods 조회 시 해당 값 타입 컬렉션 테이블 조회 SQL 발생
값 타입 컬렉션 수정
값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
Member findMember = em.find(Member.class, member.getId());
// 임베디드 값 타입 수정
Address old = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", old.getStreet(), old.getZipcode()));
// 기본 값 타입 컬렉션 수정
Set<String> favoriteFoods = findMember.getFavoriteFoods();
favoriteFoods.remove("치킨");
favoriteFoods.add("김치볶음밥");
// 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = findMember.getAddressHistory();
addressHistory.remove(new Address("old1", "street", "10000"));
addressHistory.add(new Address("newCity1", "street", "10000"));
임베디드 값 타입 수정
- homAddress는 임베디드 값 타입으로 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다.
- Member 엔티티 수정하는 것과 동일하다.
기본 값 타입 컬렉션 수정
- 기본 값 타입 컬렉션을 수정하기 위해서는 수정할 값을 제거하고, 새로운 값을 추가해야 한다.
- 치킨에서 김치볶음밥으로 수정하기 위해 치킨을 제거하고 김치볶음밥을 추가했다.
임베디드 값 타입 컬렉션 수정
- 값 타입은 불변해야 하기 때문에 기존 주소를 삭제하고 새로운 주소를 등록한다.
- 임베디드 값 타입 컬렉션을 수정하기 위해서는 equals() 메서드 재정의와 hashCode() 메서드를 구현해야 한다.
값 타입 컬렉션의 제약사항
값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 값을 변경하게 되면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 위 코드에서는 마치 수정할 값만 제거하고 추가한 것처럼 보이나 실제 SQL 실행은 다르게 나타난다.
- 값 타입 컬렉션 테이블에 존재하는 모든 데이터를 제거한 뒤, 값 타입 컬렉션에 남아있는 현재 값을 다시 저장하는 구조
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다.
- null 입력 X, 중복 저장 X
값 타입 컬렉션 대안
값 타입 컬렉션의 변경 사항에 대한 제약사항 때문에 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
- 일대다 관계를 위한 엔티티를 만들고, 해당 엔티티에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용하여 값 타입 컬렉션처럼 사용
Address 엔티티 생성
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Address address;
...
}
일대다 매핑
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@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<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
}
- 값 타입 컬렉션은 수정할 필요가 없는 정말 단순한 경우에만 사용하는 것이 좋다.
반응형
'프레임워크(Framework) > JPA' 카테고리의 다른 글
[JPA] JPQL이란? - 객체지향 쿼리 언어 JPQL (1) (0) | 2022.12.13 |
---|---|
[JPA] 엔티티(Entity) 설계 시 주의사항 (1) | 2022.12.12 |
[JPA] 값 타입의 비교 - 값 타입 (3) (0) | 2022.12.10 |
[JPA] 값 타입과 불변 객체 - 값 타입 (2) (0) | 2022.12.09 |
[JPA] 기본값 타입과 임베디드 타입 - 값 타입 (1) (0) | 2022.12.08 |