다양한 연관관계 매핑

식빵·2022년 1월 3일
0

JPA 이론 및 실습

목록 보기
8/17

🍀 개요

앞장에서는 N:1 연관관계 단방향, 양방향 매핑을 해봤다.
이번에는 N:1 뿐만 아니라 1:N, 1:1, M:N 연관관계를 매핑해보자.

여기서 1:NN:1 이랑 똑같지 않나? 라고 생각한다면 맞다.
다만 이전 장에서는 N:1 에서 왼쪽(N)이 연관관계의 주인이었다.

이번에 배울 1:N 연관관계 매핑에서는 왼쪽인 1 이 연관관계의 주인이다.
이번 장은 나올 1:1, M:N 모두 이런 식으로 왼쪽이 연관관계의 주인인 것으로 간주한다.




🍀 다대일 - N:1 (복습)


🐛 단방향 매핑

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}
@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;
}



🐛 양방향 매핑

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    // 연관관계 편의 메소드
    public void setTeam(Team team) {
        if(this.team != null) {
            this.team.getMembers().remove(this);
        }

        this.team = team;
        team.getMembers().add(this);
    }
}
@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

}




🍀 일대다 - 1:N


🐛 단방향 매핑

1 쪽을 연관관계의 주인 포지션에 두고 생각해보자.

@Entity
@Getter @Setter
public class Company {

    @Id @GeneratedValue
    @Column(name = "company_id")
    private Long id;

    private String name;

    private LocalDateTime found;

    @OneToMany
    @JoinColumn(name = "company_id")
    private List<Employee> employeeList = new ArrayList<>();

}
@Entity
@Getter @Setter
public class Employee { // "N"

    @Id
    @GeneratedValue
    @Column(name = "employee_id")
    private Long id;

    private String username;
}

1:N 에서 "1" 쪽의 테이블이 외래키를 갖지는 않지만 객체 참조관계에서
"1" 쪽이 외래키를 관리하게 된다. 그런데 이렇게 외래키를 관리하게 되면 성능상 이슈가 있다.
아래 코드를 실행해보자.


Employee employee = new Employee();
employee.setUsername("worker1");

Employee employee2 = new Employee();
employee2.setUsername("worker2");

Company company = new Company();
company.setFound(LocalDateTime.now());
company.setName("goodCompany");
company.getEmployeeList().add(employee);
company.getEmployeeList().add(employee2);

em.persist(employee);
em.persist(employee2);
em.persist(company);

콘솔에 아래같은 결과가 나온다.

Hibernate: 
    insert into employee (username, employee_id) values (?, ?)
Hibernate: 
    insert into employee (username, employee_id) values (?, ?)
Hibernate: 
    insert into company (found, name, company_id) values (?, ?, ?)
Hibernate: 
    update employee set company_id=? where employee_id=?
Hibernate: 
    update employee  set company_id=?  where employee_id=?

employee, employee2, company 가 모두 insert 되는 거 까지는 좋았지만,
employee, employee2 에 대한 update 쿼리가 각각 한번씩 실행된다.

이러는 이유는 외래키를 실제 갖고 있는 Employee 테이블의 엔티티가 아니라
Company 테이블의 엔티티가 외래키를 관리하기 때문이다.

외래키를 관리하지 않는 Employee 엔티티는 외래키에 대한 어떤 정보도 등록, 수정하지 못한다.
그래서 em.persist(employee)insert into employee (username, employee_id) values (?, ?) 을 통해서 외래키를 제외한 정보들만 저장하고,

실제 외래키를 관리하는 Company 엔티티를 저장할 때 Company.employeeList의 참조값을 확인해서 Employee 테이블에 있는 company_id 를 update 한다.

이런 헷갈리는 동작 방식 때문에 일대다 단방향 매핑은 별로 선호되지 않는다.
될 수 있으면 다대일 양방향 매핑으로 대체하자.



