참조한 강의 : 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(11) NOT NULL AUTO_INCREMENT,
`first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL,
`email` varchar(45) DEFAULT NULL,
`instructor_detail_id` int(11) DEFAULT 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(11) NOT NULL AUTO_INCREMENT,
`youtube_channel` varchar(128) DEFAULT NULL,
`hobby` varchar(45) DEFAULT 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
이렇게 설정한 연관관계 매핑을 기반으로 CRUD 를 하는 방법은 이전 포스팅에서 언급했던것 처럼, session 을 이용해서 처리해주면 된다. (자세한건 아래 참조)
sdy-study.tistory.com/197?category=995506
위에서 작성한 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(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) DEFAULT NULL,
`instructor_id` int(11) DEFAULT 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(11) NOT NULL AUTO_INCREMENT,
`comment` varchar(256) DEFAULT NULL,
`course_id` int(11) DEFAULT 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 을 작성한 것이다.
개발하는 목적과 문맥에 따라서 주종관계에 변동사항이 있을 수 있다.
더 자세한 사항은 아래 참조
-3) @ManyToMany
: N:M 의 관계에서는 @ManyToMany 어노테이션을 사용한다.
@ManyToMany 관계에서는 @JoinColumn 이 아니라 @JoinTable 을 사용한다.
이번에는 학생과 강의의 관계를 생각해보자
둘의 관계는 N:M 으로 볼 수 있다.
먼저 학생 테이블과 학생과 강의의 Join Table 을 생성해 준다
- SQL
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL,
`email` varchar(45) DEFAULT 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(11) NOT NULL,
`student_id` int(11) NOT 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
'Spring' 카테고리의 다른 글
Spring Framework - AOP Overview (0) | 2021.02.17 |
---|---|
Spring Framework - Layered Architecture (0) | 2021.02.16 |
Spring Framework - Eager, Lazy Loading (0) | 2021.02.11 |
Spring Framework - Persistence Context (0) | 2021.02.06 |
Spring Framework - CRUD Using Hibernate ORM (0) | 2021.02.04 |