Relations (M : N 관계)

2024. 2. 1. 21:48
728x90

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

 

[JPA] CascadeType 영속성 전이

** Cascade? 특정 엔티티를 영속 상태로 만들 경우, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 경우에 영속성 전이 사용. 자식 엔티티에 설정. @OneToMany(mappedBy="books", cascade=CascadeType.ALL) CASCADE OPT

hehesim.tistory.com

 

- 영속성 컨텍스트 라이프사이클

https://hehesim.tistory.com/61

 

[JPA] 영속성 컨텍스트 라이프사이클

https://hehesim.tistory.com/59 JPA (Java Persistence API) 기초/설정 * 개념 정리 1) JPA (Java Persistence API) : 자바 진영에서 ORM기술의 표준으로 자리잡은 인터페이스의 집합을 의미 2) ORM (Object Relational Mapping) : Java

hehesim.tistory.com

 

728x90

'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

BELATED ARTICLES

more