🐛 양방향 매핑

일대다 양방향 매핑은 존재하지 않는다. 정확히는 @OneToMany 는 연관관계의 주인이 될 수 없다. 애초에 데이터베이스 특성상 1:N의 경우 N쪽애 무조건 외래키가 있기 때문이다.
그래서 @ManyToOne 에는 애초에 "mappedBy" 속성이 없다.

하지만 좀 코드를 교묘(?)하게 짜면 일대다의 관계에서 양방향 매핑이 불가능한 건 아니다.

@Entity
@Getter
@Setter
public class Company {

    @Id @GeneratedValue
    @Column(name = "company_id")
    private Long id;

    private String name;

    private LocalDateTime found;

    @OneToMany
    @JoinColumn(name = "company_id")
    private List<Employee> employeeList = new ArrayList<>();

}
@Entity
@Getter
@Setter
public class Employee {

    @Id
    @GeneratedValue
    @Column(name = "employee_id")
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "company_id", updatable = false, insertable = false)
    private Company company;
}

updatable = false, insertable = false 을 사용해서 read-only 로 만들어버리면 되는 것이다.

하지만 이렇게 해도 위에 단방향 매핑에서 말한 단점은 여전히 존재한다.
그러니 될 수 있으면 다대일 양방향 매핑을 사용하자.




🍀 일대일 - 1:1

일대일 관계의 특징

  • 그냥해도 1:1 거꾸로 해도 1:1이다
  • 데이터베이스에서는 1:N 이나 N:1 에서는 N이 외래키를 갖지만 1:1에서는 둘 중 아무곳에 외래키가 있을 수 있다.
  • 테이블이 주 테이블이든, 대상 테이블이든 둘 중 하나만 외래키 있으면 OK

🐛 주 테이블 외래키

한명의 헬창(?)이 하나의 락커를 사용한다고 가정하자.
참고로 헬창이 주 테이블이고, 락커가 대상 테이블이다.

1. 단방향 매핑

@Entity
@Getter
@Setter
public class HealthFreak {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int muscularStrength;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;

}
@Entity
@Getter
@Setter
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = "locker_id")
    private Long id;

    private String password;

}

2. 양방향 매핑

@Entity
@Getter
@Setter
public class HealthFreak {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int muscularStrength;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;

}
@Entity
@Getter
@Setter
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = "locker_id")
    private Long id;

    private String password;

    @OneToOne(mappedBy = "locker")
    private HealthFreak freak;

}



🐛 대상 테이블 외래키


1. 단방향 매핑

대상 테이블에 외래키가 있고 주 테이블의 엔티티가 단방향으로 관리하는 건 불가능하다.
이전에 1:N 에서는 단방향 관계에서 대상 테이블에 외래키가 있는 매핑을 허용했지만 여기서는 불가능하다.

이럴 때는 그냥 단순하게 Locker (대상 테이블) 의 엔티티에 외래키 매핑 필드를 생성하자.



2. 양방향 매핑

@Entity
@Getter
@Setter
public class HealthFreak {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int muscularStrength;

    @OneToOne(mappedBy = "freak")
    private Locker locker;

}
@Entity
@Getter
@Setter
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = "locker_id")
    private Long id;

    private String password;

    @OneToOne
    @JoinColumn(name = "freak_id")
    private HealthFreak freak;
    
}




🍀 다대다 - N:M

참고: N:M 관계는 웬만해서는 안쓴다. 중간 테이블을 위한 엔티티를 새로이 생성하고 양쪽에서 1:N , M:1 로 해서 매핑을하는 게 일반적이다.

N:M 은 관계 데이터베이스 상에서 중계 테이블을 하나 두고 맺어진다.
하지만 객체에서는 그냥 서로 List나 Set 같은 타입으로 바로 참조가 가능하다.

얘네는 어떻게 할까?

일단 예제로는 대학생과 수업의 연관관계를 사용하겠다.


