이 글은 '스프링 부트 핵심 가이드 - 스프링 부트를 활용한 애플리케이션 개발 실무' 책을 통해 학습한 내용을 정리한 글입니다.
09장. 연관관계 매핑
연관관계 매핑 종류와 방향
- One To One : 일대일(1:1)
- One To Many : 일대다(1:N)
- Many To One : 다대일(N:1)
- Many To Many : 다대다(N:M)
- 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
- 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식
JPA와 데이터베이스의 차이
- 데이터베이스 : 두 테이블의 연관관계를 설정하면 외래키를 통해 서로 조인해서 참조하는 구조
- JPA : 엔티티 간 참조 방향을 설정
- 연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 된다. 이런 관계에서는 주인(Owner)이라는 개념이 사용된다. 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나상대 엔티티는 읽는 작업만 수행할 수 있다.
일대일 매핑
1. 단방향 매핑
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id")
private UserProfile userProfile;
}
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
}
- 'User' 엔티티는 'userProfile' 필드를 가지며, 'UserProfile' 엔티티는 'User' 엔티티와 연관되지 않은 단순한 프로필 정보를 가진다.
- @OneToOne 어노테이션은 일대일 관계를 나타내는데 사용
- cascade 속성을 'CascadeType.ALL'로 설정하여 'User' 엔티티가 저장되거나 삭제될 때 'UserProfile' 엔티티에도 해당 변경 사항이 전파되도록 설정
- @JoinColumn 어노테이션은 외래 키 컬럼을 지정하는데 사용
- name : 매핑할 외래키의 이름 설정
- referencedColumnName : 외래키가 참조할 상대 테이블의 칼럼명 지정
- foreignKey : 외래키를 생성하면서 지정할 제약조건을 설정(unique, nullable, insertable, updatable 등)
2. 양방향 매핑
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private UserProfile userProfile;
}
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
- 'User' 엔티티는 'userProfile' 필드로 'UserProfile' 엔티티와 연결되어 있으며, 'UserProfile' 엔티티는 'user' 필드로 'User' 엔티티와 연결되어 있다.
- 'User' 엔티티에서는 'mappedBy' 속성을 사용하여 양방향 관계의 주인을 'UserProfile' 엔티티의 'user' 필드로 지정
- 순환참조로 인해 toString() 메서드를 호출할 때 스택오버플로우가 발생할 수 있다.
- @ToString.Exclude 어노테이션을 'User' 엔티티의 'userProfile' 필드에 붙임으로써 해결할 수 있다.
다대일 매핑
1. 단방향 매핑
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}
- @ManyToOne : 다대일 관계를 나타내는데 사용
- @JoinColumn : 외래 키 컬럼 지정
- 위 코드에서는 'department_id'라는 외래 키 컬럼을 'Employee' 엔티티의 'department' 필드와 매핑
- 순환참조를 막으려면 @ToString.Exclude 추가
- Employee 레포지토리만으로 Deparetment 객체 조회 가능
2. 양방향 매핑
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department")
private List<Employee> employees;
}
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
}
- 'Department' 엔티티는 'employees' 필드로 'Employee' 엔티티들과의 연관 관계를 가지고 있으며, 'Employee' 엔티티는 'department' 필드로 소속된 'Department' 엔티티를 참조
- @OneToMany : 일대다 관계
- mappedBy : 양방향 관계의 주인을 'Employee' 엔티티의 'department' 필드로 지정
- 일대다 연관관계의 경우 여러 엔티티가 포함될 수 있으므로 컬렉션(Collections, List, Map) 형식으로 필드 생성
- 'Department' 엔티티는 주인이 아니라서 외래 키를 관리할 수 없다.
- 즉, Department를 등록한 후 각 Employee 객체에 설정하는 작업을 통해 DB에 저장해야 한다.
- Department 엔티티에서 정의한 employees 필드에 Employee 객체를 추가하는 방식으로 DB에 저장하게 되면 해당 데이터는 DB에 반영되지 않는다.
일대다 매핑
1. 일대다 단방향 매핑
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
}
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
}
- 'Comment' 엔티티는 'Post' 엔티티에 대한 참조인 'post' 필드를 가지고 있다.
- @ManyToOne : 다대일 관계
- @JoinColumn : 'post_id' 라는 외래 키 컬럼을 'Comment' 엔티티의 'post' 필드와 매핑
- 단점
- 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다.
- 다대일 구조와 다르게 외래키를 설정하기 위해서 다른 테이블에 대한 update 쿼리를 발생시킨다.
- 단점을 해결하기 위해서는 다대일 연관관계를 사용하는 것이 좋다.
2. 일대다 양방향 매핑
- 일대다 양방향 매핑의 경우 어느 엔티티 클래스도 연관관계의 주인이 될 수 없다.
다대다 매핑
- 실무에서 거의 사용되지 않는 구성
- 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어 진다. 이런 경우에는 교차 엔티티라고 부르느 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일 관계로 해소한다.
1. 다대다 단방향 매핑
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
- @ManyToMany : 다대다 관계
- 리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @JoinColumn 을 설정할 필요가 없다.
- @JoinTable(name = " ") : 중간 테이블 이름 지정
- 중간 테이블에서는 두 테이블에서 id 값을 가져와 두 개의 외래키가 설정된다.
2. 다대다 양방향 매핑
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses;
}
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private List<Student> students;
}
- @ManyToMany : 다대다 관계
- mappedBy : 양방향 관계의 주인 설정
- 중간 테이블이 연관관계를 설정하고 있기 때문에 DB의 테이블 구조는 변경되지 않는다.
- 중간 테이블을 통해 연관된 엔티티의 값을 가져온다.
- 그러나 중간 테이블 생성으로 인해 예기치 못한 쿼리가 생길 수 있으므로 관리가 힘들다.
- 따라서, 중간 테이블 생성 대신 일대다/다대일로 연관관계를 맺을 수 있는 테이블을 생성하여 JPA 에서 관리할 수 있도록 한다.
- @ToString.Exlucde : 순환참조자 발생하기 때문에 둘 중 한 엔티티의 필드에 어노테이션을 붙인다.
영속성 전이 (Cascade)
특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것
- ALL : 모든 영속 상태 변경에 대해 영속성 전이를 적용
- PERSIST : 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
- MERGE : 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
- REMOVE : 엔티티를 제거할 때 연관된 엔티티도 제거
- REFRESH : 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
- DETACH : 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외
고아 객체
- JPA 에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미
- JPA 에는 고아 객체를 자동으로 제거하는 기능 존재
- but, 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 고아 객체를 자동으로 제거하는 기능은 사용하지 않는 것이 좋다.
- ex) @OneToMany(..., orphanRemoval true)
'북 스터디 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
[스프링 부트] 11장. 액추에이터 활용하기 ~ 12장. 서버 간 통신 (0) | 2023.07.02 |
---|---|
[스프링 부트] 10장. 유효성 검사와 예외 처리 (0) | 2023.06.25 |
[스프링 부트] 08장. Spring Data JPA 활용 (0) | 2023.06.11 |
[스프링 부트] 06장. 데이터베이스 연동 (0) | 2023.06.04 |
[스프링 부트] 04장 ~ 05장 (0) | 2023.05.28 |