엔터티 연관 관계 매핑 시 다음의 3가지를 고려해야 한다.
보통 다대일, 일대다 관계를 가장 많이 사용하고 다대다 관계는 실무에서 거의 사용되지 않는다.
객체 관계에서 한 쪽만 참조하는 것을 단방향 관계, 양쪽이 서로 참조하는 것을 양방향 관계라고 한다.
JPA는 두 객체 연관 관계 중 하나를 정해 데이터베이스 외래 키를 관리하는데 이를 연관 관계의 주인이라고 한다. 보통 외래 키를 가진 테이블과 매핑된 엔터티가 연관 관계의 주인으로 선택된다. 연관 관계의 주인이 아닌 필드는 매핑 애너테이션의 mappedBy 속성에 연관 관계의 주인 필드 이름을 값으로 전달해야 한다.
다대일 관계와 일대다 관계를 항상 서로를 반대 방향으로 갖는다. 데이터베이스 테이블의 One(), Many() 관계에서 외래 키는 항상 Many-side에 존재한다. 그러므로 객체 양방향 관계에서도 연관 관계의 주인은 항상 Many-side이다.

예제 6.1. 회원 엔터티
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// Getter, Setter ...
}
예제 6.2. 팀 엔터티
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// Getter, Setter ...
}
회원 팀 방향의 객체 그래프 탐색만 가능하므로 구현 내용은 다대일 단방향 연관 관계이다. @JoinColumn(name = "TEAM_ID") 애너테이션으로 Member.team 필드를 TEAM_ID 외래 키와 매핑했다. 그러므로 이 필드가 연관 관계의 주인이 된다.

예제 6.3. 회원 엔터티
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
if (!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
...
예제 6.4. 팀 엔터티
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
this.members.add(member);
if (member.getTeam() != this) {
member.setTeam(this);
}
}
...
일대다, 다대일 연관 관계는 항상 Many-side에 외래 키가 있다. 그러므로 위 예제에서는 Member.team이 연관 관계의 주인이 된다. JPA는 외래 키 관리 시 연관 관계의 주인만 사용하며 연관 관계의 주인이 아닌 필드는 조회를 위한 JPQL이나 객체 그래프 탐색 시 사용된다.
어느 한 쪽만 참조하면 양방향 연관 관계가 성립하지 않는다. 회원 엔터티의 setTeam(), 팀 엔터티의 addMember()와 같은 연관 관계 편의 메서드를 통해 항상 서로를 참조하게 할 수 있다. 편의 메서드를 양쪽에 다 작성하면 무한 루프에 빠지므로 주의해야 한다. 예제 메서드는 무한 루프를 방지하기 위한 조건 블록이 포함되어 있다.
일대다 관계는 다대일 관계의 반대 방향으로 다수의 엔터티를 참조할 수 있기 때문에 자바 컬렉션 Collection, List, Set, Map 중 하나를 사용해야 한다.
일대다 단방향 관계는 JPA 2.0부터 지원한다.

