연관관계 기초
- 테이블
- 외래키 하나로 양쪽 조인 가능
- 방향이라는 개념이 없음
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이서로 참조하면 양방향
- 연관관계의 주인
- 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 A→B B→A 처럼 참조가 2군데
- 객체 양방향 관계는 참조가 2군데 있음 그중 주인을 지정
- 외래키를 관리하는 참조가 주인
- 외래키에 영향을 주지 않음, 단순 조회만 가능
다대 일 (ManyToOne)
- 가장 많이 사용하는 연관관계
- 다대일의 반대는 일대다
일대 다 단방향 (OneToMany)
- 실무에서 잘 사용되지 않음
member (다)
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
}
Team(일)
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@ToString.Exclude
// 일대 다 일방향의 관계
@OneToMany
@JoinColumn(name="team_id")
List<Member> members = new ArrayList<>();
}
해당 코드를 보면 스스로의 PK를 JoinColumn 으로 설정해준 모습이다. 코드부터가 뭔가 이상한 느낌을 받고 시작한다.
package hellojpa.section6;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class JpaRelationExam {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member = new Member();
member.setUsername("testUser");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
해당 코드를 재생시키면 DB에는 값이 정상적으로 들어가지만 필요 이상으로 쿼리가 많이 날라가는 것을 볼 수 있다.
하지만 이 관계의 정말 심각한 문제는 team 엔티티를 수정하려 코드를 작성해도 update문이 member로 날아가기 때문에 연관관계를 파악하기 정말 힘들어진다는 단점이 있다고 한다.
그럼으로 일대다 단방향으로 설계하기 보다는 양방향으로 설계하여 보다 연관관계를 직관적으로 설계하는것이 좋다. (딱봐도 그래보이긴 한다)
- 일대다 단방향 정리
- 일대다 단방향은 일대다에서 일이 연관관계의 주인이다
- 테이블 일대다 관계는 항상 다쪽에 외래키가 있다
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조
- @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함 (중간에 테이블을 하나 추가함)
- 일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 Update sql 실행
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
일대 다 양방향
- 얘도 정말 특이한 (어거지) 관계임
- 그냥 다대일 양방향 쓰자
일대 일 관계 (OneToOne)
- 일대일 관계는 거꾸로 해도 일대일
- 주 테이블이나 대상 테이블중에 외래키 선택 가능
- 주 테이블에 외래키
- 대상 테이블에 외래키
- 외래키에 데이터베이스 유니크 제약조건 추가
일대일 : 주 테이블에 외래 키 단방향
- 주 엔티티에 유니크 제약조건을 추가해주면 됨
- 다대일 단방향 매핑과 유사함
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
// 다대일 단방향 매핑과 유사함
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
}
package hellojpa.section6;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "locker_id")
private Long id;
private String name;
}
일대일 : 주 테이블에 외래 키 양방향
- 대상 엔티티에 주 엔티티 필드값 선언
package hellojpa.section6;
import jakarta.persistence.*;
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "locker_id")
private Long id;
private String name;
// 요 조건만 추가
// 읽기만 가능
@OneToOne(mappedBy = "locker")
private Member member;
}
일대일 : 주 테이블에 외래 키 양방향 정리
- 다대일 양방향 매핑처럼 외래키가 있는곳이 연관관계의 주인
- 반대편은 mappedBy 적용
일대일 : 대상 테이블에 외래 키 단방향
- 일대다 단방향과 비슷한 양상을 띈다
- 대상 테이블에 외래키가 있는 단방향 관계는 JPA 에서 지원하지 않는다
- 양방향 관계는 지원함 (그냥 양방향 쓰자)
일대일 관계는 DB를 설계할때 PK FK의 위치를 신중하게 정해야 한다.
- 일대일 관계 정리
- 주 테이블에 외래키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 찾음
- 객체지향 개발자가 선호
- JPA매핑 편리
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점 : 값이 없으면 외래 키에 null 허용
- 대상 테이블에 외래키
- 대상 테이블에 외래키가 존재
- 전통적인 데이터베이스 개발 선호
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨
- 주 테이블에 외래키
다대 다 관계 (ManyToMany)
- 결론적으로 보면 ManyToMany는 사용하면 안된다 (왜냐)
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
- 연결 테이블을 추가해서 일대다, 다대다 관계로 풀어내야 함
- 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하긴 하다 하지만 JPA를 사용한다 해도 DB의 구조를 뛰어넘는 작업은 불가능하다 (결국 ManyToMany를 사용해도 임의의 연결 테이블을 생성하게 된다)
ManyToMany 어노테이션을 사용해서 member_product 테이블 생성
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
@ManyToMany
@JoinTable(name = "member_product")
private List<Product> productList = new ArrayList<>();
}
package hellojpa.section6;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "product_list")
private List<Member> memberList = new ArrayList<>();
}
코드만 보면 굉장히 편해보인다 하지만 절대로 사용해선 안되는 이유가 있다
- 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 주문시간, 수량같은 데이터가 들어올 수 있음
- 단순 테이블 조인을 위한 테이블에는 이런 정보를 담을 수가 없음
- 쿼리가 정말 이상하게 나와 코드가 직관적이지 못함
다대 다 한계 극복
- 연결 테이블용 엔티티를 추가(연결 테이블을 엔티티로 승격)
- @ManyToMany → @OneToMany, @ManyToOne 으로 풀어서 작성
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
public class Product {
@Id @GeneratedValue
@Column(name = "product_id")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "product")
private List<MemberProduct> memberList = new ArrayList<>();
}
package hellojpa.section6;
import jakarta.persistence.*;
@Entity
public class MemberProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne()
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne()
@JoinColumn(name = "product_id")
private Product product;
}
package hellojpa.section6;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String username;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
이와 같이 엔티티를 명시해줘서 좀 더 직관적으로 설계해야 함
그리고 웬만하면 PK는 의미없는 값을 선언해줘서 사용하는게 좋다 (설계에 있어 좀 더 유연하게 대처할 수 있기때문)
'코딩딩 > Spring' 카테고리의 다른 글
프록시 (0) | 2023.11.08 |
---|---|
고오급 매핑 (0) | 2023.11.07 |
연관관계 매핑 기초 (1) | 2023.11.03 |
객체와 테이블 매핑 (1) | 2023.10.31 |
JPA의 준영속 상태 (0) | 2023.10.31 |