association의 대상이 되는 Entity(class)를 명시적으로 지정할 수 있다.
따로 설정하지 않으면, 기본적으로 필드의 타입값으로 설정된다.
@ManyToOne(targetEntity = Car.class)
@JoinColumn(name = "user_id", nullable = false)
private User user;
필드의 타입은 User
인데, targetEntity로 지정한 타입은 Car
이므로, 컴파일 타임에 어떤 Table(Class, Entity)를 reference하는지 알 수 없어서 예외가 발생한다.
org.hibernate.sql.ast.tree.from.UnknownTableReferenceException: Unable to determine TableReference (`car`) for `autobid.autobid.bid.domain.Bid.user.auctions.{fk-target}
@ManyToOne(targetEntity = User.class)
@JoinColumn(name = "user_id", nullable = false)
private User user;
같은 경우에는 문제 없이 작동한다. 생략해도 무관하다.
부모가 어떻게 변할 때, 자식이 부모의 상태를 따라갈 것인가를 지정할 수 있다.
기본적으로는 자식은 부모의 어떤 작업에도 상태가 변하지 않는다.
public enum CascadeType {
ALL,
PERSIST,
MERGE,
REMOVE,
REFRESH,
DETACH
}
class Parent {
@OneToMany(mappedBy = "parent", cascade = {CascadeType.REMOVE})
private List<Child> children = new ArrayList<>();
}
class Child {
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
위와 같은 상황에서 아래의 코드를 수행한다면,
Parent p = repo.find(...); // p에 자식이 들어있는 상황
repo.remove(p);
p가 remove되었으므로, 자식도 따라서 remove 된다. -> 자식에 대한 DELETE 쿼리가 발생한다.
그러나 아래의 상황에서는 자식에 대한 DELETE 쿼리가 발생하지 않는다.
p.children.remove(0); // 0번 자식을 제거
부모가 remove된 것이 아니므로, 자식도 remove되지 않는 것이다.
orphanRemoval
은 @OneToMany
의 속성이다.
@OneToMany(mappedBy = ..., orphanRemoval = true) // 기본값은 false
orphanRemoval을 사용할 경우, 두 상황 모두에서 자식이 제거된다.
// case 1. 부모 객체 제거
repo.remove(p); -> 자식도 따라서 DELETE 쿼리 발생
// case 2. 자식 객체만 제거
p.child.remove(0) -> 연관 관계가 끊어졌으므로 자식에 대해서만 DELETE 쿼리 발생
특정 필드를 실제로 언제 불러올 것인지 지정할 수 있다.
@OneToMany
의 경우 기본적으로 FetchType.LAZY
로 설정되어 있다.
Lazy Loading
을 사용할 경우, 해당 필드에는 Proxy 객체
가 할당된다.
해당 필드를 실제로 사용할 때 조회 쿼리가 발생한다.
// `@ManyToOne`는 `FetchType.EAGER`가 기본값.
@ManyToOne(fetch = FetchType.EAGER)
// `@OneToMany`는 `FetchType.LAZY`가 기본값.
@OneToMany(fetch = FetchType.LAZY)
FK에 not null 제약조건
여부를 지정할 수 있다.
// Car Entity
@ManyToOne(optional = false) // true가 기본값
private User user;
// User Entity
@OneToMany(mappedBy = "user")
private List<Car> cars;
optional = false
로 설정되어 있으면, FK에 not null 제약조건이 걸린 DDL이 발생한다.
기본값인 optional = true
을 사용하면 FK가 nullable하므로, User가 없는 Car (record)가 존재할 수 있다.
이런 상황에서 Car를 조회하면, Join이 포함된 쿼리의 결과에 차량 데이터가 포함되지 않을 수 있다.
SELECT *
// car의 user_id = null인 레코드는 제외된다.
FROM Car JOIN User ON Car.user_id = User.id
WHERE Car.id = ?;
이를 막기 위해, optional = true
인 경우 JPA는 OUTER JOIN
을 수행한다.
select b1_0.id, a1_0.id
from b b1_0 left join a a1_0 on a1_0.id=b1_0.fk
where b1_0.id=?
# 'Outer' 생략 가능
# OUTER Join에는 LEFT, RIGHT, FULL JOIN이 있다
# MySQL은 OUTER JOIN을 지원하지 않는다 -> 대신 Left + Right로 대체 가능
// em : EntityManager
B b = em.find(B.class, 2l);
b // not null
b.getA() // null
그런데 optional = false
로 설정하면, FK가 not null하므로, FK가 없는 레코드가 생성될 수 없다. 그러므로 JPA는 INNER JOIN
을 수행한다.
select b1_0.id, a1_0.id
from b b1_0 join a a1_0 on a1_0.id=b1_0.fk
where b1_0.id=?
// em : EntityManager
B b = em.find(B.class, 2l);
b // not null: Table B 레코드의 FK가 not null이다는 가정하에
b.getA() // not null
만약 어떤 이유에서든지 Table B 레코드의 FK가 Null이면, optional = false
로 인해 INNER JOIN이 수행되므로, b는 null이 된다.