Spring

Spring Framework - 연관 관계 매핑

728x90

참조한 강의 : Spring & Hibernate For Beginners (www.udemy.com/course/spring-hibernate-tutorial/)

 

 

 

이번에는, Hibernate ORM 을 이용하여, DB 의 테이블과 엔티티 클래스간 관계 매핑을 하는 방법에 대해서 알아본다

 

관계형 데이터베이스 에서 테이블 설정시에, 각 테이블 간의 관계를 설정할 수 있는데,

두 테이블간의 관계는 아래 처럼 3가지로 설정된다. (여기에 추가로 단방향, 양방향이 존재하는 경우가 있다)

 

1) 1:1 관계 

2) 1:N 관계

3) N:M 관계 

 

 

각각의 관계를 Hibernate ORM 을 통해서 어떻게 구성하는지 학사정보 관리앱 예제를 만들어보면서 파악해본다.

(거창한 이름이 붙었을뿐 그냥 간단한 예제 수준이다)

 

 

-1) @OneToOne

: 1:1 관계를 설정할때는, @OneToOne 어노테이션을 사용한다.

제일 먼저, 강사 테이블과 강사에 대한 상세 정보를 담는 테이블 이렇게 두개를 만들어보자

 

- SQL

CREATE TABLE `instructor` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45DEFAULT NULL,
  `last_name` varchar(45DEFAULT NULL,
  `email` varchar(45DEFAULT NULL,
  `instructor_detail_id` int(11DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_DETAIL_idx` (`instructor_detail_id`),
  CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`REFERENCES `instructor_detail` (`id`ON DELETE NO ACTION ON UPDATE NO ACTION
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
 
CREATE TABLE `instructor_detail` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `youtube_channel` varchar(128DEFAULT NULL,
  `hobby` varchar(45DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
cs

 

그리고 두 테이블을 Hibernate ORM 에서도 사용할 수 있도록, 엔티티 클래스들을 만들자.

 

- Instructor.java

@Entity
@Table
public class Instructor {
    
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="instructor_detail_id")
    private InstructorDetail instructorDetail;
}
cs

 

 

 

- InstructorDetail.java

@Entity
@Table(name="instructor_detail")
public class InstructorDetail {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="youtube_channel")
    private String youtubeChannel;
    
    @Column(name="hobby")
    private String hobby;
}
cs

 

여기서 주목해야 할 것은, 두 테이블이 연결되기 위한 외래키가 있는 쪽에 연관관계매핑을 위한 어노테이션을 붙인다는 점 그리고 @JoinColumn 어노테이션을 이용해서, 외래키가 어디에 있는지 알려준다는점이 중요하다.

 

두 테이블의 구조를 보면, 두 테이블을 연결하기 위한 외래키가 instructor 테이블에 있고, instructor_detail_id 라는 이름으로 들어가있다. 그래서 @JoinColumn 부분에 외래키를 써줄때, 칼럼의 이름을 써준다. (자바에서 필드 이름이 아니라, 테이블에서 칼럼이름을 써야함)

 

그리고 @OneToOne 어노테이션에서 CASCADE 타입을 설정해줬는데, JPA 에서 제공하는 CASCADE 타입은 6개가 있다.

- ALL

- PERSIST

- MERGE

- REMOVE

- REFERESH

- DETACH

 

CASCADE 라는 것은, 연관된 테이블에도 동일한 연산을 적용하겠는가를 묻는 것이다.

예를들면, SQL 에서 CASCADE DELETE 하면 어떤 테이블이 지워졌을때, 그와 연결된 테이블도 지워진다. 

위의 6가지는 각각의 CASCADE 타입 중 뭘 지정할지를 정해주는 것이다.

(더 자세한 사항은 아래 참조)

www.baeldung.com/jpa-cascade-types

 

Overview of JPA/Hibernate Cascade Types. | Baeldung

A quick and practical overview of JPA/Hibernate Cascade Types.

www.baeldung.com

 

이렇게 설정한 연관관계 매핑을 기반으로 CRUD 를 하는 방법은 이전 포스팅에서 언급했던것 처럼, session 을 이용해서 처리해주면 된다. (자세한건 아래 참조)

sdy-study.tistory.com/197?category=995506

 

Spring Framework - CRUD Using Hibernate ORM

(본 포스팅 내용은 유데미의 Spring & Hibernate For Beginners (www.udemy.com/course/spring-hibernate-tutorial/) 강좌의 내용을 기반으로 공부한 것들을 요약한 포스팅이다) 이번에는, Hibernate ORM 을 이용..

sdy-study.tistory.com

 

위에서 작성한 1:1 매핑 방식은 단방향 매핑 방식이다.

Instructor 를 거쳐야만, InsturctorDetail 로 갈 수 있다는 의미이다.

역방향으로 가는건 전혀 설정해주지 않았다.

그러면 양방향으로 설정하려면 어떻게 해야하는가?

 

아래처럼, @OneToOne 이 붙지 않은 InstructorDetail 엔티티에 어노테이션을 추가하면된다.

 

- InstructorDetail.java

@Entity
@Table(name="instructor_detail")
public class InstructorDetail {
 
    @OneToOne(mappedBy="instructorDetail", cascade={CascadeType.REFRESH, CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST})
    private Instructor instructor;
}
cs

위에서 보는것처럼 @OneToOne(mappedBy="instructorDetail") 를 써주면된다.

mappedBy 에 들어가는 값은, 테이블값이 아니라, 상대 엔티티 클래스의 외래키 필드이름을 쓰면된다

Instructor 엔티티에는 외래키를 표시하는 필드가 private InstructorDetail instructorDetail 로 들어가있다.

그리고 cascade 타입을 CascadeType.ALL 이 아니라 다른 것들을 써준이유는, InstructorDetail 이 지워졌을때, Instructor 가 지워지지 않게 하기 위함이다.

 

 

 

-2) @OneToMany

: 1:N 관계를 설정할때는 @OneToMany 어노테이션을 쓴다.

이와반대로 N:1 관계를 설정할때는 @ManyToOne 어노테이션을 쓰면 된다.

 

이번에는, 강사와 강의의 관계를 생각해보자.

한명의 강사가 다수의 강의를 맡아서 일하는 경우를 생각해보자.

(물론 한강의에 꼭 한명만 있어야 되는것은 아니긴 하다, 그냥 편의상 한명으로 취급)

그리고 이경우는 양방향으로 볼 수 있다.

강사 정보를 조회하면서 강의 내역을 볼 수 있고,

강의 정보를 조회하면서 강사 정보를 볼 수 있기 때문이다.

 

- SQL

CREATE TABLE `course` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `title` varchar(128DEFAULT NULL,
  `instructor_id` int(11DEFAULT NULL,
  
  PRIMARY KEY (`id`),
  
  UNIQUE KEY `TITLE_UNIQUE` (`title`),
  
  KEY `FK_INSTRUCTOR_idx` (`instructor_id`),
  
  CONSTRAINT `FK_INSTRUCTOR` 
  FOREIGN KEY (`instructor_id`
  REFERENCES `instructor` (`id`
  
  ON DELETE NO ACTION ON UPDATE NO ACTION
ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1;
cs

@OneToOne 에서 만든 Instructor, InstructorDetail 테이블은 그대로 두고, Course 테이블만 위와 같이 새로 만들어준다.

 

그리고 Course 엔티티 클래스를 만들고, Instructor 엔티티 클래스를 다음과 같이 수정한다.

 

- Course.java

@Entity
@Table(name="course")
public class Course {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="title")
    private String title;
    
    @ManyToOne(cascade={CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinColumn(name="instructor_id")
    private Instructor instructor;
}
cs

Course 엔티티 클래스에서 @OneToMany 가 아니라 @ManyToOne 을 쓴 이유는 Course 가 1:N 관계에서 N 에 해당하기 때문이다.

그리고 Cascade 타입에서 DELETE 를 지운이유는, Course 가 지워진다고 강사가 지워져서는 안되기 때문이다.

 

 

 

- Instructor.java

@Entity
@Table(name="instructor")
public class Instructor {
    
    @OneToMany(mappedBy="instructor",
               cascade={CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    private List<Course> courses;
    
    public void addCourse(Course course) {
        if (courses == null) courses = new ArrayList<>();
        
        courses.add(course);
        
        course.setInstructor(this);
    }    
}
cs

여기서도 마찬가지로, mappedBy 를 사용했는데 Course 엔티티 클래스에 정의한 외래키가 있는 필드와 연결시켰으며, CASCADE 타입을 DELETE 만 제거한것은, 강사가 지워졌을때, 강의가 지워지지 않게 하기 위함이다.

 

 

위의 경우는 양방향으로 구성했지만, 이번엔 @OneToMany 를 단방향으로 구성해보자

예를들어서, 강의 테이블과 강의에 대한 리뷰를 담은 테이블이 있다고 쳐보자

그러면, 1:N 관계에서 강의가 1 이고, 리뷰가 N 이 된다.

 

먼저 리뷰 테이블을 만들어보자

 

- SQL

CREATE TABLE `review` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `comment` varchar(256DEFAULT NULL,
  `course_id` int(11DEFAULT NULL,
 
  PRIMARY KEY (`id`),
 
  KEY `FK_COURSE_ID_idx` (`course_id`),
 
  CONSTRAINT `FK_COURSE` 
  FOREIGN KEY (`course_id`
  REFERENCES `course` (`id`
 
  ON DELETE NO ACTION ON UPDATE NO ACTION
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
cs

 

- Review.java

@Entity
@Table(name="review")
public class Review {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="comment")
    private String comment;
}
cs

 

 

- Course.java

@Entity
@Table(name="course")
public class Course {
 
    @OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
    @JoinColumn(name="course_id")
    private List<Review> reviews;
    
    public void addReview(Review review) {
        if (reviews == null) reviews = new ArrayList<>();
        reviews.add(review);
    }    
}
cs

 

여기서 Course 와 Instructor 관계와는 한가지 다른점은

Course 와 Instructor 관계는 N:1 에서 N 인쪽이 @JoinColumn 을 썼지만, 

Review 와 Course 와의 관계에서는 1인 Course 에서 @JoinColumn 을 썼다.

 

왜 이런 차이가 생긴걸까?

 

 

 

 

* 이것은 연관관계의 주인에 대해서 알아야한다.

 

객체와 테이블의 차이점은, 객체의 경우 다른 연관된 객체의 데이터를 얻기 위해서는, 그 객체의 메모리를 참조해야한다.

그러나, 테이블의 경우 서로 다른 두 테이블이 연관관계를 맺고 있을때, 상대방의 정보를 얻기위해서는 외래키를 이용해서 JOIN 연산을 하면 얻어올 수 있다.

 

테이블의 경우, 외래키를 이용해서 두 테이블을 양방향으로 조회할 수 있지만,

-> (A <-> B)

객체의 경우, 양방향이 없다. 서로 다른 두 객체가 서로를 참조하게 해서 양방향처럼 보이게 하는것 뿐이다. 즉, 단방향이 2개 있는것이다.

-> (A -> B) , (A <- B)

 

이런 객체와 테이블의 차이점 때문에, 객체와 테이블을 서로 매핑할때 고려해야될 사항이, 외래키를 누가 관리하게 할것인가? 이다.

두 객체 A, B 가 있을때, 두 객체 중 누가 외래키와 매핑되서 외래키를 관리해야할까?

이 둘 중 하나를 정해서 외래키를 관리하는 쪽을 연관관계의 주인이라 부른다.

 

주인으로 설정된 부분은 DB 의 연관관계와 매핑되서 외래키를 관리하고, 다른 한쪽은 읽기만 가능하게된다.

외래키를 관리하는쪽 즉, 주인은 @JoinColumn 을 써주고, 그렇지 않은쪽은 mappedBy 를 쓰는것이다.

(단방향일때는 mappedBy 안써도됨)

 

Course 와 Review 의 관계에서는 연관관계의 주인이 Course 이고 (강의 없이 강의에 대한 리뷰가 있는것은 아무런 의미가 없음) 종속된 대상이 Review 이다. 그래서 @JoinColumn 을 Course  엔티티에 정의한것이고, Course 와 Instructor 의 관계에서는 Course 를 주종관계에서 '주'로 잡았기 때문에 Course 에 @JoinColumn 을 작성한 것이다.

 

개발하는 목적과 문맥에 따라서 주종관계에 변동사항이 있을 수 있다.

더 자세한 사항은 아래 참조

velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

 

[JPA] 연관관계 매핑 기초 #2 (양방향 연관관계와 연관관계의 주인)

이번 글에서는 에 대해 알아보겠습니다. 이 시리즈 글은 김영한 님의 강의, 책을 보고 적은 것임을 알려드립니다. (강추) 오타 및 피드백 환영합니다. 양방향 연관관계 이전 글에서는 회원에서

velog.io

 

 

 

-3) @ManyToMany

: N:M 의 관계에서는 @ManyToMany 어노테이션을 사용한다.

@ManyToMany 관계에서는 @JoinColumn 이 아니라 @JoinTable 을 사용한다.

 

이번에는 학생과 강의의 관계를 생각해보자

둘의 관계는 N:M 으로 볼 수 있다.

 

먼저 학생 테이블과 학생과 강의의 Join Table 을 생성해 준다

 

- SQL 

DROP TABLE IF EXISTS `student`;
 
CREATE TABLE `student` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45DEFAULT NULL,
  `last_name` varchar(45DEFAULT NULL,
  `email` varchar(45DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
 
DROP TABLE IF EXISTS `course_student`;
 
CREATE TABLE `course_student` (
  `course_id` int(11NOT NULL,
  `student_id` int(11NOT NULL,
  
  PRIMARY KEY (`course_id`,`student_id`),
  
  KEY `FK_STUDENT_idx` (`student_id`),
  
  CONSTRAINT `FK_COURSE_05` FOREIGN KEY (`course_id`
  REFERENCES `course` (`id`
  ON DELETE NO ACTION ON UPDATE NO ACTION,
  
  CONSTRAINT `FK_STUDENT` FOREIGN KEY (`student_id`
  REFERENCES `student` (`id`
  ON DELETE NO ACTION ON UPDATE NO ACTION
ENGINE=InnoDB DEFAULT CHARSET=latin1;
cs

 

그리고 Course 엔티티를 수정하고 Student 엔티티를 생성한다

 

- Course.java

@Entity
@Table(name="course")
public class Course {
    
    @ManyToMany(fetch=FetchType.LAZY, cascade={CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="course_student", joinColumns=@JoinColumn(name="course_id"), inverseJoinColumns=@JoinColumn(name="student_id"))
    private List<Student> students;
    
}
cs

 

- Student.java

@Entity
@Table(name="student")
public class Student {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private int id;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    @ManyToMany(fetch=FetchType.LAZY, cascade= {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name="course_student", joinColumns=@JoinColumn(name="student_id"), inverseJoinColumns=@JoinColumn(name="course_id"))
    private List<Course> courses;
}
 
cs

 

@ManyToMany 관계에서는 @JoinColumn 대신 @JoinTable 을 쓰는데,

여기에 작성하는 name 값은 앞서 만들었던 course_student 라는 Join Table 의 이름을 적으면 되고

이 테이블에는 두 테이블을 연결시킬 외래키값들이 들어있다.

그리고 @JoinTable 에 joinColumns 속성은 현재 엔티티가 Join Table 과 연결되기 위해 매칭 시킬 키 값이고, inverseJoinColumns 는 이름에서 알 수 있듯, 상대 엔티티가 가진 키값을 의미한다

Course 의 경우 joinColumns 가 course_id 이고, inverseJoinColumns 가 student_id 이다.

Student 의 경우 joinColumns 가 student_id 이고, inverseJoinColumns 가 course_id 이다.

 

 

 

여기까지 연관관계 매핑에 대해서 알아보았다.

다음은 AOP 에 대해서 알아본다.

 

 

- References)

1) Cascade Types : www.baeldung.com/jpa-cascade-types

2) 연관관계 매핑 : velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

 

 

 

 

 

 

 

 

728x90