@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이 허용된다.
데이터 무결성을 지키지 못한다.
대상 테이블의 데이터를 참조하기 때문에 삭제될 때 외래 키 값을 처리하는 관리 필요
대상 테이블
장점
데이터베이스 무결성 보장
주 테이블과 대상 테이블의 연관관계 변경 시 테이블 구조가 유지된다.
단점
조회 성능이 떨어진다.
연관관계 매핑 설정이 복잡하다.
지연 로딩으로 설정해도 즉시 로딩된다.(중요)
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;
}
}