JPA는 데이터 타입을 엔티티 타입과 값 타입으로 분류한다.
엔티티 타입은 @Entity로 정의한 객체
엔티티의 속성이 변경되면 식별자(id)로 인식 가능함 ==> 데이터가 변경돼도 식별자로 추적 가능
값 타입은 int, Integer, String같은 기본타입
식별자가 없고 값만 있으므로 변경시 추적불가
값 타입의 분류
- 기본값타입
- 임베디드 타입
- 컬렉션 값 타입
기본값타입
자바 기본 타입(int, double), 래퍼 클래스(Integer,Long), String
생명주기를 엔티티에 의존함.
값 타입은 공유하면 안됨.(만약 회원1과 회원2가 속성으로 값타입을 공유하면 회원1이 속성을 변경했을 때 회원2도 같이 적용되겠지) (근데 래퍼클래스, String등은 변경이 불가능하기때문에 문제없음)
임베디드 타입
새로운 값 타입을 직접 정의할 수 있음
@Embeddable
@NoArgsConstructor
class Address{
private String city;
private String street;
private String zipcode;
public ...(){
//이 클래스에서 사용하는 메소드
}
}
재사용이 가능하고 응집도가 높다는 장점이 있다.(해당 값 타입만 사용하는 의미 있는 메소드를 만들어 사용할 수 있다)
@Entity
class Member{
...
@Embedded
Address address;
}
엔티티에는 @Embedded 어노테이션을 사용해서 값을 추가하면 된다.
임베디드 값은 내부에 엔티티를 가질 수 있다.
또, 하나의 엔티티 안에 같은 임베디드 타입을 여러개 가질 수도 있다.
@Entity
class Member{
...
@Embedded
@AttributeOverrides(
{
@AttributeOverride(name="city",column=@Column("home_city")),
@AttributeOverride(name="street",column=@Column("home_street")),
@AttributeOverride(name="zipcode",column=@Column("home_zipcode"))
}
)
Address homeAddress;
@Embedded
@AttributeOverrides(
{
@AttributeOverride(name="city",column=@Column("work_city")),
@AttributeOverride(name="street",column=@Column("work_street")),
@AttributeOverride(name="zipcode",column=@Column("work_zipcode"))
}
)
Address workAddress;
}
그런 경우 DB테이블에 컬럼명이 중복되니 @AttributeOverrides, @AttributeOverride를 이용해 위와 같이 설정해주어야 함.
값 타입 공유 참조
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
예를들어 회원1과 회원2가 address라는 임베디드 값 타입 하나를 공유한다고 하자.
두 회원은 하나의 값 타입을 공유하므로, 한 회원이 값을 변경하면 나머지 한 회원도 똑같이 값이 변경된다.
따라서 값 타입은 불변 객체로 설계해야한다.
생성자로만 값을 설정할 수 있게 하고, 수정자 setter를 만들지 않아야 한다(또는 setter를 private으로 사용)
만약 값을 수정해야하는 경우에는 완전히 새로운 임베디드 타입 값 객체를 생성하여 변경해야 한다.
member.setAddress(new Address("aaa","bbb","ccc")); //완전히 새로운 객체를 만들어 변경
값 타입의 비교
값 타입을 비교할 때, 인스턴스가 달라도 그 안에 있는 값이 같으면 같은 것으로 봐야한다.
Address a = new Address("1","2","3");
Address b = new Address("1","2","3");
System.out.println(a == b); //false
위 예시에서 a와 b는 서로 다른 인스턴스이므로 ==비교시에는 false가 된다.(동일성 비교)
값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 한다.(속성값을 비교)
(equals는 메소드 override해서 사용해야한다. 기본적인 equals는 ==비교이기 때문. 인텔리제이에서는 우클릭->generate->equals and hashcode를 선택하여 equals메소드를 자동으로 작성가능하다)
a.equals(b); //true
값 타입 컬렉션
값 타입을 컬렉션으로 가진다면
class Member{
...
private List<Address> addresses;
}
객체는 이렇게 작성할 수 있겠지만 DB테이블은 어떻게 해야할까?
값 타입에 대한 테이블을 별도로 가지고, 외래키를 이용해서 조인할 수 있게 해야한다.(일대다 개념과 유사)
class Member{
...
@ElementCollection
@CollectionTable(name="FAVORITE_FOOD", joinColumns = @JoinColumn(name="MEMBER_ID"))
@Column(name="FOOD_NAME")//예외적으로 콜렉션테이블에서 String컬럼명을 지정가능
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name="ADDRESS_HISTORY", joinColumns = @JoinColumn(name="MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
}
DB테이블이 따로 생겼지만, member만 persist하면 값 컬렉션도 자동으로 persist됨.
cascadeALL, orphanRemoval=true인것과 같음
또, 조회시 기본적으로 지연로딩이 적용된다.
값 수정시 마찬가지로 불변객체 원칙을 따라야 한다.
findMember.setAddress(new Addres(...));
findMember.getFavoriteFoods().remove("변경대상");
findMember.getFavoriteFoods().add("변경내용");
findMember.getAddressHistory().remove(new Address(...)); //equals가 정의되어있어야 제대로 작동함!
findMember.getAddressHistory().add(new Addres(...));
값 타입은 식별자 개념이 없으므로 추적이 어렵다.
따라서 변경사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
그래서 쿼리를 보면.. 변경대상 삭제, 변경후내용 추가 2번만 일어나는것이 아니라...
모든 내용을 삭제하고 변경후 남은값을 모두 인서트한다
따라서 아주 간단한 경우가 아니라면 값 타입 컬렉션 대신 일대다 관계를 사용하는것을 고려해야한다.
@Entity
class AddressEntity{
@Id @GeneratedValue
@Column(name="ADDRESS_ID")
private Long id;
private Address address;
}
@Entity
class Member{
...
@OneToMany(cascade=CASCADE.ALL,orphanRemoval=true)
List<AddressEntity> addressHistory = new ArrayList<>();
}
이렇게 엔티티클래스로 래핑해서 엔티티로 승격해 사용할 수 있다.
'웹개발 > JPA' 카테고리의 다른 글
JPQL (0) | 2023.07.22 |
---|---|
JPA가 지원하는 다양한 쿼리 방법 (0) | 2023.07.22 |
영속성 전이 CASCADE (0) | 2023.07.19 |
즉시 로딩과 지연 로딩 (0) | 2023.07.16 |
프록시 (0) | 2023.07.14 |
댓글