보통 다대일, 일대다 관계에서 외래 키는 Many-side 테이블에 존재하기에 연관 관계의 주인 역시 Many-side 엔터티에 존재한다. 하지만 이 경우엔 One-side인 팀 엔터티의 members 필드가 연관 관계의 주인이 되어 관계의 반대편 테이블의 외래 키를 관리하는 특이한 양상이 된다.
예제 6.5. 일대다 단방향 팀 엔터티
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
// Getter, Setter ...
}
예제 6.6. 일대다 단방향 회원 엔터티
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
// Getter, Setter ...
}
일대다 단방향 관계 매핑 시엔 @OneToMany 매핑 애너테이션과 함께 @JoinColumn 애너테이션을 사용해야 한다. 그렇지 않으면 JPA는 외래 키 관리 주체의 모호함을 해결하기 위해 연결 테이블을 중간에 두고 연관 관계를 관리하는 조인 테이블(JoinTable) 전략을 기본으로 사용해서 매핑한다.
연관 관계의 주인이 다른 테이블과 매핑된 엔터티에 존재한다는 것이 가장 큰 문제점이다. 엔터티 자신과 대응되는 테이블에 외래 키가 있으면 INSERT SQL 한 번으로 엔터티의 저장 및 연관 관계 처리가 가능하지만 다른 테이블에 외래 키가 있으면 연관 관계의 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
예제 6.7. 일대다 단방향 매핑의 단점
@Test
@Transactional
void testSave() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1);
em.persist(member2);
em.persist(team1);
em.flush();
}
하이버네이트가 생성하는 SQL
Hibernate:
insert
into
member
(username, member_id)
values
(?, ?)
Hibernate:
insert
into
member
(username, member_id)
values
(?, ?)
Hibernate:
insert
into
team
(name, team_id)
values
(?, ?)
Hibernate:
update
member
set
team_id=?
where
member_id=?
Hibernate:
update
member
set
team_id=?
where
member_id=?
Member 엔터티는 Team 엔터티를 모르고 연관 관계의 주인은 Team 엔터티에 존재한다. 그러므로 Member 엔터티 저장 시엔 MEMBER 테이블의 TEAM_ID 컬럼에 어떤 값도 저장되지 않는다. 대신 Team 엔터티 저장 시 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트한다.
엔터티와 매핑된 테이블이 아닌 다른 테이블의 외래 키를 관리해야 한다는 것은 성능 문제도 유발하지만 관리의 부담도 가중시킨다. 이를 해결하는 좋은 방법은 차라리 일대다 단방향 매핑을 사용할 것이라면 다대일 양방향 매핑을 사용해 연관 관계의 주인은 Many-side 엔터티에 둘 수 있도록 하는 것이다.

일대다 양방향 매핑이란 One-side 엔터티가 연관 관계의 주인 필드를 갖고 있는 양방향 매핑을 의미할 수 있다. 이것은 유효하지 않고 대신 다대일 양방향 매핑을 사용해야 한다. @ManyToOne 매핑 애너테이션에는 mappedBy 속성이 없기 때문에 일반적인 방식으로는 일대다 양방향 매핑을 구현할 수 없고, 다음과 같이 @ManyToOne 애너테이션으로 지정한 필드에 트릭을 사용해야 한다.
예제 6.8. 일대다 양방향 팀 엔터티
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
// Getter, Setter ...
}
예제 6.9. 일대다 양방향 회원 엔터티
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
// Getter, Setter ...
}
이 트릭은 물리적으로는 연관 관계의 주인을 두 개 만드는 것이지만, Many-side 엔터티가 외래 키를 수정하지 못하도록 insertable = false, updatable = false로 설정해 읽기만 가능하게 설정한 것이다. 그러므로 이 트릭은 일대다 양방향 매핑이라기보단 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가해서 일대다 양방향처럼 보이도록 하는 방법이다.
일대일 관계는 양쪽이 서로 하나의 관계만 가지고 다음과 같은 특징이 있다.
주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두어 대상 테이블을 참조한다. 외래 키를 객체 참조와 비슷한 방식으로 사용할 수 있어 객체지향 개발자들이 선호한다. 이 방법은 주 테이블만 확인해도 대상 테이블과의 연관 관계를 파악할 수 있다는 장점이 있다.
대상 테이블에 외래 키를 두면 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있어 전통적인 데이터베이스 개발자들은 보통 이 방법을 선호한다.
일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA 또한 주 테이블에 외래 키가 있으면 엔터티를 좀 더 편리하게 매핑할 수 있다.

예제 6.10. 일대일 주 테이블에 외래 키, 단방향 예제 코드
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
...
}
@OneToOne 매핑 애너테이션은 객체를 매핑하고 Member 엔터티 인스턴스가 하나의 Locker 엔터티 인스턴스와 대응되어야 한다는 논리적 요구사항을 정의한다. @JoinColumn 애너테이션은 이를 물리적으로 구현하여 LOCKER_ID 외래 키에 유니크 제약 조건(UNI)을 추가한다.

예제 6.11. 일대일 주 테이블에 외래 키, 양방향 예제 코드
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
...
}
양방향이므로 mappedBy 속성에 연관 관계의 주인(Member.locker)을 지정했다.

