1. 일대일 매핑
1) 특징
- 둘 중 어느 곳에나 외래 키를 가질 수 있다.
- 객체 매핑에 @OneToOne 어노테이션을 사용하고 DB의 외래 키에는 유니크 제약 조건을 건다.
- 예시로 직원과 사물함의 관계가 있다.
2) 외래 키 전략
(1) 주 테이블에 외래 키를 두는 방법
- 외래 키를 객체 참조와 비슷하게 사용할 수 있음
(2) 대상 테이블에 외래 키를 두는 방법
- 테이블 관계가 일대일에서 일대다로 변경되어도 테이블의 구조를 유지하기 쉬움
- 단방향으로는 불가능한 전략이기 때문에 양방향으로 서로 참조하고 있어야 한다.
2. 다대다 매핑
1) 특징
- 일반적으로 연결 엔티티를 추가하여 일대다/다대일 관계로 분리한다.
- 결론부터 말하자면, [4) 연결 엔티티에 새로운 기본 키를 부여하는 방식] 전략을 사용하는 것이 제일 유연하고 깔끔하다. 그 이유는 2), 3)을 읽어보면서 이해하는 것을 권장한다.
2) @ManyToMany 방식
- 실무에서 사용하기에는 한계가 있지만 사용법 정도는 가볍게 짚고 넘어가보자.
- 회원과 상품을 다대다 관계의 예시로 들어보자. 하나의 회원은 여러 상품을 주문할 수 있고, 하나의 상품(종류)는 여러 회원들에 의해 주문된다.
<Member.class>
@Entity
public class Member {
@Id @Column(name="MEMBER_ID")
private String id;
private String username;
@ManyToMany
@JoinTable(name="MEMBER_PRODUCT", joinColumns=@JoinColumn(name="MEMBER_ID"), inverseJoinColumns=@JoinColumn(name="PRODUCT_ID"))
private List<Product> products = new ArrayList<Product>();
...
}
- 여기서 @JoinTable은 연결 테이블을 생성하기 위한 어노테이션으로, 각 속성의 의미는 다음과 같다.
- name : 연결 테이블의 이름
- joinColumns : 현재 방향에서 매핑할 @JoinColumn 정보를 지정
- inverseJoinColumns : 반대 방향에서 매핑할 @JoinColumn 정보를 지정
<Product.class>
@Entity
public class Product {
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
@ManyToMany(mappedBy="products")
private List<Member> members;
...
}
- 이런식으로 @ManyToMany를 사용하여 연결 테이블을 자동으로 만들도록 할 수는 있다. 하지만, 연결 테이블에는 양 쪽 테이블의 기본키 외에도 추가적인 컬럼이 필요한 경우가 많다. 그래서 엔티티에서는 이 추가적인 컬럼들을 매핑할 방법이 없게 되는 것이다.
3) 복합 키를 갖는 연결 엔티티 방식
- 연결 엔티티를 추가하면 연결 엔티티 안에 추가되는 다양한 컬럼들을 사용할 수 있다. 이번에도 회원과 상품을 예시로 들어보자.
- 연결 엔티티를 통해서 다대다 관계가 일대다/다대일 두 쌍의 관계로 분리되기 때문에, 양방향 매핑 시에는 항상 연결 엔티티쪽이 연관관계의 주인이 된다.
- 복합 키와 복합 키를 구성하는 컬럼에는 @GenerateValue를 사용할 수 없다.
(1) 연결 엔티티 생성
<MemberProduct.class>
@Entity
@IdClass(MemberProductId.class) // 식별자 클래스를 지정 (잠시 후에 살펴보자)
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
- 다대다 관계를 갖는 두 엔티티의 기본 키를 각각 가져와야 하는데, 가져온 각 기본 키는 연결 엔티티에서 기본 키로 사용함과 동시에 외래 키로도 활용한다. 그래서 @Id와 @JoinColumn을 같이 사용하는 것이다.
- 위와 같이 복합 키를 사용하기 위해서는 두 기본 키를 엮기 위한 해당 식별자 클래스가 있어야 한다. 식별자 클래스를 지정하기 위한 어노테이션으로는 @IdClass와 @EmbeddedId가 있다.
(2) 연결 엔티티 식별자 클래스 생성
<MemberProductId.class>
@EqualsAndHashCode
public class MemberProductId implements Serializable {
private String member;
private String product;
...
}
- 복합 키를 위한 식별자 클래스는 public이어야 하고, Serializable 인터페이스를 상속해야 하고, equals()와 hashCode() 메소드를 구현해야 하고, 기본 생성자가 있어야 한다. 그리고 식별자 클래스의 속성명과 엔티티 클래스의 속성명은 서로 같아야 한다.
※ equals()와 hashCode()를 구현하지 않으면 식별자 객체의 동등성이 지켜지지 않아서 엔티티 관리에 심각한 문제가 발생할 수 있다.
(3) 엔티티
<Member.class>
@Entity
public class Member {
@Id @Column(name="MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy="member") // Member은 연관관계의 주인이 아님
private List<Product> products = new ArrayList<Product>();
...
}
<Product.class>
@Entity
public class Product {
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
...
}
(4) 조회
- 다음과 같이 영속성 컨텍스트에 저장되었다고 가정하자.
Member member1 = new Member();
member1.setId("member1");
member1.setName("jooyeok");
em.persist(member1);
Product productA = new Product();
productA.setId("productA");
productA.setName("사과");
em.persist(productA);
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1);
memberProduct.setProduct(productA);
memberProduct.setOrderAmount(5);
em.persist(memberProduct);
- 조회는 MemberProductId를 통해 MemberProduct 엔티티를 조회하고, MemberProduct를 통해 다시 Member 객체와 Product 객체를 가져오는 식으로 동작한다.
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();
(5) 연결 엔티티 참조
- 연결 엔티티를 참조하는 필드에 매핑되는 외래 키는 복합 키가 된다. 그래서 이 외래 키를 매핑 할 때는 여러 컬럼을 매핑해야 하므로 @JoinColumns를 사용해서 각 외래 키 컬럼을 @JoinColumn으로 매핑하자.
- @JoinColumn의 name은 외래 키의 이름이고, referencedColumn은 외래 키가 조인할 대상 테이블의 컬럼 이름이다.
@JoinColumns({
@JoinColumn(name="MEMBER_ID", referencedColumnName="MEMBER_ID"),
@JoinColumn(name="PRODUCT_ID", referencedColumnName="PRODUCT_ID")
})
private MemberProduct memberProduct;
※ JoinColumn의 name 속성값과 referencedColumnName 속성값이 같다면 referencedColumnName 속성은 생략해도 된다.
4) 연결 엔티티에 새로운 기본 키를 부여하는 방식
- 복합 키를 갖는 연결 엔티티 방식도 나쁘진 않아보이지만 일일이 식별자 클래스를 만드는 것이 번거로울 수 있다. 그래서 연결 엔티티에는 새로운 기본 키 하나만 부여해주고 기존에 사용하던 복합 키는 모두 외래 키로 사용하는 방식을 생각해볼 수도 있다.
※ 이 때, 기본 키 생성 전략은 DB에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 전략을 사용하자. 간편하면서도, 기본 키를 거의 영구적으로 쓸 수 있고 비즈니스에 의존하지 않는다는 장점이 생긴다.
- 이제 이 전략을 적용한 소스 코드를 살펴보자. 이번에도 회원과 상품을 예시로 들어보자.
(1) 연결 엔티티
@Entity
public class Order {
@Id @GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
(2) 엔티티
- 변경 사항 없음
(3) 조회
- 다음과 같이 영속성 컨텍스트에 저장되었다고 가정하자.
Member member1 = new Member();
member1.setId("member1");
member1.setName("jooyeok");
em.persist(member1);
Product productA = new Product();
productA.setId("productA");
productA.setName("사과");
em.persist(productA);
Order order = new Order();
order.setMember(member1);
order.setProduct(productA);
order.setOrderAmount(5);
em.persist(order);
- 이번에는 단순히 Order 엔티티의 id를 통해 조회할 수 있게 되었다.
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Product product = order.getProduct();
● 참고자료 : 자바 ORM 표준 JPA 프로그래밍
'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글
[JPA] 프록시 객체 (0) | 2022.08.01 |
---|---|
[JPA] 고급 매핑 기술 (0) | 2022.07.28 |
[JPA] 단방향 매핑과 양방향 매핑 (0) | 2021.08.18 |
[JPA] EntityManager / persistence.xml (0) | 2021.08.16 |
[JPA] Entity Annotations (0) | 2021.08.16 |
댓글