[자바 ORM 표준 JPA 프로그래밍] 6장. 다양한 연관관계 매핑
by Jo
6장. 다양한 연관관계 mapping
6.1 N:1
- 항상 N:1 의 반대 방향은 1:N, 1:N의 반대 방향은 N:1
- 1:N, N:1 관계에서 foreign key는 항상 N 쪽에 위치
- ∴ owner는 N 쪽
6.1.1 N:1 단방향 [N:1]
코드
// member entity
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
...
}
...
// team entity
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
- member 에서는 Member.team 으로 team entity 참조 가능, team 에서는 member 참조 불가능 -> N:1 단방향 연관관계
- Member entity 에서 Member.team 으로 member table team_id foreign key 관리
6.1.2 N:1 양방향 [N:1 & 1:N]
코드
// member entity
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
public void setTeam(Team team) {
this.team = team;
// 무한루프 방지
if (!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
...
}
...
// team entity
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
public void setMembers(Member member) {
this.members.add(member);
// 무한루프 방지
if (member.getTeam() != this) {
member.setTeam(this);
}
}
...
}
- 위 N:1 양방향 예제에서 실선이 연관관계 owner side(Member.team), 점선이 non-owning side(Team.members)
- 양방향에선 foreign key 있는 쪽이 owner
- N:1, 1:N 에선 항상 N 쪽에 foreign key 위치
- 여기선 N 쪽인 member 의 Member.team 이 owner
- non-owning side인 Team.members 는 조회를 위한 JPQL 또는 객체 그래프 탐색 시 사용
- 양방향 연관관계 에선 항상 서로 참조해야 함
- 어느 한 쪽만 참조시 양방향 성립 X
- 이를 위해 연관관계 편의 method 작성하는게 좋음
- 위 예제에서의 setTeam, setMembers
6.2 1:N
- 1:N 에서는 entity를 하나 이상 참조할 수 있음
- 자바 컬렉션인 Collection, List, Set, Map 중 하나 사용
6.2.1 1:N 단방향 [1:N]
- 1:N 단방향 관계는 JPA 2.0부터 지원
- 1:N, N:1 관계에서는 항상 N 쪽 table에 fk 있기 때문에 1:N 단방향 관계는 좀 특수
- 아래 예제처럼 N쪽 table에 있는 fk를 반대쪽 entity 에서 관리하게 됨
- 1:N 단방향 mapping 시에는 @JoinColumn 명시해야 함
- 명시하지 않을 경우 JPA는 JoinTable 전략을 기본으로 사용하여 mapping
- JoinTable 전략: 연결 테이블을 중간에 두고 연관관계를 관리하는 방식
- 명시하지 않을 경우 JPA는 JoinTable 전략을 기본으로 사용하여 mapping
코드
@Entity
public clas Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "team_id") // member table 에 있는 fk 사용
private List<Member> members = new ARrayList<Member>();
...
}
...
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
...
}
- 1:N 단방향 mapping 단점
- 다른 테이블에 있는 fk를 관리하게 되기 때문에 쿼리 추가로 필요해짐
- INSERT 한번으로 끝날 게 연관관계 처리를 위해 UPDATE 쿼리를 추가로 실행해야 함
- 다른 테이블에 있는 fk를 관리하게 되기 때문에 쿼리 추가로 필요해짐
코드
Member member1 = new Member("Alice");
Member member2 = new Member("Bob");
Team team1 = new Team("Alpha");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1); // INSERT member1
em.persist(member2); // INSERT member2
em.persist(team1); // INSERT team1, UPDATE member1.fk, UPDATE member2.fk
...
6.2.2 1:N 양방향 [1:N & N:1]
- 원칙적으로 1:N 양방향은 존재 X
- 대신 N:1 양방향 mapping 사용
- 굳이 1:N 양방향 mapping 구현하려면 1:N 단방향과 함께 반대편에서 같은 fk 쓰는 N:1 단방향 mapping 을 read only로 추가하면 됨
코드
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
// 1:N 단방향
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ARrayList<Member>();
...
}
...
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
// N:1 단방향 read only
@ManyToOne
@JoinColumn(name = "team_id", insertable = false, updatable = false)
private Team team;
...
}
- 위 예제처럼 1:N, N:1 모두 동일한 fk 사용하게 하고 N:1 쪽에서
insertable = false, updatable = false
로 설정해서 read only 만들면 됨
6.3 1:1
- 1:1 관계에서는 양쪽이 서로 하나의 관계만 가짐
- 어느 쪽이던 fk 가질 수 있음
- 주 테이블이랑 대상 테이블 중 어느쪽이 fk 가질 지 선택해야 함
- 대상 테이블에 fk
6.3.1 주 테이블에 fk
- 주 객체가 대상 객체 참조하듯 주 테이블에 fk 두고 대상 테이블을 참조
- fk를 객체 참조와 유사하게 사용할 수 있음
- 주로 객체지향 개발자들이 선호
- JPA에서도 좀 더 편리하게 mapping
- 주 테이블만 확인해도 대상 테이블과 연관관계 파악 가능
단방향
코드
@Entity
public class Member {
@Id
@GeneratedValue
private long id;
private String username;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
...
}
...
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
- 1:1 이므로 @OneToOne 으로 객체 mapping
- DB member table locker_id foregin key에 유니크 제약조건 UNI 추가
양방향
코드
@Entity
public class Member {
@Id
@GeneratedValue
private long id;
private String username;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
...
}
...
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
...
}
- 양방향이므로 연관관계 owner 필요
- member table이 fk 가지고 있으므로 Member entitiy의 Member.locker 가 owner
- 반대쪽인 Locker entity 의 Locker.member 는 mappedBy 설정
6.3.2 대상 테이블에 fk
- 테이블 관계를 1:1에서 1:N으로 변경할 때에도 테이블 구조 유지 가능
- DB 개발자들이 주로 선호
단방향
- JPA 에서는 위와 같이 대상 테이블에 fk 있는 1:1 단방향 관계 지원 안함
양방향
코드
@Entity
public class Member {
@Id
@GeneratedValue
private long id;
private String username;
@OneToOne
@JoinColumn(name = "member")
private Locker locker;
...
}
...
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "member_id")
private Member member;
...
}
- 1:1 mapping 시 대상 테이블에 fk 두려면 이렇게 양방향으로 해야 함
!!주의사항!!
proxy 사용할 때 fk 직접 관리하지 않는 1:1 관계는 lazy loading 설정이 안먹힘
예를 들어 위 예제에서 Locker.member 는 lazy loading 가능, Member.locker 는 불가능
proxy 대신 bytecode instrumentation 사용하면 해결할 수 있음
6.4 N:N
- 관계형 DB는 정규화된 테이블 2개로 N:N 표현 불가
- ∴ 일반적으로는 N:N을 1:N, N:1로 풀어내는 연결 테이블을 사용
- 객체의 경우엔 객체 2개만으로 N:N 관계 표현 가능
- @ManyToMany 사용하여 N:N mapping
6.4.1 N:N 단방향
코드
@Entity
public class Member {
@Id
private String id;
private String username;
@ManyToMany
@JoinTable(name = "member_product",
joinCoulmns = @JoinColumn(name = "member_id")
inverseJoinColumns = @JoinColumn(name = "product_id"))
private List<Product> products = new ArrayList<Product>();
...
}
...
@Entity
public class Product {
@Id
priavte String id;
private String name;
...
}
- @ManyToMany와 @JoinTable 을 사용하여 연결 테이블 매핑
- @JoinTable.name: 연결 테이블을 지정
- @JoinTable.joinColumns: 현재 방향 테이블과 매핑할 join column 정보 지정
- @JoinTable.inverseJoinColumns: 반대 방향 테이블과 매핑할 join column 정보 지정
코드
Product productA = new Product();
productA.setId("productA");
productA.setName("A");
em.persist(productA);
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("Alice");
member1.getProducts().add(productA); // 연관관계 설정
em.persist(member1);
sql
INSERT INTO product(id, name) VALUES("productA", "A");
INSERT INTO member(id, username) VALUES("member1", "Alice");
INSERT INTO member_product(member_id, product_id) VALUES("productA", "member1");
- 위와 같이 @ManyToMany, @JoinTable 로 연관관계 설정에둔 entity 저장할 때 연결 테이블에도 같이 값이 저장됨
코드
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts(); // 객체 그래프 탐색
for (Product product: products) {
System.out.println("product.name = " + product.getName());
}
sql
SELECT *
FROM member_product mp
INNER JOIN product p
ON mp.product_id = p.id
WHERE mp.member_id=?
- 위 예제 처럼 연관관계 설정된 Member::getProducts() 호출하면 연결 테이블을 join 하여 조회하는 쿼리가 실행됨
6.4.2 N:N 양방향
- N:N 양방향의 경우에도 owner 정해야 하며, 마찬가지로 mappedBy 속성을 사용한다
- 1:N, N:1과 마찬가지로 non-owning side entity 에 mappedB 설정
코드
@Entity
public class Member {
@Id
private String id;
private String username;
@ManyToMany
@JoinTable(name = "member_product",
joinCoulmns = @JoinColumn(name = "member_id")
inverseJoinColumns = @JoinColumn(name = "product_id"))
private List<Product> products = new ArrayList<Product>();
...
// N:N 양방향도 아래와 같이 연관관계 편의 method 추가해서 사용하는게 좋음
public void addProduct(Product product) {
...
products.add(product);
product.getMembers().add(this);
}
...
}
...
@Entity
public class Product {
@Id
priavte String id;
private String name;
@ManyToMany(mappedBy = "products") // 역방향 연관관계 설정 추가
private List<Member> members;
...
}
6.4.3 N:N 매핑의 한계와 극복, 연결 엔티티 사용
- @ManyToMany 를 사용하면 편리하긴 하지만 연결 테이블에 id 외에 다른 컬럼들을 추가적으로 사용할 때 매핑이 불가능
- ∴ 이러한 경우엔 연결 테이블을 매핑하는 연결 엔티티를 만들고 여기에 추가 컬럼들을 매핑 후 1:N, N:1 관계로 entity 연결해야 함
코드
@Entity
public class Member {
@Id
private String id;
// Member 와 MemberProduct N:1 양방향 관계
// 여기선 fk (member_id) 가지고 있는 MemberProduct 가 owner
@OneToMany(mappedBy = "member") // 역방향
private List<MemberProduct> memberProducts;
...
}
...
@Entity
public class Product {
@Id
private String id;
private String name;
...
}
...
@Entity
@IdClass(MemberProductId.class) // identifier clas 설정정
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name = "member_id")
private Member member; // MemberProductId.member와 연결
@Id
@ManyToOne
@JoinColumn(name = "product_id")
private Product product; // MemberProductId.product와 연결
private int orderAmount;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
...
}
...
// MemberProduc 식별자 클래스
public class MemberProductId implements Serializable {
private String member; // MemberProduct.member 와 연결
private String product; // MemberProduct.product 와 연결
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
- pk를 매핑하는 @Id와 fk를 매핑하는 @JoinColumn 을 같이 사용하여 pk + fk 한번에 매핑
- @IdClass 를 사용하여 복합 기본키를 매핑
복합 기본키 (복합키)
- 연결 엔티티는 pk가 두 fk로 이루어진 복합 기본키임
- 위 예제의 경우 MemberProduct entity 의 pk는 member_id 와 product_id 로 이루어진 복합키
- JPA 에서 복합키 사용하려면 별도의 식별자 클래스 만들어야 함
- 식별자 클래스는 @IdClass 로 연결 엔티티에 지정
- 위 예제의 경우 MemberProductId 클래스가 식별자 클래스
- 식별자 클래스의 특징
- 복합 키는 별도의 식별자 클래스로 만들어야 함
- Serializable 을 implement 해야 함
- equals, hashCode method 구현해야 함
- 기본 생성자 있어야 함
- 식별자 클래스는 반드시 public
- @IdClass 대신 @EmbeddedId 사용하는 방법도 있음
식별 관계
- 부모 테이블의 pk 받아서 자신의 pk & fk 로 사용하는 것을 identifying relationship 이라고 함
코드
public void save() {
// Member save
Member member1 = new Member("member1", "Alice");
em.persist(member1);
// Product save
Product productA = new Product("productA", "A");
em.persist(productA);
// MemberProduct save
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1);
memberPRoduct.setProduct(productA);
memberProduct.setOrderAmount(2);
em.persist(memberProduct);
}
...
public void find() {
// pk 생성
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + product.getName());
System.out.println("orderAmount = " + memberProduct.getOrderAmount());
}
- 위 예제에서 MemberProduct는 member, product 의 pk 를 받아서 MemberProductId 식별자 클래스로 묶어 복합 기본 키로 사용하고, 각각 member, product 와의 관계를 위한 fk 로도 사용함
- MemberProduct entity 는 DB 에 저장될 때 연관된 member 와 product 의 식별자 가져와서 자신의 pk 로 사용
- 조회시에는 식별자 클래스를 만들어서 사용
6.4.4 N:N 새로운 기본 키 사용
- 6.4.3절에서 처럼 복합 기본 키를 사용하면 복잡해짐
- 대신 DB에서 자동으로 생성해주는 대리 키를 사용하는 방법이 있음
- 간편하고 비즈니스 의존성 X
- ORM 매핑 시에 복합 키 만들지 않아도 돼서 매핑도 간단
코드
@Entity
public class Order {
@Id
@Column
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private int orderAmount;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
...
}
...
@Entity
public class Member {
@Id
@Column
private Long id;
@OneToMany(mappedBy = "member")
private List<Order> order = new ARrayList<Order>();
...
}
...
@Entity
public class Product {
@Id
@Column
private Long id;
@OneToMany(mappedBy = "product")
private List<Order> order = new ArrayList<Order>();
}
- MemberProduct 대신 Order table 을 만들고, 기본키를 member_id + product_id 복합키 대신 id 로 설정. member_id 와 product_id 는 fk 로만 활용
코드
public void save() {
// Member save
Member member1 = new Member("member1", "Alice");
em.persist(member1);
// Product save
Product productA = new ProductA("productA", "A");
em.persist(productA);
// Order save
Order order = new Order();
order.setMember(member1);
order.setProduct(productA);
order.SetOrderAmount(2);
em.persist(order); // {id: 1L, member_id: "member1", product_id: "productA", order_amount: 2}
}
...
public void find() {
Long orderId = 1L;
Order order = em.find(Order.class)
Member member = order.getMember();
Product product = order.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + member.getProduct());
System.out.println("orderAmount = " + order.getOrderAmount());
}
- 대리 키를 사용하여 N:N 관계를 매핑하면 위와 같이 좀 더 단순하게 저장 및 조회할 수 있음
6.4.5 N:N 연관관계 정리
- N:N 관계를 1:N & N:1 관계로 풀어내기 위해 연결 테이블 만들 때 식별자 구성 방법을 선택해야 함
- 식별 관계: 받아온 식별자를 pk(복합 기본키) & fk 로 활용
- 비식별 관계: 받아온 식별자는 단순히 fk로만 쓰고 새로운 식별자 추가해서 사용
- ORM 매핑 편리성 때문에 비식별 관계로 구성하는 게 좋음
Subscribe via RSS