🐛 단반향 매핑

@Entity
@Getter
@Setter
public class Student {

    @Id @GeneratedValue
    @Column(name = "student_id")
    private Long id;

    private String name; // 이름

    @ManyToMany
    @JoinTable(name = "student_lecture", //연결 테이블명
            joinColumns = @JoinColumn(name = "student_id"),// 현재 엔티티와 매핑할 조인 컬럼 정보지정
            inverseJoinColumns = @JoinColumn(name = "lecture_id"))// 반대 쪽 엔티티와 조인할 컬럼 정보 지정
    private List<Lecture> lectures = new ArrayList<>();
}
@Entity
@Getter
@Setter
public class Lecture {

    @Id @GeneratedValue
    @Column(name = "lecture_id")
    private Long id;

    private String lectureName; // 강의명

}

JoinTable 컬럼 간략 설명

  • name = "student_lecture" : 연결 테이블명
  • joinColumns = @JoinColumn(name = "student_id") : 현재 엔티티와 매핑할 조인 컬럼 정보지정
  • inverseJoinColumns = @JoinColumn(name = "lecture_id") : 반대 쪽 엔티티와 조인할 컬럼 정보 지정

테스트 코드

Lecture lecture = new Lecture();
lecture.setLectureName("재료역학");
em.persist(lecture);

Student student = new Student();
student.setName("dailyCode");
student.getLectures().add(lecture);
em.persist(student);

콘솔 출력

Hibernate: 
    insert into lecture (lecture_name, lecture_id) values (?, ?)
Hibernate: 
    insert into student (name, student_id) values (?, ?)
Hibernate: 
    insert into student_lecture (student_id, lecture_id) values (?, ?)

db 결과



🐛 양방향 매핑

@Entity
@Getter @Setter
public class Lecture {

    @Id @GeneratedValue
    @Column(name = "lecture_id")
    private Long id;

    private String lectureName; // 강의명

    @ManyToMany(mappedBy = "lectures")
    private List<Student> students = new ArrayList<>();

}



🐛 중간 테이블 사용하기

현실적으로 @ManyToMany는 쓰는 건 지나치게 확장성이 떨어진다.
왜냐하면 @ManyToMany를 사용하면 중간 테이블이 자동으로 생성되는데, 이후로 우리가 중간 테이블에 컬럼을 추가하고 싶어도 자동 생성이 되는 것이라서 불가능하다.

그러니 될 수 있으면 중간 테이블을 위한 엔티티 클래스를 생성하고 해당 엔티티에 다대다 관계를 갖는 엔티티들을 1:다, 다:1 로 연관관계를 맺자.



@Entity
@Setter
@Getter
public class Student_Lecture { // 중간 테이블 매핑

    @Id @GeneratedValue
    @Column(name = "student_lecture_id")
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "lecture_id")
    private Lecture lecture;
    
}

@Entity
@Getter
@Setter
public class Student {

    @Id @GeneratedValue
    @Column(name = "student_id")
    private Long id;

    private String name; // 학번

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

@Entity
@Getter
@Setter
public class Lecture {

    @Id @GeneratedValue
    @Column(name = "lecture_id")
    private Long id;

    private String lectureName; // 강의명
    
}

등록 테스트

Student student = new Student();
student.setName("dailyCode");
em.persist(student);

Lecture lecture = new Lecture();
lecture.setLectureName("기하학");
em.persist(lecture);

Student_Lecture student_lecture = new Student_Lecture();
student_lecture.setLecture(lecture);
student_lecture.setStudent(student);
em.persist(student_lecture);

콘솔 결과

Hibernate: 
    insert into student (name, student_id) values (?, ?)
Hibernate: 
    insert into lecture (lecture_name, lecture_id) values (?, ?)
Hibernate: 
    insert into student_lecture (lecture_id, student_id, student_lecture_id) values (?, ?, ?)




🍀 참고

자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글