JPA는 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 지원하지 않는다. JPA 2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했지만 일대일 단방향은 이런 매핑을 허용하지 않는다.

예제 6.12. 일대일 대상 테이블에 외래 키, 양방향 예제 코드
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 위와 같이 대상 엔터티에 연관 관계의 주인을 두어 LOCKER 테이블이 외래 키를 관리하도록 설정한다.
이 방식은 프록시를 사용할 때 지연 로딩으로 설정해도 즉시 로딩된다는 기술적 한계를 가지고 있다. 이를 해결하기 위해 bytecode instrumention을 참조할 수 있다.

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없기 때문에 보통 연결 테이블을 추가하여 일대다, 다대일 관계로 풀어낸다. 그런데 객체는 테이블과 달리 객체 2개로 다대다 관계를 만들 수 있다. 두 엔터티 객체 모두 컬렉션을 사용해 반대편의 엔터티 객체를 보관하면 된다. 이를 위해 @ManyToMany 매핑 애너테이션을 사용하는 것이다.
예제 6.13. 다대다 단방향 회원
@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<>();
...
}
예제 6.14. 다대다 단방향 상품
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
@ManyToMany 매핑 애너테이션 사용 시 @JoinTable 애너테이션으로 연결 테이블을 즉시 매핑한다. 그러므로 회원과 상품 엔터티를 연결하는 회원_상품(Member_Product) 엔터티 정의가 불필요하며 연결 테이블을 신경 쓰지 않아도 된다.
@JoinTable의 속성은 다음과 같다.
name: 연결 테이블의 이름(MEMBER_PRODUCT)joinColumns: 현재 방향인 회원 엔터티와 매핑할 조인 컬럼(MEMBER_ID)inverseJoinColumns: 반대 방향인 상품 엔터티와 매핑할 조인 컬럼(PRODUCT_ID)예제 6.15. 저장
@Test
@Transactional
void save() {
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA);
em.persist(member1);
em.flush();
}
위 테스트 실행 시 하이버네이트는 다음과 같은 SQL을 생성한다.
Hibernate:
insert
into
product
(name, product_id)
values
(?, ?)
Hibernate:
insert
into
member
(username, member_id)
values
(?, ?)
Hibernate:
insert
into
member_product
(member_id, product_id)
values
(?, ?)
예제 6.16. 탐색
@Test
@Transactional
public void find() {
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts(); // 객체 그래프 탐색
Assertions.assertEquals(1, products.size());
Assertions.assertEquals("productA", products.get(0).getId());
}
member.getProducts() 메서드를 호출하면 하이버네이트는 다음과 같은 SQL을 생성한다.
Hibernate:
select
p1_0.member_id,
p1_1.product_id,
p1_1.name
from
member_product p1_0
join
product p1_1
on p1_1.product_id=p1_0.product_id
where
p1_0.member_id=?
실행된 SQL을 보면 MEMBER, MEMBER_PRODUCT, PRODUCT의 삼중 테이블 조인이 아닌 MEMBER_PRODUCT, PRODUCT의 이중 조인만으로 연관된 상품을 조회하고 있다. 이처럼 복잡한 다대다 관계를 @ManyToMany 매핑 애너테이션을 사용하여 애플리케이션에서는 아주 단순하게 사용할 수 있다.
다대다 매핑의 역방향도 @ManyToMany 매핑 애너테이션을 사용하고, 반대쪽의 연관 관계의 주인을 mappedBy 속성에 지정한다.
예제 6.17. 역방향 추가
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
@ManyToMany(mappedBy = "products")
private List<Member> members;
...
}
회원 엔터티의 연관 관계 편의 메서드
public void addProduct(Product product) {
products.add(product);
if (!product.getMembers().contains(this)) {
product.getMembers().add(this);
}
}
여기서 다대다 매핑의 논리적인 불편함이 그대로 드러난다. 연결 테이블에 직접 접근할 수 없기 때문에 엔터티 중 하나가 연관 관계의 주인이 되어야 하는데 이는 엔터티 자신과 직접 매핑되지 않은 테이블의 외래 키를 관리하는 행위이다. 이로 인해 야기될 수 있는 불편함을 생각하면 연결 테이블에 대한 엔터티도 따로 정의하고 다대다 관계를 두 개의 다대일 관계로 분해하는 것이 효율적임을 짐작할 수 있다. 그렇게 되면 연결 테이블에 대한 엔터티가 양측 엔터티와의 관계에서 Many-side 엔터티가 되어 두 개의 연관 관계의 주인을 가질 수 있게 된다.
예제 6.18. 역방향 탐색
@Test
@Transactional
public void inverseSearch() {
Product product = em.find(Product.class, "productA");
List<Member> members = product.getMembers();
List<Executable> assertions = new ArrayList<>();
for (Member member : members) {
assertions.add(() -> {
Assertions.assertTrue(member.getProducts().contains(product));
});
}
Assertions.assertAll(assertions);
}
위 테스트 코드는 양방향 연관 관계를 통한 객체 그래프의 역방향 탐색을 검증한다.

