
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만, 반대로 팀에는 회원을 참조하는 필드가 없다. 따라서 다대일 단방향 연관관계이다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@JoinColumn(name = "TEAM_ID") 을 통해 TEAM_ID 외래키와 매핑하였다.


@Entity
public class Team{
...
@OneToMany
@JoinColumn(name= "TEAM_ID") // MEMBER 테이블의 TEAM_ID
private List<Member> members = new ArrayList<Member>();
...
}
일대다 단방향 관계를 매핑할 때는 @JoinColumn 을 명시해야한다. 그렇지 않으면 JPA는 연관관계를 관리하는 조인테이블 전략을 기본으로 사용해서 매핑한다.
일대다 단방향 매핑의 단점
단점은, 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 것이다. 이렇게 되면, INSERT 할 때 연관관계를 처리하기 위한 UPDATE 문을 추가로 실행해야한다.
private static void testSave(EntityManager em) {
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team1 = new Team("team1", "팀1");
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
}
그러므로 일대다 단방향 보다는 다대일 양방향 매핑을 사용하자.
일대다 양방향 매핑은 존재하지 않는다. 대신 다대일 양방향 매핑을 사용해야한다.
(방법이 있긴한데.. Member에 읽기 전용으로 다대일 단방향 매핑을 추가하면 된다.)
@Entity
public class Member{
...
@ManyToOne
@JoinColumn(name="TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
양쪽 다 같은 키를 관리하면 문제가 발생하므로 읽기만 가능하도록 설정해두었다.
얘도 마찬가지로 일대다 단방향의 단점을 그대로 가지고 있다. 다대일 양방향 매핑을 사용하자!
일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느곳이나 외래키를 가질 수 있다.
주 테이블에 외래키
주 테이블에 외래키를 두고 대상테이블을 참조한다. 외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다.
대상 테이블에 외래키
전통적인 데이터베이스 개발자들이선호하는 방식이다. 장점은, 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

회원과 사물함의 일대일 단방향 관계를 나태낸 그림이다. MEMBER가 주테이블이고 LOCKER 는 대상 테이블 이다.
@Entity
public class Member2 {
@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을 사용했고, 데이터베이스는 LOCKER_ID 외래 키에 유니크 제약조건을 추가했다.

@Entity
public class Member2 {
@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;
...
}
Member가 외래키를 갖고 있으므로 연관관계의 주인이다. 따라서 Lcoker.member 에 mappedBy 를 선언해 연관관계의 주인이 아니라고 설정했다.
일대일 관계 중 대상테이블에 외래키가 있는 단방향관계는 JPA에서 지원하지 않는다.

그림으로 봐도 매핑할 수 있는 방법이 없다.

@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;
...
}
주 엔티티인 Member 엔티티 대신에 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 LOCKER 테이블의 외래키를 관리하도록 했다.
관계형 데이터베이스는 다대다 관계를 2개의 테이블로 표현할 수 없다. 그래서 이것을 풀어내는 연결테이블을 사용한다.
객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.
객체 연관관계

테이블 연관관계

회원과 상품 엔티티 코드를 보자.
@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>();
...
}
@Entity
public class Product{
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
@JoinTable을 통해서 연결 테이블을 바로 매핑하였다.
@JoinTable 속성
public static void save(EntityManager em) {
Product product = new Product();
product.setName("상품A");
em.persist(product);
Member4 member4 = new Member4();
member4.setUsername("회원4");
//1번째 방법
member4.getProducts().add(product);
em.persist(member4);
}
위 로직을 실행하면
INSERT INTO PRODUCT...
INSERT INTO MEMBER...
INSERT INTO MEMBER_PRODUCT...
위 쿼리가 실행된다.
public static void find() {
Member findMember = em.find(Member.class, memberId);
//객체 그래프 탐색
List<Product> findProducts = findMember.getProducts();
for (Product item : findProducts) {
System.out.println("itemName : " + item.getName());
}
}
member.getProducts()를 호출해서 상품 이름을 출력하면 다음 SQL이 실행된다.
select
products0_.MEMBER_ID as MEMBER_I1_25_0_,
products0_.PRODUCT_ID as PRODUCT_2_25_0_,
product1_.PRODUCT_ID as PRODUCT_1_34_1_,
product1_.name as name2_34_1_
from
MEMBER_PRODUCT products0_
inner join
Product product1_
on products0_.PRODUCT_ID=product1_.PRODUCT_ID
where
products0_.MEMBER_ID=?
```
N:N 양방향도 마찬가지로 @ManyToMany 를 사용하면 된다. 그리고 연관관계의 주인이 아닌 곳에 mappedBy 를 지정한다.
코드를 보자.
@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>();
...
}
@Entity
public class Product{
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
@ManyToMany(mappedBy = "products") // 역방향 추가
private List<Member> members;
...
}
Member 필드는 바꿀게 없고, Product.members 를 추가하여 양방향 관계를 설정한다.
그리고 편의를 위해 Member엔티티에 연관관계 편의 메소드를 추가하자.
public void addProduct(Product product)
...
products.add(product);
product.getMembers().add(this);
}
양방향 연관관계를 설정하였으므로 product.getMember()를 사용해서 역방향으로 객체그래프를 탐색할 수 있다.
public static void findInverse(EntityManager em) {
Product product = em.find(Product.class, 1L);
List<Member4> members = product.getMembers();
for (Member4 entityMember : members) {
System.out.println("entityMember = " + entityMember.getUsername());
}
}
위에서 사용했던 @ManyToMany 는 실무에서 사용하기에 한계가 있다. 왜냐하면, 보통은 연결테이블에 추가 컬럼이 필요하다.(예를들어서, 주문날짜/주문수량 같은 것...)

엔티티를 수정해보자.
1. Member 엔티티 수정
@Entity
public class Member{
@Id @Column(name = "MEMBER_ID")
private String id;
private String username;
@OneToMany
private List<MemberProduct> memberProducts;
...
}
이제 Member는 연관관계의 주인이 아니다.
2. Product 엔티티 수정
@Entity
public class Product{
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
Product(상품코드) 엔티티에서 회원상품 엔티티로 객체그래프 탐색이 필요하지 않아 연관관계를 만들지 않았다.
3. MemberProduct 엔티티 추가
@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;
...
}
public class MemberProductId implements Serializable {
private String member;
private String product;
//@Override equals, hashcode...
...
}
MemberProduct (회원상품)엔티티를 보면 기본 키를 매핑하는 @Id와 외래키를 매핑하는 @JoinColumn 을 동시에 사용해서 기본키+외래키를 한번에 매핑했다.
JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그리고 엔티티에 @IdClass 를 사용해서 식별자 클래스를 지정하면 된다.
복합키를 위한 식별자 클래스의 특징
- 복합 키는 별도의 식별자 클래스로 만들어야한다.
- Seializable을 구현해야한다.
- equals 와 hashCode 메소드를 구현해야한다.
- 기본 생성자가 있어야한다.
- 식별자 클래스는 public 이어야한다.
- @IdClass 를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
부모테이블의 기본키를 받아서 자신의 기본키+외래키로 사용하는 것을 식별관계라고 한다.
위 예에서는 회웡상품에서 회원(부모)의 기본키를 받아서 자신의 기본키로 사용함과 동시에 외래키로 사용한다. 상품테이블과도 마찬가지다.
따라서 식별관계라고 할 수 있다.
public static void save(EntityManager em) {
// 회원 저장
Member643 member = new Member643();
member.setId("member1");
member.setUsername("회원1");
em.persist(member);
//상품 저장
Product643 product = new Product643();
product.setId("productA");
product.setName("상품A");
em.persist(product);
//회원 상품 저장
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member);
memberProduct.setProduct(product);
memberProduct.setOrderAmount(10000);
em.persist(memberProduct);
}
회원상품 엔티티는 연관된 회원의 식별자와 상품의 식별자를 가져와서 자신의 기본키 값으로 사용한다.
public static void findByMemberProductId(EntityManager em) {
// 기본 키 값 생성
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member643 member = memberProduct.getMember();
Product643 product = memberProduct.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + product.getName());
System.out.println("order amount = " + memberProduct.getOrderAmount());
}
복합키는 항상 식별자 클래스를 만들고, 생성한 식별자 클래스로 엔티티를 조회한다.
이렇게 복합키를 사용하는 방식은 복잡하다.
별도의 식별자 클래스생성과 @IdClass 또는 @EmbeddedId도 사용해야한다. 또한 equals, hashCode 도 구현해야한다.
N:N관계에서 추천하는 기본키 생성전략은 데이터베이스에서 자동으로 생성해주는 대리키를 Long값으로 사용하는 것이다.
장점은? 영구히 쓸 수 있으며, 비즈니스에 의존하지 않는다.

테이블 명을 MEMBER_PRODUCT 에서 ORDER로 변경하였다.
@Entity
@Table(name = "Orders")
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member644 member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product644 product;
private int orderAmount;
...
}
ORDER_ID라는 새로운 기본키를 하나 만들고 MEMBER_ID, PRODUCT_ID 는 외래키로만 사용한다.
저장시에는?
...
Order order = new Order();
order.setMember(member);
order.setProduct(product);
order.setOrderAmount(10000);
em.persist(order);
조회시에는?
...
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member644 member = order.getMember();
Product644 product = order.getProduct();
얼마나 간단한가!
이처럼 새로운 기본키를 사용해서 다대다 관계를 풀어내는 것도 좋은 방법이다.
식별 관계 : 받아온 식별자 기본키+외래키
비식별관계 : 받아온 식별자는 외래키로만 사용하고, 새로운 식별자를 추가한다.