본문 바로가기

JPA/양방향연관관계

N:M(다 대 다) 매핑 3: 매핑테이블을 엔티티로 만들기

N:M 관계를 풀어낼 때 두 테이블을 엮기 위해 중간에 매핑테이블을 만들어야 한다.

그런데 이 때 매핑테이블을 @JoinTable과 @ManyToMany 어노테이션을 사용하여 만들면, 

개발 도중에 발생하는 상황들에 대해 유연하게 대처하기 힘들어진다.

따라서 돌발상황이 많은 실무에서는 매핑테이블을 엔티티로 승격시키는 것이 좋다. 

 


테이블 구조

Student:Subject는 N:M의 관계를 가지고 있고, 이를 매핑하기 위한 테이블로 StudentSubject를 사용하고 있다.

매핑테이블을 Entity로 사용했을 때의 장점을 살펴보기 위해 StudentSubject에 registrationDate 컬럼을 추가했다.

이번 시간에는 매핑테이블인 StudentSubject를 Entity로 만들어서 사용해볼 것이다!


StudentSubject.java

package hellojpa;

import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@Entity
public class StudentSubject {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "STUDENT_ID")
    private Student student;

    @ManyToOne
    @JoinColumn(name = "SUBJECT_ID")
    private Subject subject;

    @CreationTimestamp
    private LocalDateTime registrationDate;
}

 

StudentSubject:Student와 StudentSubject:Subject는 다 대 일(N:1)의 관계를 가진다.

따라서 N:1 관계 설정을 위해 @ManyToOne과 @JoinColumn을 사용해서 Student 및 Subject Entity와 매핑을 했다.
또한 registrationDate에는 @CreationTimestamp라는 어노테이션이 있다.
(이 어노테이션은 데이터가 DB에 저장될 때의 시간을 받아와서 값을 넣어준다.

LocalDateTime에만 사용가능하다. 더 궁금한 점은 직접 찾아보자~~~)

 

Student.java

package hellojpa;

import lombok.Data;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Data
public class Student {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int age;

    @ManyToOne
    @JoinColumn(name = "CLUB_ID", insertable = false, updatable = false)
    private Club club;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    Locker locker;

    @OneToMany(mappedBy = "student")
    private List<StudentSubject> studentSubjects = new ArrayList<>();
}

Subject.java

package hellojpa;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Subject {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "subject")
    private List<StudentSubject> studentSubjects = new ArrayList<>();
}

 

Student 및 Subject Entity에서는 @ManyToOne의 반대인 @OneToMany 어노테이션을 달아주고

mappedBy 옵션을 통해 연관관계 방향이 종이라는 것을 밝혔다.

(Entity 설명들이 잘 이해되지 않는다면 JPA/양방향연관관계의 글들을 정독하자!)

 

JpaMain.java

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {
            // 수학 과목 등록
            Subject math = new Subject();
            math.setName("수학");
            em.persist(math);

            // 한국사 과목 등록
            Subject history = new Subject();
            history.setName("한국사");
            em.persist(history);

            // codeMania 학생 등록
            Student codeMania = new Student();
            codeMania.setName("code-mania");
            codeMania.setAge(21);
            em.persist(codeMania);

            // codeLover 학생 등록
            Student codeLover = new Student();
            codeLover.setName("code-lover");
            codeLover.setAge(21);
            em.persist(codeLover);

            // codeMania 학생 수학 과목 수강
            StudentSubject studentSubject = new StudentSubject();
            studentSubject.setStudent(codeMania);
            studentSubject.setSubject(math);
            em.persist(studentSubject);

            // codeMania 학생 한국사 과목 수강
            StudentSubject studentSubject2 = new StudentSubject();
            studentSubject2.setStudent(codeMania);
            studentSubject2.setSubject(history);
            em.persist(studentSubject2);

            // codeLover 학생 한국사 과목 수강
            StudentSubject studentSubject3 = new StudentSubject();
            studentSubject3.setStudent(codeLover);
            studentSubject3.setSubject(history);
            em.persist(studentSubject3);

            tx.commit();
            System.out.println("codeMania가 수강 중인 과목 수 = " + codeMania.getStudentSubjects().size());
            System.out.println("수학을 수강 중인 학생 수 = " + math.getStudentSubjects().size());
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        em.close();
        emf.close();
    }
}

 

JpaMain에서는 Entity에 값을 넣은 후 em.persist() 메서드를 통해 DB에 저장하는 간단한 작업만 하므로 자세한 설명은 생략하겠다!

System.out.println 부분에 대한 설명은 아래쪽에 있으니 꼭 읽어보자!!!

코드를 실행하면 DB에는 아래와 같이 들어가게 된다.


System.out.println("codeMania가 수강 중인 과목 수 = " + codeMania.getStudentSubjects().size());
System.out.println("수학을 수강 중인 학생 수 = " + math.getStudentSubjects().size());

 

JpaMain.java의 이 코드에 주의하자! codeMania는 2개의 과목을 수강했고, 수학을 수강 중인 학생은 1명이다.

따라서 codeMania.getStudentSubjects().size()math.getStudentSubjects().size()는 각각 2와 1을 반환해야 한다.

하지만 둘 다 0이 나온다. 이유가 뭘까?
우리는 StudentSubject 타입의 객체를 통해 수강 과목을 DB에 저장했다.

다른 말로 codeManinamathstudentSubjects 필드는 전혀 건드리지 않았다.

따라서 DB에는 codeMania가 2개의 과목을 수강한다는 데이터가 저장되지만,
codeMania와 math의 studentSubjects List의 size는 0이다.

이러한 문제를 해결하기 위해 우리는 특별한 작업을 해줘야 한다!!! 이 부분은 다음 글에서 알아보겠다~~~~

 

이번 글에서는 매핑테이블을 Entity로 사용하여 N:M 관계를 매핑해보았다.

이에 따라 @ManyToMany@JoinTable을 사용하지 않고,

@ManyToOne@OneToMany를 사용하여 N:1 양방향매핑을 2번 진행하는 형태가 되었다.

실무에서는 이번 글에서 한 것과 같이 @ManyToMany@JoinTable은 사용하지 말고,

여러 상황에 유연하게 대처할 수 있도록 매핑 테이블을 엔티티로 사용하자!
(@ManyToMany@JoinTable을 통해 매핑한다면,
매핑테이블에 컬럼추가와 같은 변경사항들에 유연한 대처가 불가능하다.)

 

github 주소(branch: blog/ManyToManyWithEntity)

참고강의: 배달의 민족 개발팀장 김영한 강사님의 JPA 강의