이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
다중성
종류 | 애노테이션 | 방향 | 비고 |
---|---|---|---|
다대일 | @ManyToOne | 단방향, 양방향 | 가장 많이 사용 |
일대다 | @OneToMany | 단방향, 양방향 | 가장 많이 사용 |
일대일 | @OneToOne | ◾ 주 테이블 단방향, 양방향 ◾ 대상 테이블 단방향, 양방향 | |
다대다 | @ManyToMany | 단방향, 양방향 | 거의 사용 x |
참조방향 : 객체 관계에서 어느쪽 관계를 참조하는가
연관관계의 주인 : 두 객체 연관관계 중 데이터베이스 외래 키를 관리하는 연관관계
mappedBy
속성은 연관관계의 주인이 아닌 쪽에서 사용@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID") //Member.team 필드를 TEAM_ID 외래키와 매핑
private Team team;
//Getter, Setter ...
...
}
@Entity
public class Team {
@Id @GeneratedVaue
@Column(name = "TEAM_ID")
private String id;
private String name;
//Getter, Setter ...
...
}
Member.team
으로 팀 엔티티를 참조할 수 있지만 팀에는 회원을 참조하는 필드 xMember.team
필드로 회원 테이블의 TEAM_ID
외래키 관리@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 void setTeam(Team tema) {
this.team = team;
//무한루프에 빠지지 않도록 체크
if(!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
@Id @GeneratedVaue
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
public void addMember(Member member) {
this.members.add(member);
if(member.getTeam() != this) { //무한루프에 빠지지 않도록 체크
member.setTeam(this);
}
}
...
}
MEMBER
테이블이 외래 키 가지고 있음Member.team
이 연관관계의 주인Team.members
는 조회를 위한 JPQL이나 객체 그래프 탐색 시 사용setTeam()
, addMember()
일대다 관계는 엔티티를 하나 이상 참조 가능하므로 자바 컬렉션인 Collection
, List
, Set
, Map
중에 하나를 사용해야 한다.
@Entity
public class Team {
@Id @GenereatedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") //MEMBER 테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<Member>();
//Getter, Setter ...
}
@Entity
public class Member {
@Id @GenereatedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
//Getter, Setter ...
}
@JoinColumn
명시 필수UPDATE
SQL을 추가로 실행해야 함public 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); //INSERT-member1
em.persist(member2); //INSERT-member2
em.persist(team1); //INSERT-team1,
//UPDATE-member1.fk,
//UPDATE-member2.fk
transaction.commit();
}
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Team (TEAM_ID, name) values (null, ?)
update Member set TEAM_ID=? where MEMBER_ID=?
update Member set TEAM_ID=? where MEMBER_ID=?
Member
엔티티 저장 시 MEMBER
테이블의 TEAM_ID
외래 키에 저장되는 값 xMember
엔티티는 Team
엔티티를 인식하지 못 함Team
엔티티의 members
가 관리Team
엔티티 저장 시 Team.members
의 참조 값 확인하여 회원 테이블의 TEAM_ID
외래 키 업데이트@OneToMany
는 연관관계의 주인이 될 수 없음@Entity
public class Team {
@Id @GenereatedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<Member>();
//Getter, Setter ...
}
@Entity
public class Member {
@Id @GenereatedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn( //다대일 단방향 매핑 추가
name = "TEAM_ID",
insertable = false, updatealbe = false //읽기 전용
)
private Team team;
//Getter, Setter ...
}
양쪽이 서로 하나의 관계만 가지는 경우
LOCKER_ID
외래 키에 유니크 제약 조건 추가@Entity
public class Member {
@Id @GenereatedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GenereatedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
...
}
@Entity
public class Member {
@Id @GenereatedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GenereatedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker") //연관관계의 주인이 아님을 설정
private Member member;
...
}
MEMBER
테이블이 외래 키 소유Member
엔티티의 Member.locker
가 연관관계의 주인Locker
에서 Member
방향으로 수정Locker
를 연관관계의 주인으로 설정@Entity
public class Member {
@Id @GenereatedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GenereatedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
Locker
를 연관관계의 주인으로 설정LOCKER
테이블의 외래 키 관리💡 주의
- 프록시를 사용할 때 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩 된다.
ex)Member.locker
지연 로딩 설정 → 즉시 로딩- 프록시의 한계로 인해 발생하는 문제이다.
- bytecode instrumentation을 사용하여 해결할 수 있다.
@ManyToMany
를 사용하여 다대다 관계 매핑 가능@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private Long 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 | 현재 방향 엔티티와 매핑할 조인 컬럼 정보 지정 |
inverseJoinColumns | 반대 방향 엔티티와 매핑할 조인 컬럼 정보 지정 |
public 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);
}
// 코드 실행
INSERT INTO PRODUCT ...
INSERT INTO MEMBER ...
INSERT INTO MEMBER_PRODUCT ...
public void find() {
Member member = em.find(Member.class, "memeber1");
List<Product> products = member.getProducts(); //객체 그래프 탐색
for (Product product : products) {
System.out.println("product.name = " + product.getName());
}
}
// 코드 실행
SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID=P.PRODUCT_ID
WHERE MP.MEMBER_ID=?
MEMBER_PRODUCT
와 상품 테이블을 조인하여 연관 상품 조회@Entity
public class Product {
@Id
private String id;
@ManyToMany(mappedBy = "products") //역방향 추가
private List<Member> members;
}
@ManyToMany
사용mappedBy
로 연관관계 주인 지정mappedBy
가 없는 곳이 연관관계의 주인member.getProducts().add(product);
product.getMembers().add(member);
@Entity
pulbic class Member {
...
public void addProduct(Product product) {
...
products.add(product);
product.getMembers().add(this);
}
}
member.addProduct(product);
public void findInverse() {
Product product = em.find(Product.class, "productA");
List<Member> members = product.getMembers();
for(Member member : members) {
System.out.println("member = " + member.getUsername());
}
}
@ManyToMany
를 사용한 다대다 매핑은 실무에서 사용하기에는 한계가 존재한다.
예를 들어, 회원이 상품을 주문하면 연결 테이블에 주문한 회원 아이디와 상품 아이디 뿐만 아니라 주문 수량이나 주문 날짜와 같은 컬럼이 더 필요하다.
연결 테이블에 주문 수량(ORDERAMOUNT
)과 주문 날짜(ORDERDATE
) 컬럼을 추가하면 더 이상 @ManyToMany
를 사용할 수 없다. 왜냐하면, 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.
연결 테이블을 매핑하는 연결 엔티티를 만들어 추가한 컬럼들을 매핑해야 한다. 또한 엔티티 간의 관계도 일대다, 다대일 관계로 풀어야 한다.
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
//역방향
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProduct;
...
}
Member.memberProducts
에 mappedBy
사용@Entity
public class Product {
@Id @Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
@Entity
@IdClass(MemberProductId.class) //복합 기본 키 매핑
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와 연결
...
}
MEMBER_ID
, PRODUCT_ID
→ 복합 기본 키public class MemberProductId implements Serializable {
private String member; //MemberProduct.member와 연결
private String product; //MemberProduct.product와 연결
//hashCode and equals
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
@IdClass
를 사용하여 식별자 클래스 지정Serializable
을 구현해야 한다.equals
와 hashCode
메소드를 구현해야 한다.💡 자바 IDE에 대부분 자동 생성 기능 존재
public
이어야 한다.@IdClass
외에 @EmbeddedId
도 사용 가능 하다.부모 테이블의 기본 키를 받아 자신의 기본 키 + 외래 키로 사용하는 것
MemberProduct
)MemberProductId
식별자 클래스로 두 기본 키를 묶어 복합 기본 키로 사용public void save() {
//회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
//상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
//회원상품 저장
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1); //주문 회원 - 연관관계 설정
memberProduct.setProduct(productA); //주문 상품 - 연관관계 설정
memberProduct.setOrderAmount(2); //주문 수량
em.persist(memberProduct);
}
public void find() {
//기본 키 값 생성
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());
}
em.find(MemberProduct.class, memberProductId)
Long
값으로 사용@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;
...
}
public void save() {
//회원 저장
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
em.persist(member1);
//상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품1");
em.persist(productA);
//주문 저장
Order order = new Order();
order.setMember(member1); //주문 회원 - 연관관계 설정
order.setProduct(productA); //주문 상품 - 연관관계 설정
order.setOrderAmount(2); //주문 수량
em.persist(order);
}
public void find() {
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Product product = order.getProduct();
System.out.println("member = " + member.getUsername());
System.out.println("product = " + product.getName());
System.out.println("orderAmount = " + order.getOrderAmount());
}
다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자 구성 방법을 선택해야 한다.