연관관계 매핑
📌 JPA 연관관계 매핑을 통해 데이터베이스 테이블 간의 관계를 객체 지향적으로 표현하여 엔티티 클래스들 간의 관계를 설정한다. JPA를 통해 연관관계를 매핑하면 SQL을 직접 작성하지 않고도 객체 간의 관계를 활용하여 쉽게 데이터를 조회하고 조작할 수 있다.
1 : N 단방향
📌 한 엔티티가 @OneToMany를 통해 여러 엔티티와 관계를 맺는 경우를 말한다. 이 경우 연관관계의 주인은 1에서 가지고 있다.
- 1의 Entity가 외래 키(FK)를 관리한다. (연관관계의 주인)
- DB 입장에서는 항상 외래 키가 N 쪽에 위치해야 한다. (설계상 불가)
- 1(Company)이 N(Tutor)의 외래 키를 수정해야 한다.
더보기
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
}
@Entity
@Table(name = "company")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "company_id")
private List<Tutor> tutors = new ArrayList<>();
public Company() {
}
public Company(String name) {
this.name = name;
}
public List<Tutor> getTutors() {
return tutors;
}
}
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("entity");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
// Tutor 생성 및 persist
Tutor tutor = new Tutor("wonuk" );
em.persist(tutor);
// Company 생성 및 persist
Company company = new Company("sparta");
// Tutor 테이블에 추가
company.getTutors().add(tutor);
em.persist(company);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
} finally {
em.close();
}
emf.close();
}
}
@JoinColumn 미사용
@Entity
@Table(name = "company")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany
// @JoinColumn(name = "company_id") 주석처리
private List<Tutor> tutors = new ArrayList<>();
public Company() {
}
public Company(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Tutor> getTutors() {
return tutors;
}
}
- 실행결과
- 기존 테이블 삭제 후 실행(충돌 방지)
- @JoinColumn 을 사용하지 않으면 중간 테이블 방식을 사용한다.
- 사용 필수
1 : N 양방향
📌 양방향 연관 관계는 하나의 엔티티가 다른 엔티티와 관계를 맺고 그 반대 방향에서도 서로 참조가 가능하도록 설정한 관계이다.
- Tutor의 참조용 객체 Company는 읽기 전용 매핑이어야 한다.
- 만약, 양쪽에서 수정이 가능하면 예측이 불가능해진다.
- 1:N 양방향 연관관계의 주인은 Company 이다.
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "company_id", insertable = false, updatable = false)
private Company company;
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
- 연관관계의 주인이 되지 않도록 insertable = false, updatable = false 설정
- 1:N 단방향과 같은 이유로 N:1 양방향을 쓰면된다.
1 : 1 단방향
📌 두 Entity가 @OneToOne 을 통해 서로 관계를 맺는 경우를 말한다.
- 외래 키(FK)의 주인을 선택할 수 있다.(Tutor로 가정)
- 외래 키에 유니크 제약조건이 필요하다.(1:1)
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Company company;
@OneToOne
@JoinColumn(name = "address_id", unique = true)
private Address address;
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
- 실행결과
- 기존 테이블 삭제 후 실행(충돌 방지)
대상 테이블에 외래 키
- Tutor Entity의 Address로 Address 테이블의 tutor_id는 관리하지 못한다.
- 애초에 JPA가 지원하지 않는다.
1 : 1 양방향
- N:1 양방향 연관관계와 유사하다.
- 외래 키(FK)가 있는 곳이 연관관계의 주인
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 읽기 전용
@OneToOne(mappedBy = "address")
private Tutor tutor;
}
- 실행결과는 N:1의 양방향과 같이 읽기 전용으로 동작한다.
대상 테이블에 외래 키 양방향
- 1:1연관관계에서 자신이 가지고 있는 외래 키는 자신이 관리해야 한다.
- Tutor Entity의 Address 는 참조(읽기 전용)객체 이다.
1 : 1 연관관계 외래 키
📌 1:1 연관관계에서 외래 키는 양쪽 모두가 관리할 수 있다.
- 1:1 연관관계 외래 키
- 둘중 어떤 테이블을 사용해도 무방하다.
- 단, 테이블은 한번 만들어지면 변경이 어렵다.
- 요구사항 변경
- 한명의 Tutor가 여러개의 Address를 가질 수 있다.
- 기존 테이블
수정 테이블
- UNIQUE 제약조건만 지우면 된다.
- N:1 연관관계로 자연스럽게 변경이 가능하다.
- 단, 양방향으로 만들어야한다.
개발자 관점
- Tutor 테이블에서 접근할 확률이 높으니 Tutor 테이블이 외래 키(FK)를 가지면 편하다.
- 성능상 이점이 생긴다.
- 명확한 1:1 연관관계라면 해당 방법을 선택하면 된다.
- 결국 여러가지 방법 중 장점과 단점을 비교하여 선택하면 된다.
외래 키 위치 장단점
- 주 테이블
- 장점
- JPA로 객체 지향적인 개발이 가능해진다.
- 주 테이블만 조회해도 대상 테이블을 조회할 수 있다.
- 단점
- 대상 테이블에 값이 없다면 null이 허용된다.
- 데이터 무결성을 지키지 못한다.
- 대상 테이블의 데이터를 참조하기 때문에 삭제될 때 외래 키 값을 처리하는 관리 필요
- 대상 테이블에 값이 없다면 null이 허용된다.
- 장점
- 대상 테이블
- 장점
- 데이터베이스 무결성 보장
- 주 테이블과 대상 테이블의 연관관계 변경 시 테이블 구조가 유지된다.
- 단점
- 조회 성능이 떨어진다.
- 연관관계 매핑 설정이 복잡하다.
- 지연 로딩으로 설정해도 즉시 로딩된다.(중요)
- 장점
N : M 연관관계
📌 두 Entity가 @ManyToMany를 통해 서로 다수의 관계를 가진다.
- 조심해서 사용해야 한다.
N : M 단방향
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Company company;
@OneToOne
@JoinColumn(name = "address_id", unique = true)
private Address address;
@ManyToMany
@JoinTable(
name = "tutor_language",
joinColumns = @JoinColumn(name = "tutor_id"),
inverseJoinColumns = @JoinColumn(name = "language_id")
)
private List<Language> languages = new ArrayList<>();
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
- 실행결과
- 기존 테이블 삭제 후 실행(충돌 방지)
N : M 양방향
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "languages")
private List<Tutor> tutors = new ArrayList<>();
}
@OneToOne 양방향 처럼 동작하지만 중간 테이블이 생성된다.
N : M 매핑의 문제점
📌 @ManyToMany로 N:M 연관관계 설정을 하게되면 편리해 보이지만 실제로 사용하기 까다롭다.
- 실제 설계에서는 level, license 와 같은 추가적인 데이터가 필요하다.
- @ManyToMany에서 사용할 수 없다.
- 중간 테이블이 숨겨져 있어서 생각하지 못한 SQL Query가 실행된다.
- tutor_id, language_id 를 묶어서 PK로 설정된다.
- PK가 종속적이면 사이드 이펙트가 생긴다.
- PK 값은 비지니스적으로 의미없는 Long값으로 설정하는것이 유연성에 좋다.
문제점 해결
- 중간 테이블을 실제 Entity로 만들어서 관리하면 된다.
- @ManyToMany
- @OneToMany
- @ManyToOne
@Entity
@Table(name = "tutor_language")
public class TutorLanguage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "tutor_id")
private Tutor tutor;
@ManyToOne
@JoinColumn(name = "language_id")
private Language language;
private Integer level;
private String license;
}
@Entity
@Table(name = "language")
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "language")
private List<TutorLanguage> tutorLanguages = new ArrayList<>();
}
@Entity
@Table(name = "tutor")
public class Tutor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Company company;
@OneToOne
@JoinColumn(name = "address_id", unique = true)
private Address address;
@OneToMany(mappedBy = "tutor")
private List<TutorLanguage> tutorLanguages = new ArrayList<>();
public Tutor() {
}
public Tutor(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
- 실행결과
- 기존 테이블 삭제 후 실행(충돌 방지)
- <class>org.example.entity.TutorLanguage</class> **주석제거
'DB 접근 > JPA ( Java Persistence API )' 카테고리의 다른 글
Proxy (0) | 2025.01.14 |
---|---|
상속관계 매핑 (0) | 2025.01.13 |
[JPA] Spring Data JPA (0) | 2025.01.07 |
[JPA] 연관관계 Mapping (0) | 2025.01.06 |
[JPA] Entity (1) | 2025.01.05 |