@ManyToMany 매핑 애너테이션을 사용하면 연결 테이블을 자동으로 관리해 주기 때문에 도메인 모델이 단순해진다는 장점이 있다. 하지만 연결 테이블에도 주문 시간, 수량 등의 고유한 정보가 있을 것인데 그에 대한 컬럼을 추가할 수 없기 때문에 실무에서 적용하기는 어려운 방식이다.

그러므로 위와 같이 연결 엔터티를 정의하고 추가가 필요한 컬럼들을 매핑해야 한다. 또한 엔터티 간의 관계도 두 개의 다대일 관계를 활용하는 방식으로 풀어나가야 한다.
예제 6.19. 회원 코드
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
회원 엔터티와 회원상품 엔터티를 다대일 양방향 관계로 만들었다. 회원상품 엔터티가 연관 관계의 주인 필드를 갖는다.
예제 6.20. 상품 코드
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
상품 엔터티에서 회원상품 엔터티로 객체 탐색이 자주 발생하지 않을 것 같다면 유연하게 관계를 생략할 수도 있다.
예제 6.21. 회원상품 엔터티 코드
@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;
...
}
예제 6.22. 회원상품 식별자 클래스
public class MemberProductId implements Serializable {
private String member; // MemberProduct.member와 연결
private String product; // MemberProduct.product와 연결
// hashCode and equals
@Override
public boolean equals(Object object) {
if (!(object instanceof MemberProductId that)) return false;
return Objects.equals(member, that.member) && Objects.equals(product, that.product);
}
@Override
public int hashCode() {
return Objects.hash(member, product);
}
}
회원상품 엔터티(MemberProduct)는 기본 키를 매핑하는 @Id, 외래 키를 매핑하는 @JoinColumn 애너테이션을 함께 사용해서 식별자 관계를 정의하고 있다. 그리고 @IdClass 애너테이션을 사용해서 복합 기본 키도 매핑하고 있다.
JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들고 @IdClass 애너테이션이 이를 참조하도록 해야 한다. 복합 키를 위한 식별자 클래스는 다음과 같은 특징이 있다.
Serializable을 구현해야 한다.equals와 hashCode 메서드를 구현해야 한다.public이어야 한다.여기서 가장 중요한 특징은 2, 3번이다. @IdClass 애너테이션을 사용하면 엔터티의 여러 @Id 필드들이 모여 별도로 정의된 복합 키의 타입(MemberProductId)을 이룬다는 것을 JPA에 알릴 수 있다. JPA는 이를 토대로 @Id 애너테이션으로 지정된 필드의 객체 타입(또는 엔터티의 기본 키 필드 타입)을 바탕으로 식별자 클래스 객체를 구성하고 Java가 기본적으로 제공하는 객체를 기본 키로 사용할 때처럼 식별자 클래스 타입의 객체를 기본 키로 사용한다.
이때 3번과 관련하여 기본 키의 값을 영속성 컨텍스트의 1차 캐시에서 해시 테이블의 키 값으로 사용하기 위해 equals, hashCode 메서드를 재정의해야 하는 것이다.
그리고 2번과 관련하여 Java에서 기본적으로 제공되는 객체들은 이미 안전하게 직렬화할 수 있다고 표시되어 있기 때문에 JPA가 즉시 영속성 컨텍스트뿐만 아니라 그 밖에서도 객체를 안전하게 직렬화하여 바이트 스트림으로 관리할 수 있다. 하지만 앞서 정의한 식별자 클래스 객체는 Serializable 마커 인터페이스를 구현하여 '안전하게 직렬화할 수 있다'고 표시해야 한다. 그러면 JPA는 영속성 컨텍스트의 경계 밖에 있는 2차 캐시, 네트워크 등에서도 식별자 클래스 타입의 기본 키를 바이트 스트림으로 안전하게 보존할 수 있다.
@IdClass 애너테이션을 사용하는 방식은 이러한 원리를 이해하기엔 다소 비직관적이나 @EmbeddedId 애너테이션을 사용하면 직관적으로 이해할 수 있다.
회원상품 엔터티 클래스 정의
@Entity
public class MemberProduct {
@EmbeddedId
private MemberProductId id;
@MapsId("member")
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@MapsId("product")
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
회원상품 식별자 클래스 정의
@Embeddable
public class MemberProductId implements Serializable {
private String member;
private String product;
public MemberProductId() {}
@Override
public boolean equals(Object object) {
if (!(object instanceof MemberProductId that)) return false;
return Objects.equals(member, that.member) && Objects.equals(product, that.product);
}
@Override
public int hashCode() {
return Objects.hash(member, product);
}
}
예제 6.23. 저장하는 코드
@Test
@Transactional
void save() {
// 회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
// 상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
// 회원상품 저장
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1);
memberProduct.setProduct(productA);
memberProduct.setOrderAmount(2);
em.persist(memberProduct);
em.flush();
em.clear();
}
회원상품 엔터티를 만들면서 연관된 회원 엔터티와 상품 엔터티를 설정했다. 이후 회원상품 엔터티가 데이터베이스에 저장될 때 회원, 상품 엔터티의 식별자를 가져와 자신의 기본 키 값으로 사용한다.
예제 6.24. 조회 코드
@Test
@Transactional
void find() {
// 기본 키 값 생성
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Member foundMember = em.find(Member.class, "member1");
Product product = memberProduct.getProduct();
Product foundProduct = em.find(Product.class, "productA");
Assertions.assertEquals(member, foundMember);
Assertions.assertEquals(product, foundProduct);
}
복합 키는 반드시 식별자 클래스를 별도로 정의하여 관리해야 한다. em.find() 메서드는 사용자가 생성한 식별자 클래스의 객체를 기반으로 엔터티를 조회한다.

