연관관계 매핑시 고려사항 3가지
• 다중성
• 단방향, 양방향
• 연관관계의 주인
연관관계의 종류
• 다대일: @ManyToOne
• 일대다: @OneToMany
• 일대일: @OneToOne
• 다대다: @ManyToMany (실무에서는 사용x)
@ManyToOne < ---------> @OneToMany(Mappedby)
@JoinColumn
일대다 단방향 매핑의 단점
일대다 양방향 정리
일대일 관계는 그 반대도 일대일
주 테이블이나 대상 테이블 중에 외래 키 선택 가능
외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
일대일: 주 테이블에 외래 키 단방향 정리
다대일(@ManyToOne) 단방향 매핑과 유사
다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
반대편은 mappedBy 적용
주 테이블에 외래 키
대상 테이블에 외래 키
다대다 관계는 그렇게 사용하지 말고, 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
중간의 Member_Product 엔티티를 만들어 풀어내면 된다.
3가지 방법
1. 각각 테이블로 변환 -> 조인 전략
2. 통합 테이블로 변환 -> 단일 테이블 전략
3. 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
3가지 방법은 테이블은 다르지만, 객체는 전부 하나의 형태이다. JPA는 어떤 방법으로 구현하든 전부 지원을 한다.
JPA의 기본은 단일 테이블 전략이다.
@Entity
@Inheritance(starategy=InheritanceType.JOINED)
public class Item {
@Id @GeneratedValue
private Long id;
private Long name;
private int price
}
@Entity
public class Book extends Item {
private String ISBN;
private String author
}
@Entity
public class Movie extends Item {
private String actor;
private String direcotr;
}
@Entity
public class Album extends Item {
private String artist;
}
장점
단점
JPA 스펙상은 DTYPE을 넣어야 된다고 되있지만, 하이버네이트가 그것이 필수적이지 않게 만들어 준다..
장점
단점
단일 테이블 전략은 DTYPE이 필수다. 아니면 Album, movie, book을 구별을 못한다.
ITEM 클래스를 추상 클래스로 바꿔줘야 한다. 또한 DiscriminatorColumn이 의미가 없어 사용할 필요 없다.
장점
단점
이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천x
생성날짜, 수정날짜, 생성인, 수정인 등 공통적인 정보의 클래스
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
실제 클래스를 상속 받아서 만들어짐.
실제 클래스와 겉 모양이 같다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨
프록시 특징
프록시 객체는 처음 사용할 때 한 번만 초기화
프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 도 실제 엔티티 반환(한 트랜잭션 안에서 JPA는 항상 같음을 보장한다.)
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
clear, detach, close 등 영속성 컨텍스트를 비워버리거나 하면 예외..
프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
프록시 클래스 확인 방법
entity.getClass().getName()
출력(..javasist.. or HibernateProxy…)
프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
참고: JPA 표준은 강제 초기화 없음
강제 호출: member.getName()
정리
사실 위에서 본 내용들을 거의 사용할 일은 없다. em.getReference()를 사용할까? 아니다. 그렇다면 왜 이런 것을 알아 보았는가? 바로 즉시로딩과 지연로딩 때문이다. 지금부터 알아보자.
//지연로딩 FetchType.LAZY
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
//즉시 로딩 FetchType.EAGER
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
LAZY 로딩을 사용시
Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)
즉, 처음 쿼리문을 날릴때, join 쿼리를 날리지 않음.
EAGER 로딩을 사용시
Team team = member.getTeam();
team.getName(); // 별다른 DB 조회 없이 조회됨.
처음 find를 할때 join문을 함께 날려서 조회 됨.
즉시로딩의 주의점
N + 1 문제란?
Member를 조회했는데 거기에 딸린 Team을 같이 조회하는 쿼리문을 날린다. 만약 다른 연관된 테이블들이 더 있다면 그 개수 만큼(N개) 쿼리가 더 나갈 것이다.
처음 쿼리 하나(1)을 날렸는데 거기에 따른 N개의 쿼리가 나간다고 N+1문제이다. (1+N이어야 되는거같긴한데..)
3가지 해결점
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
종류
ALL : 모두 적용
PERSIST : 영속
REMOVE : 삭제
MERGE : 병합
REFRESH : REFREHS
DETACH : DETACH
CASCADE의 경우 소유관계가 하나일때 사용해도 되지만, 그렇지 않다면 사용하지마라. (단일 엔티티에 완전히 종속적일때)
고아 객체 제거 - 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
orphanRemoval = true
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
CascadeType.ALL + orphanRemovel=true
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
JPA의 데이터 타입 분류
값 타입 분류
public class Hotel {
...
private String address1;
private String address2;
private String zipCode;
}
위 3개의 필드를 묶어서 하나의 클래스로
@Embeddable
public class Address {
private String address1;
private String address2;
private String zipCode;
}
그럼 호텔은 Address만 받으면 됨.
public class Hotel {
@Embedded
private Address address;
}
@AttributeOverrdie
로 설정 재정의를하면 사용 가능.```
@AttributeOverrides({
@AttributeOverride(name="address1", column=@Column(name="waddr1")),
@AttributeOverride(name="address2", column=@Column(name="waddr2")),
@AttributeOverride(name="zipcode", column=@Column(name="wzipcode")),
})
```
@Embeddable
은 왜 사용하는가? @Embeddable을 사용하면 모델을 더 잘 표현할 수 있음.
개별 속성을 모아서 이해 -> 타입으로 더 쉽게 이해 가능
addr1, addr2, zipcode를 보고 주소로 이해 -> Address 타입을 보고 주소로 바로 이해
불변 객체
동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
동등성(equivalence) 비교: 인스턴스의 값을 비교, equals()사용
값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)
테이블은 컬랙션을 저장할 수 없다. 객체로는 하나로 표현되는 것이 테이블은 다르게 표현된다.
값 타입을 하나 이상 저장할 때 사용
@ElementCollection, @CollectionTable 사용
데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
컬렉션을 저장하기 위한 별도의 테이블이 필요함
위 그림을 나타낸 코드
@ElementCollection
@CollectionTable(name ="FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
@Column(name="FOOD_NAME") //값타입이 하나(String), 그래서 컬럼 명을 바꿀 수 있음.
priavte Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name ="ADDRESS" joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
priavte List<Address> addressHistory = new ArrayList<>();
컬랙션 사용
값 타입 조회 시, (값 타입 컬렉션도 지연 로딩 전략 사용, @ElementCollection에 전략이 Lazy로...)
값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 수정 -> 수정시에는 그냥 새 객체를 넣어줘야됨. (이는 불변과 관련됨). setter를 열어두면 큰 문제가 생길 수 있음.
컬렉션 수정 -> Food의 값을 변경 하고싶을때, remove하고 add를 하자.
Member findMember = em.find(Member.class, member.getId());
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
값 타입 컬렉션 대신에 일대다 관계를 사용하는 것을 고려해라. 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬
렉션 처럼 사용