JPA에는 일대다 연관관계를 구성하기 위한 @ManyToOne
, @OneToMany
어노테이션이 있다. 필드 값에 해당 어노테이션을 붙이고 타입을 collection으로 하면, ORM 측에서 collection(여기서 List
라고 하겠다.)의 원소에 DB의 entity값을 매핑해준다.
예시라 하기에도 민망한 기본 코드지만.. 일대다 관계인 Parent
: Child
는 아래와 같이 구성할 수 있다.
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "parent",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
private List<Child> children = new ArrayList<>();
// Constructors, getters, setters, etc.
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
// Constructors, getters, setters, etc.
}
@JoinColumn
은 many측인 Child
에서 fk를 보관할 필드와 column이름을 지정할 수 있도록 해주는 어노테이션이다. 이대로 hibernate가 동작하게 하면 child 테이블의 "parent_id" 컬럼에 parent의 pk가 저장된다.
mappedBy
는 연관관계의 주인이 아닌 엔티티, 그러니까 fk를 가지고 있지 않은 Parent
측에서 연관관계의 주인 엔티티의 어느 필드에 연관을 맺고있는지 명시하는 기능을 한다. 만약 Parent
와 Child
가 위 코드의 일대다 관계 이외에 어떤 관계를 하나 더 갖고있다면 그를 구분해주는 과정이 필요한데, 그때 mappedBy
속성이 쓰인다.
cascade
는 영속성 전이 설정인데, 하나의 @Transactional
단위에서 관리되는 연관 영속성 객체들의 행위를 어떻게 전파할지 결정하는 역할을 한다. CascadeType
내의 ALL
, PERSIST
, REMOVE
등이 있다. 특정 객체를 persist 상태로 설정하는 동작이 수행되었을 경우(save 등), CascadeType.PERSIS
가 적용되었다면, 트랜잭션 안에서 관리되는 연관 엔티티 역시 persist 상태로 설정되는 동작이 수행된다. 위의 예시에선 Parent
엔티티의 save동작이 일어날 때 children
리스트의 원소 Child
객체들 역시 save동작이 한꺼번에 일어난다는 뜻이다. forEach
같은거 이용해서 원소 하나하나를 돌아다닐 필요가 없다는 뜻!
fetch
는 해당 엔티티가 조회되었을 때, 연관 관계에 있는 엔티티 역시 한꺼번에 조회할 것인지 옵션을 설정할 수 있게 해준다. Parent
엔티티에 딸린 Child
가 한 1억개가 될 수도 있다. Parent
가 조회될 때마다 필요하지도 않은 1억개의 children
필드가 매번 조회된다면 엄청난 손해가 될 것이다. 이 때 FetchType.LAZY
를 설정하면 children
필드에 접근하지 않는 한 연관관계의 엔티티 정보는 불러와지지 않는다.
@ManyToMany
?가장 직관적이고 간단한 관계가 @OneToMany
+ @ManyToOne
이라 기본 설명은 해당 예시로 진행했다. 이외에도 JPA에는 다대다 연관관계를 구성하기 위한 @ManyToMany
가 있다. 아주 간단한 예시로, 학생 엔티티 Student
와 학생이 듣는 수업 엔티티 Course
를 생각해보자. 한 명의 학생은 여러 개의 수업을 들을 수 있고, 한 개의 수업 역시 여러 명의 학생이 들을 수 있다. 여러 명의 학생이 여러 개의 수업을 들으므로, 두 엔티티의 연관 관계는 다대다라고 할 수 있다.
일반적으로 RDB는 다대다 연관관계를 "relation table"로 표현한다. 일대다 연관 관계에서 "다" 측의 테이블에 fk를 두는 방법이 아니라, 두 엔티티의 연관 관계 정보만을 담은 제 3의 테이블을 따로 두는 것이다.
JPA에는 기본적으로 이를 위한 어노테이션을 제공한다. @ManyToMany
이다. 앞선 학생-수업 예시를 엔티티 코드로 나타내보자.
@Entity
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 = new ArrayList<>();
// Constructors, getters, setters, etc.
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
// Constructors, getters, setters, etc.
}
@ManyToMany
는 어노테이션의 configuration에 따라 relation 테이블을 만들어주는 역할을 한다. @JoinTable
을 봐야한다.
name
으로 연관 테이블의 이름을 설정했다.joinColumns
로 현재 엔티티의 pk값을 참조할 relation 테이블의 fk 컬럼명을,inverseJoinColumns
로 연관 엔티티의 pk값을 참조할 relation 테이블의 fk 컬럼명을 설정했다.Course
엔티티의 mappedBy
로 엔티티의 필드값을 매핑했다.이렇게 되면, "student_course"
라는 이름을 가진 제3의 테이블이 새로 생성된다. Student
나 Parent
쪽에서 가진 List 객체에 원소를 더하거나 뺀 뒤 트랜잭션을 완료하면 JPA가 알아서 relation table에 관계 정보를 추가해준다.
@OneToMany
두개 쓰세요그. 러. 나.
relation table을 개발자가 직접 생성하고 설정해주지 못한 채 라이브러리에 숨겨져있다는건 꽤.. 아니 많이 별로다. relation table에 원하는 쿼리를 날릴 수도 없고, 해당 테이블에 추가 정보나 필드를 더하고 싶어도 할 수 없다. 그렇기 때문에 보통은 제 3의 relation table을 직접 만들고 각각의 엔티티에 @OneToMany
를 붙이는 방법을 사용할 수 있다.
원래 Student
, Course
의 @ManyToMany
를 @OneToMany
로 바꿔보자.
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "student")
private List<StudentCourse> studentCourses = new ArrayList<>();
// Constructors, getters, setters, etc.
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "course")
private List<StudentCourse> studentCourses = new ArrayList<>();
// Constructors, getters, setters, etc.
}
StudentCourse
라는 타입의 엔티티를 리스트 필드로 갖고있다. 해당 엔티티 클래스는 이렇게 생겼다.
@Entity
public class StudentCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
// Constructors, getters, setters, etc.
}
두 개의 fk를 갖고있다는 점을 제외하고는 처음 봤던 @OneToMany
, @ManyToOne
과 같은 구조이다. 이제 연관 관계 테이블을 개발자가 자유롭게 수정하고 설정할 수 있다.
정답은 없는듯. 연관 관계를 맺고 있는 어느 쪽의 엔티티에서도 어노테이션을 이용하기만 하면 "Many"측에서 fk 컬럼이 생성되긴 한다. 단방향이 맞다고 주장하는 사람도, 양방향이 맞다고 주장하는 사람도 있는데 역시 가장 많은건 두 쪽 모두 맞다, 상황에 따라 맞는 것을 선택하는게 좋다인듯.
many 측에서 one측을 불러올 일이 있으면 many측에 @ManyToOne
필드를 두면 좋다. 반대의 경우에 @OneToMany
를 두면 좋다. 뭐 사실 @ManyToOne
은 거의 반필수인 것 같긴 하지만ㅋㅋ
연관 관계 설정할 때의 순환 문제 이런건 코딩하면서 잘 피하면 되는거고. 일단 프로젝트에서는 User을 제외한 모든 관계에 대해 양방향을 적용했다. 이것과 관련해서 객체지향적으로 문제가 되는 부작용이 있을 것 같은데, 그것은 멘토님과 나중에 한 번 더 상의를 해보도록 하자!
https://www.nowwatersblog.com/jpa/ch6/6-4
https://velog.io/@yuseogi0218/JPA-%EB%8B%A4%EB%8C%80%EB%8B%A4-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84#manytomany-%EC%9D%98-%ED%95%9C%EA%B3%84