JPA에서 데이터 타입은 엔티티 타입과 값 타입으로 분류할 수 있다.
여기서 값 타입은 단순히 값으로 사용하는 자바의 프리미티브 타입이나 객체를 말한다.
값 타입의 분류
- 기본값 타입 : 자바 프리미티브 타입, 래퍼 클래스(Integer, Long 등), String
- 임베디드 타입(복합 값 타입)
- 값 타입 컬렉션 : 값 타입을 여러 개 저장하고자 할 때 사용하며, 자바의 컬렉션을 사용한다.
값 타입 컬렉션
@ElementCollection, @CollectionTable 애노테이션을 붙여 값 타입 컬렉션을 사용할 수 있다.
//임베디드 타입
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
//기본적으로 컬렉션들은 값의 비교를 eqauls 메서드를 사용하기 때문에 재정의 해준다.
@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);
}
}
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String name;
@ElementCollection
@CollectionTable(
name = "foods",
joinColumns = @JoinColumn(name = "person_id")
)
@Column(name = "food_name") //값이 하나고 내가 정의한 것이 아니기 때문에 예외적으로 컬럼명 변경 허용
Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(
name = "address",
joinColumns = @JoinColumn(name = "person_id")
)
List<Address> addressList = new ArrayList<>();
public Person(String name) {
this.name = name;
}
public Set<String> getFavoriteFoods() {
return favoriteFoods;
}
public List<Address> getAddressList() {
return addressList;
}
}
문자열 String 타입과 임베디드 타입인 Address 타입을 컬렉션에 저장하는 예제이다.
위 코드를 보면 @CollectionTable 애노테이션을 사용하여 테이블의 이름과 외래 키를 지정해 주는 것을 볼 수 있다.
데이터베이스는 컬렉션을 저장할 수 없기 때문에 별도의 테이블을 만들어 일대다 관계로 풀어서 컬렉션을 저장해야 한다. 그렇기 때문에 별도로 만들어질 테이블의 이름과 외래 키를 지정해준다.
위 코드를 실행해보면 Hibernate도 다음과 같이 테이블을 만들어준다.
Hibernate:
create table address (
person_id bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
create table foods (
person_id bigint not null,
food_name varchar(255)
)
Hibernate:
create table Person (
id bigint not null,
name varchar(255),
primary key (id)
)
값타입 컬렉션도 결국은 값 타입이기 때문에 따로 생명주기를 가지지 않고, 엔티티와 같은 생명주기를 따라간다. 그렇기 때문에 값을 저장하거나 삭제하고자 할 때 일반적으로 컬렉션을 사용하는 것과 마찬가지로 단순히 값을 추가하거나 삭제하기만 하면 데이터베이스에 반영이 된다.
- 일대다 관계에서 cascade = ALL, orphanRemoval = true를 설정한 것과 같다고 볼 수 있다.
Person person = new Person("kim");
entityManager.persist(person);
//=====저장 과정=====
person.getAddressList().add(new Address("city1", "street1", "1000"));
person.getAddressList().add(new Address("city2", "street2", "1001"));
person.getAddressList().add(new Address("city3", "street3", "1002"));
//======삭제 과정=====
entityManager.flush(); //데이터베이스에 반영
entityManager.clear(); //영속성 컨텍스트 초기화
Pesron findPerson = entityManager.find(Person.class, person.getId());
findPerson.getAddressList().remove(new Address("city1", "street1", "1000"));
테이블을 보면 저장,삭제 모두 정상적으로 실행된 것을 알 수 있다.
하지만 삭제 과정에서 실행된 쿼리를 보면 생각지 못한 쿼리가 나가는 것을 볼 수 있다.
Hibernate:
/* delete collection hello.Person.addressList */
delete from address where person_id=?
Hibernate:
/* insert collection row hello.Person.addressList */
insert into address(person_id, city, street, zipcode) values (?, ?, ?, ?)
Hibernate:
/* insert collection row hello.Person.addressList */
insert into address (person_id, city, street, zipcode) values (?, ?, ?, ?)
단순히 삭제하고자 하는 컬럼만 삭제하는 것이 아닌 관련된 컬럼을 모두 삭제하고, 남아 있는 컬럼을 다시 저장하는 것을 볼 수 있다.
값 타입 컬렉션의 제약
- 값 타입은 엔티티와 다르게 식별자 개념이 없기 때문에 값을 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항(저장, 삭제)이 발생하면, 소유하는 엔티티와 연관된 모든 데이터를 삭제하고, 현재 남아있는 값을 모두 다시 저장한다.(예제에서는 삭제를 예로 들었지만, 저장도 마찬가지)
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함 → null 입력 X, 중복 저장 X
값 타입 컬렉션의 대안
- 값 타입 컬랙션 대신에 일대다 관계를 고려하자.
- 일대다 엔티티를 만들고, 엔티티에서 값 타입을 사용.
- cascade와 고아 객체 제거를 설정해서 값 타입 컬렉션처럼 사용.
@Entity
@Table(name = "address")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
protected AddressEntity() { }
public AddressEntity(Address address) {
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
public class Person {
//....생략
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "member_id")
private List<AddressEntity> addressList = new ArrayList<>();
//....생략
}
값 타입은 정말 값 타입이라 판단되고, 정말 단순할 때 사용하는 것이 좋다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 값 타입이 아닌 엔티티로 사용해야 한다.
참고
'데이터베이스 > JPA' 카테고리의 다른 글
[JPA] Cascade(영속성 전이), OrphanRemoval(고아객체 제거) (0) | 2021.03.17 |
---|---|
[JPA] @MappedSuperClass (0) | 2021.03.13 |
JPA JPQL 페치 조인(fetch join) (0) | 2021.02.21 |
JPA 임베디드 타입( Embedded Type, 복합 값 타입) : @Embedded, @Embeddable, @AttributeOverride, @AttributeOverrides (0) | 2021.02.20 |
JPA 지연로딩을 사용해야하는 이유, 지연로딩(Lazy)과 즉시로딩(Eager) (0) | 2021.02.12 |
댓글