김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.
1. 다중성
관계 | 어노테이션 |
---|---|
다대일 | @ManyToOne |
일대다 | @OneToMany |
일대일 | @OneToOne |
다대다 | @ManyToMany |
2. 단방향, 양방향
테이블은 외래키 하나로 양쪽 조인이 가능하므로 사실상 방향 이라는 개념은 없습니다.
반대로 객체는 참조용 필드가 있는 쪽으로만 참조가 가능합니다. 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향입니다.
3. 연관관계의 주인
테이블은 외래키 하나로 두 테이블이 연관관계를 맺습니다. 객체에서 양방향 관계는 두 개의 참조를 사용해야 하는데, 이 둘 중에서 어느 참조가 외래키를 관리할 것인지를 지정해야합니다.
외래키를 관리하는 참조를 연관관계의 주인이라고 하며, 주인의 반대편은 외래키에 영향을 주지 않고 단순 조회만 가능합니다.
데이터베이스 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 N 쪽에 있습니다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 N 쪽입니다.
단방향에서 양방향으로 만들기 위해 반대쪽 사이드에 참조를 추가한다고 해서 테이블에 전혀 영향을 주지 않습니다. 왜냐하면 연관관계의 주인이 이미 외래키를 관리하고 있기 때문입니다.
일대다 단방향은 다대일의 반대로, 일대다(1 : N)에서 일(1)이 연관관계의 주인입니다.
위의 예시로 살펴보면 Team 은 Member 를 알고 싶은데, Member 는 Team 을 알고 싶지 않은 경우입니다. 하지만 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 존재합니다.
그래서 위의 설계를 구현하게 되면 Team 의 List memebrs 값을 변경했을 때 Member 테이블이 가진 TEAM_ID 외래키가 업데이트됩니다.
public class Team {
...
@OneToMany
@JoinColumn(name = "TEAM_ID") // 외래키를 관리
private List<Member> members = new ArrayList<>();
}
// ---------------------------------------------------
Member member = new Member();
member.setUsername("memebr1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist();
일대다 단방향의 경우, @OneToMany
와 함께 @JoinColumn
을 사용해서 외래키를 관리합니다. 일대다에서 @JoinColumn
은 필수이기 때문에 사용하지 않으면 중간에 테이블을 하나 추가하는 조인 테이블 방식을 사용하게 됩니다.
위의 코드에서 team.getMembers().add(member)
가 수행될 때 Member 테이블의 외래키를 업데이트하는 쿼리가 날라가게됩니다. 그래서 Member 저장 1번, Team 저장 1번, Member 업데이트 1번해서 총 3번의 쿼리가 실행됩니다.
해당 관계는 엔티티가 관리하는 외래키가 다른 테이블에 존재하며, 연관관계 관리를 위해 추가로 Update 쿼리가 실행된다는 단점때문에, 다(N)측이 일(1)로 갈 일이 없어도 일대다 단방향보다는 다대일 양방향을 사용하는 것이 좋습니다.
public class Member {
...
@ManyToOne
@JoinColum(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
}
일대다 양방향은 다(N)측에 해당하는 Member 의 @JoinColumn
에서 insertable, updatable 속성을 사용해서 읽기 전용으로 만들어 구현할 수는 있습니다.
JPA 에서 일대다 양방향은 공식적으로 존재하지 않고, 다대일 양방향을 사용하는 것이 좋습니다.
일대일 관계는 그 반대도 일대일 관계이며, 주 테이블이나 대상 테이블 중에서 외래키를 어느 테이블에 넣을지 선택할 수 있습니다. 외래키에 DB 유니크 제약조건을 추가해야 일대일 관계가 됩니다.
회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용된다는 가정과 함께 아래 예시들을 살펴보겠습니다.
주 테이블에 외래키 단방향의 경우 Member 에 LOCKER_ID 라는 외래키를 넣고 유니크 제약조건을 걸었을 때, Member 에 Locker 라는 참조를 선언하고 외래키를 매핑하면 됩니다.
양방향으로 만드려면 반대쪽에도 @OneToOne
어노테이션을 사용하고, mappedBy 속성을 사용하면 됩니다. 다대일 양방향 매핑 처럼 외래키가 있는 곳이 연관관계의 주인이 됩니다.
주 테이블에 외래키가 있을 때 단점은 값이 없으면 외래키에 null 을 허용해야 한다는 점입니다.
대상 테이블에 외래키가 있다는 의미는 예를 들어, Member 의 Locker 참조가 외래키를 관리하고 싶은데 외래키가 Locker 테이블에 있는 것을 의미하는데 대상 테이블 단방향의 경우는 JPA 에서 지원하지 않습니다.
대상 테이블 양방향은 지원이 가능하며, 일대일 주 테이블 외래키 양방향과 매핑 방법은 같은데 단순하게 생각해서 일대일 관계는 내 엔티티에 있는 외래키는 내가 직접 관리해야 한다고 생각하면 됩니다.
대상 테이블에 외래키가 있을 때 단점은 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시로딩 된다는 점입니다.
주 테이블에 외래키 단방향 그림을 보았을 때 JPA 가 Member 를 로딩할 때 LOCKER_ID 에 값이 있으면 프록시를 넣고, 없으면 null 을 집어넣으면 됩니다. 그래서 Member 만 쿼리하면 됩니다.
하지만 대상 테이블에 양방향 그림을 보면 Member 를 조회할 때 Locker 의 값이 있는지 없는지 알려면 Member 테이블만 조회해서는 알 수 없기 때문에 Locker 에 Member 의 ID 가 있는지 확인해야 합니다.
그래서 어차피 두 테이블 모두에서 조회를 해야 프록시를 넣을지, null 을 넣을지 알게 되기 때문에 일대일에서 대상 테이블에 외래키가 있으면 지연로딩으로 설정해도 즉시로딩이 됩니다.
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다. 그래서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 합니다.
예를 들어, 회원이 여러 개의 상품을 선택할 수 있고 하나의 상품은 여러 명의 회원들에게 선택될 수 있습니다. 이런 다대다 관계를 MEMBER_PRODUCT 는 중간 테이블을 만들어 일대다, 다대일로 풀어야 합니다.
하지만 객체는 컬렉션을 이용해서 객체 2개로 다대다 관계를 만들 수 있습니다. @ManyToMany
를 사용하고 @JoinTable
로 연결 테이블을 지정하면 됩니다.
// 하나의 엔티티에만 지정하면 단방향, 둘 다 지정하면 양방향
public class Member {
...
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
}
// -------------------------------------------------------
public class Product {
...
@ManyToMany
@JoinTable(mappedBy = "products")
private List<Member> members = new ArrayList<>();
}
위의 그림에서 연결 테이블인 MEMBER_PRODUCT 에는 두 개의 외래키만 가지고 있으며 추가적인 정보를 넣는게 불가능한데, 실제로 연결 테이블은 연결만 하고 끝나지 않고 주문시간이나 수량과 같은 데이터가 들어갈 수 있습니다.
연결 테이블용 엔티티를 추가( 연결 테이블을 엔티티로 승격 )해서 위처럼 @ManyToMany
를 @OneToMany
와 @ManyToOne
으로 사용하면 됩니다.
속성 | 설명 | 기본값 |
---|---|---|
name | 매핑할 외래키 이름 | 필드명_참조 테이블의 기본키 |
referencedColumnName | 외래키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본키 |
foreignKey | 외래키 제약조건을 직접 생성, 테이블 생성 시에만 사용 |
속성 | 설명 | 기본값 |
---|---|---|
optional | false 로 설정하면 연관된 엔티티가 항상 있어야 한다 | true |
fetch | 글로벌 패치 전략을 설정 | FetchType.EAGER |
cascade | 영속성 전이 기능을 사용한다 | |
targetEntity | 연관된 엔티티의 타입 정보를 설정 |
속성 | 설명 | 기본값 |
---|---|---|
mappedBy | 연관관계의 주인 필드를 선택 | |
fetch | 글로벌 패치 전략을 설정 | FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다 | |
targetEntity | 연관된 텐티티의 타입 정보를 설정 |