Relations (M : N 관계)
1. ManyToMany
1) Entity 관계 M : N 관계
ex)
게시글과 좋아요 관계
- 하나의 게시글은 여러 사용자에게 좋아요를 받는다. + 한 명의 사용자는 여러 게시글에 좋아요를 남길 수 있다.
강의와 학생의 관계
- 한 학생은 여러 강의를 듣는다. + 한 강의에는 여러 학생이 참석한다.
- 이러한 M : N 관계를 표현하기 위해서 양쪽 테이블의 PK를 외래키로 갖는 새로운 테이블을 별도로 만들게 된다.
2) Entity 만들어보기
- Student
// Student Entity class
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String phone;
private String email;
@ManyToMany
@JoinTable(name = "attending_lectures")
private final List<Lecture> attending = new ArrayList<>();
}
- Lecture
// Lecture Entity class
@Entity
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String name;
@Setter
private String day;
@Setter
private Integer startTime;
@Setter
private Integer endTime;
@ManyToMany(mappedBy = "attending")
private final List<Student> students = new ArrayList<>();
}
- 이렇게 만들 때 @ManyToMany 어노테이션을 활용한다. 이때 mappedBy 속성으로 어떤 테이블과 조인하는지 명확하게 알려줘야한다. (mappedBy에는 내가 설정해둔 필드 이름으로 넣는다!! )
- @JoinTable : Join Table의 형태를 조정하기 위한 용도.
3) 저장해보기 (repository)
: 간단하게 controller에서 모두 구현했다 (따로 서비스 두지 않음)
@RestController
@RequestMapping("school")
@RequiredArgsConstructor
public class SchoolController {
private final StudentRepository studentRepository;
private final LectureRepository lectureRepository;
@GetMapping("many-to-many")
public String test() {
// 학생1
Student alex = Student.builder()
.name("alex")
.build();
// 학생1 저장
alex = studentRepository.save(alex);
// 학생2
Student brad = Student.builder()
.name("brad")
.build();
// 학생2 저장
brad = studentRepository.save(brad);
// 강의1
Lecture jpa = Lecture.builder()
.name("jpa")
.startTime(9)
.endTime(16)
.build();
// 강의1 저장
jpa = lectureRepository.save(jpa);
// 강의2
Lecture spring = Lecture.builder()
.name("spring boot")
.startTime(9)
.endTime(16)
.build();
// 강의2 저장
spring = lectureRepository.save(spring);
//학생1의 수강강의에 강의1 추가
alex.getAttending().add(jpa);
// 강의2의 수강학생에 학생1, 2 추가
spring.getStudents().add(alex);
spring.getStudents().add(brad);
// 학생1 다시 저장
studentRepository.save(alex);
// 강의2 다시 저장
lectureRepository.save(spring);
return "done";
}
}
2. Join Table에 속성 추가
1) @JoinTable에 속성 추가가 가능하다.
- joinColumns = @JoinColumn(name = " ") : Join Table의 나를 가르키는 FK를 설정한다.
- inverseJoinColumns= @JoinColumn(name=" "): Join Table의 관계를 맺는 상대방을 가르키는 FK의 설정
- 즉, 서로의 PK가 FK가 되면서 컬럼의 이름을 직접 지정한다.
// Student Entity class
@Entity
@NoArgsConstructor
@AllArgsConstructor
// 테이블의 이름을 설정하고 싶을 때 (그 외의 기능도 많음)
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String firstName;
@Setter
// 컬럼의 이름을 설정하고 싶을 때 (그 외의 기능도 많음)
@Column(name = "last_name")
private String lastName;
@ManyToMany
// Join Table의 모습을 정의하고 싶을 때
@JoinTable(
name = "attending_lectures",
// Join Table의 나를 가르키는 FK의 설정
joinColumns = @JoinColumn(name = "student_id"),
// Join Table의 관계를 맺는 상대방을 가르키는 FK의 설정
inverseJoinColumns = @JoinColumn(name = "lecture_id")
)
private final List<Lecture> attending = new ArrayList<>();
}
// Lecture Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String name;
@Setter
private String day;
@Setter
private Integer startTime;
@Setter
private Integer endTime;
// 상세설정은 mappedBy가 가르키는 속성에 따른다.
@ManyToMany(mappedBy = "attending")
private final List<Student> students = new ArrayList<>();
}
** 그러나 실무에서는! @ManyToMany를 가지고는 관계설정이 어려울 수 있다.
- 연결 테이블이 단순 연결만이 아니라, 여러 가지 추가 속성이 들어갈 수 있다.. (고객과 상품 사이의 주문정보 테이블에는 배송지정보와 같은 정보가 추가될 수 있다.)
2) 연결 테이블을 엔티티로 만들기
- 이를 극복하기 위해 중간 연결 테이블을 엔티티로 만들고 1:N관계로 서로 연결해준다.
ex) Student --- AttendingLectures --- Lecture 요렇게 연결해보았다. + AttendingLectures에 중간고사점수와 기말고사점수 컬럼을 추가해주었다.
// Student Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String phone;
private String email;
@OneToMany(mappedBy = "student")
private final List<AttendingLectures> attendingLectures = new ArrayList<>();
}
// Lecture Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String name;
@Setter
private String day;
@Setter
private Integer startTime;
@Setter
private Integer endTime;
@OneToMany(mappedBy = "lecture")
private final List<AttendingLectures> attendingStudents = new ArrayList<>();
}
// AttendingLectures Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "attending_lectures")
public class AttendingLectures {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@Setter
@ManyToOne
@JoinColumn(name = "lecture_id")
private Lecture lecture;
@Setter
private Integer midTermScore;
@Setter
private Integer finalsScore;
}
- 이렇게 해서 정보 회수를 해보자. (alex는 jpa / brad는 spring을 듣는다고 저장한 상태)
// SchoolController class
@GetMapping("many-to-many-get")
public String manyToManyGet() {
Student alex = studentRepository.findById(1L).get();
for (AttendingLectures attending : alex.getAttendingLectures()) {
log.info("{} listens {}", alex.getName(), attending.getLecture().getName());
}
Lecture spring = lectureRepository.findById(2L).get();
for (AttendingLectures attending : spring.getAttendingStudents()) {
log.info("{} listens {}", attending.getStudent().getName(), spring.getName());
}
for (AttendingLectures attendingLectures : alex.getAttendingLectures()) {
attendingLectures.setMidTermScore(80);
attendingLectures.setFinalsScore(80);
attendingLectureRepository.save(attendingLectures);
}
return "done";
}
>> 결과는 이렇게 나오고
AttendingLectures에 요렇게 점수 저장, 짝 저장이 되어있다.
3. 영속성 전이 - Cascade
1) 연관계의 주인
- 일반적인 상황에서 @OneToMany -와 함께 @ManyToOne으로 양방향 관계로 활용하게 되는데 > 이 때 mappedBy를 써서 관계의 주인이 누구인지를 지정하게 된다. 그니까 By절에 붙은 속성을 가지고 있는 엔티티가 그 둘의 관계의 주인이라는 뜻.
ex) Student --- Instructor 로 새로운 관계를 만들어 보자.
- Instructor는 여러 명의 Student의 어드바이저를 담당한다.
// Instructor Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Instructor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String name;
// 교수님 1 : 학생들 N의 관계 (advisor 속성을 갖고있는 Student가 이 관계의 주인이라는 뜻)
@OneToMany(mappedBy = "advisor")
private final List<Student> advisingStudents = new ArrayList<>();
}
- Student마다 한 명의 Advisor 교수님이 있다.
// Student Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String phone;
private String email;
// 학생들 N : 교수님 1의 관계
@ManyToOne
@Setter
@JoinColumn(name = "advisor_id")
private Instructor advisor;
@OneToMany(mappedBy = "student")
private final List<AttendingLectures> attendingLectures = new ArrayList<>();
}
조금 헷갈릴 수 있다. mappedBy절에 붙는 속성의 이름값은 연관된 엔티티에서 그 객체의 필드의 이름이다.
그래서 Student 엔티티에서 Instructor객체 필드를 advisor로 지정했다 >> 그래서 Instructor 엔티티에서 advisingStudents를 @OneToMany붙일 때 mappedBy절에 "advisor"로 지정한 것이다. 즉, 이 advisor 속성을 가지고 있는 객체는...?
=> Student 엔티티라는 말! 즉, Student가 주인인 관계이다.
> 이런 경우에 Instructor 객체에 advisingStudents에 여러 Student 객체를 넣고 save한다고 해서 저장이 되는 것이 아니다!
@OneToMany & @ManyToOne Entity 둘의 관계에서 데이터의 주도권은 @ManyToOne으로 표현되는 FK를 갖는 Student이기 때문이다.
그래서 데이터를 저장하고자 할 때 Instructor 객체를 Student에 전달하는 방식으로 만들어야 한다.
휴... 이렇게 되면 얼마나 귀찮은가..........!!!!!!!! 매번 주인관계를 생각해야 하고..
이럴 때 쓸 수 있는 것이 영속성 전이이다.
2) CascadeType
- CascadeType.PERSIST : 저장 시 함께 반영
// SchoolController class
@GetMapping("cascade-persist")
public String cascadePersist() {
//강사를 만들고,
Instructor instructor = Instructor.builder()
.name("Issac Newton")
.build();
//여러 학생을 만들고,
Student alex = Student.builder()
.name("Alex")
.advisor(instructor)
.build();
Student brad = Student.builder()
.name("Brad")
.advisor(instructor)
.build();
//강사의 지도학생으로 등록한다.
instructor.getAdvisingStudents().add(alex);
instructor.getAdvisingStudents().add(brad);
instructorRepository.save(instructor);
return "done";
}
// Instructor Entity class
@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Instructor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String name;
@OneToMany(mappedBy = "advisor", cascade = CascadeType.PERSIST)
private final List<Student> advisingStudents = new ArrayList<>();
}
> 원래라면 이렇게 해도 학생은 저장이 되지 않는 상황이지만,
Cascade 옵션으로 바로 학생이 저장된다.
SQL문을 확인해보면 insert into student가 두개 있는걸 볼 수 있다.
- CascadeType.REMOVE : 삭제 시 함께 반영
위처럼 저장이 되어 있을 때 삭제를 한번 해보겠다.
// SchoolController class
@GetMapping("cascade-remove")
public String cascadeRemove() {
instructorRepository.deleteById(1L);
return "done";
}
// Instructor
@OneToMany(mappedBy = "advisor", cascade = CascadeType.REMOVE)
private final List<Student> advisingStudents = new ArrayList<>();
}
이렇게 확인해보면 instructor가 삭제되기 전에 student가 삭제된다.
이런식으로 영속성 전이를 활용해 주인이 아닌 엔티티에서도 추가/수정/삭제가 가능하다.
예전에 정리해놨던 페이지도 첨부하겠다. (용어가 연관관계 주인 말고 부모 엔티티와 자식 엔티티로 설명이 되어있다.
주인이 부모일거 같지만 아니다. 1:N에서 1이 부모고, N이 자식이다.)
https://hehesim.tistory.com/72
- 영속성 컨텍스트 라이프사이클
https://hehesim.tistory.com/61
'Programming > Spring, SpringBoot' 카테고리의 다른 글
[JPA] 영속성 컨텍스트 (Persistence Context) (0) | 2024.02.05 |
---|---|
@Query (1) | 2024.02.01 |
[JPA] MappedSuperclass / JpaAuditing (0) | 2024.01.31 |
OAuth2 - Kakao Login 구현 (1) | 2024.01.30 |
OAuth2 - 소셜 로그인 (0) | 2024.01.30 |