연결 테이블도 데이터베이스에서 자동으로 생성해 주는 Long 타입의 대리 키를 사용하면 ORM 매핑 시 복합 키를 만들지 않아도 되므로 간단히 매핑 완성이 가능하다. 연결 테이블은 이제 기본 키까지도 있기 때문에 Order 엔터티라고 불려도 손색이 없다.
예제 6.25. 주문 코드
@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;
...
}
대리 키 사용으로 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉬워졌다.
예제 6.26. 회원 엔터티와 상품 엔터티 코드
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
...
}
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
@OneToMany(mappedBy = "product")
private List<Order> orders = new ArrayList<>();
...
}
다음은 저장 및 조회에 대한 테스트 코드이다.
예제 6.27. 저장 코드
@BeforeEach
@Transactional
void save() {
// 회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
// 상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
// 주문 저장
Order order = new Order();
order.setMember(member1);
order.setProduct(productA);
order.setOrderAmount(2);
em.persist(order);
}
예제 6.28. 조회 코드
@Test
@Transactional
void find() {
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Member foundMember = em.find(Member.class, member.getId());
Product product = order.getProduct();
Product foundProduct = em.find(Product.class, product.getId());
Assertions.assertEquals(foundMember, member);
Assertions.assertEquals(foundProduct, product);
Assertions.assertTrue(foundMember.getOrders().contains(order));
Assertions.assertTrue(foundProduct.getOrders().contains(order));
}
다대다 관계를 일대다 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자 구성 방식을 선택해야 한다.
객체 입장에서는 비식별 관계가 ORM 매핑이 편리하다.