@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member() {
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
@JoinColumn FK의 이름을 설정한다. 생략할 경우 기본 네이밍 전략(테이블명_조인컬럼명)을 사용한다.
다대일 양방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member() {
}
public Member(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team() {
}
public Team(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member> getMembers() {
return members;
}
}
연관관계의 주인이 아닌 쪽에 mappedBy 속성을 준다. 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
일대다 단방향
일대다 단방향 연관관계는 JPA 2.0부터 지원한다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String name;
public Member() {
}
public Member(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
public Team() {
}
public Team(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void addMember(Member member) {
members.add(member);
}
public List<Member> getMembers() {
return members;
}
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team("teamA");
em.persist(team);
Member member = new Member("memberA");
em.persist(member);
team.addMember(member);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("member name = " + m.getName());
}
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
tx.commit();
emf.close();
}
}
일대다 단방향 연관관계를 매핑할 때는 @JoinColumn 을 명시해야한다. 그렇지 않으면 JPA는 기본적으로 중간 테이블을 만들어 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용한다.
상대 엔티티에 대한 참조는 Team만 가지고 있지만 실제 DB에서 FK는 Member가 가지고 있기 때문에 addMember() 를 호출할 때 Member 테이블에 FK를 설정하기 위한 update 쿼리가 추가적으로 날아간다.
성능도 성능이지만, 이렇게 예상치 못한 쿼리가 날아가는 것은 애플리케이션 관리 측면에서도 어려움을 야기한다.
따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 낫다.
일대다 양방향
일대다 양방향 매핑은 JPA 스펙상 지원하지 않는다. 일대다 단방향 매핑 반대편에 @JoinColumn 을 달아주고 insertable과 updateble 속성을 false로 주어서 양방향 매핑이 가능하도록 억지로 설정할 수는 있지만 일대다 단방향 매핑과 마찬가지의 이유로 사용하지 않는 것이 좋다.
일대일 연관관계
일대다, 다대일 연관관계에서는 항상 다쪽이 FK를 가진다.
하지만 일대일 연관관계에서는 어느쪽이 FK를 가지든 상관없다. 따라서 주 테이블(자주 사용하는 테이블, 이를테면 Member)에 외래 키를 둘 지, 대상 테이블에 외래 키를 둘 지 선택해야한다.
주 테이블에 외래 키를 두는 경우: 주 테이블 데이터만 긁어와도 대상 테이블의 데이터를 사용할 수 있어 애플리케이션 개발이나 성능 측면에서 우월하다.
대상 테이블에 외래 키를 두는 경우: 테이블 관계를 일대일에서 일대다로 변경할 때, 테이블 변경에 대한 부담이 적다. (항상 즉시 로딩)
단방향
@Entity
@Table(uniqueConstraints = { @UniqueConstraint(name = "UniqueLockerConstraint", columnNames = { "LOCKER_ID" }) })
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
public Member() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Locker getLocker() {
return locker;
}
public void setLocker(Locker locker) {
this.locker = locker;
}
}
@Entity
public class Locker {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "LOCKER_ID")
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
양방향일 경우 mappedBy 속성을 통해서 연관관계의 주인을 정해주면 된다.
주의할 점
FK를 들고 있지 않은 테이블에 매핑된 객체가 단방향 관계를 갖는 경우(위 예제에서 Locker 테이블이 Member의 FK를 가지고 있으나 Member가 단방향으로 Locker를 참조하는 경우)는 JPA에서 지원하지 않는다.
다대다 연관관계
데이터베이스에서는 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 @ManyToMany 애너테이션으로 다대다 연관관계를 만들면 JPA는 중간 테이블을 만드는 방식으로 관계를 풀어낸다.
Member - Member_Product - Product
Member_Product는 Member와 Product의 id를 외래키로 갖는 테이블.
하지만 실무에서는 이 연결 테이블에 추가적인 정보를 넣을 일이 많기 때문에 @ManyToMany 애너테이션을 사용하는 일은 거의 없다. 대신 연결 테이블을 엔티티로 승격시켜서, M:N 관계를 1:N, N:1 관계로 풀어낸다. (강의자료 OrderItem 참고)
상속관계 매핑
객체는 상속관계가 있지만 DB에는 그런게 없음. 슈퍼타입, 서브타입이라는 논리 모델링 기법이 객체의 상속과 유사하긴 하다.
이를 실제 물리 모델로 구현하는 방법에는 세 가지가 있다.
각각 테이블로 변환
통합 테이블로 변환
서브타입 테이블로 변환
JPA는 세 가지 방법을 모두 지원한다. 부모 클래스에 @Inheritance 애너테이션을 주고 strategy 속성을 주어 각 방법을 선택할 수 있다.
각각 테이블로 변환 (JOINED)
조인 전략을 사용한다. 하위 타입들을 모두 테이블로 만들고 조인을 통해 데이터를 가져온다. 인서트는 각각의 테이블에 일어난다.
가장 정규화된 모델링 방법. 외래키 참조 무결성 제약 조건을 적극 활용할 수 있다.(부모 테이블만 보고 조회 가능)
조회시 조인을 많이 사용하고 데이터 저장 시 인서트가 두 번 일어난다.
그러나 성능 측면에서 그렇게까지 나쁘지는 않다고 한다. 이 전략을 선택하는게 무난하다.
통합 테이블로 변환 (SINGLE_TABLE)
단일 테이블 전략. 하나의 테이블에 다 때려넣는다. JPA의 기본 전략이며 성능 측면에서 좋다.
@DiscriminatorColumn 애너테이션이 없어도 자동으로 DTYPE이 생성된다.
자식 엔티티가 매핑한 컬럼은 모두 null을 허용해주어야 한다.
테이블이 커진다면 오히려 성능이 떨어질 수도 있다.
단순한 데이터의 경우 선택하면 좋다.
서브타입 테이블로 변환 (TABLE_PER_CLASS)
상위 타입의 데이터를 서브 타입 테이블이 다 가지고 있는다. 상위 타입을 추상 클래스로 만들면 테이블이 생성되지 않고 하위 타입의 테이블만 생성된다.
인서트할 때는 단순하고 좋아보이지만 부모 타입으로 조회할 경우(em.find(Item.class)) 조회 쿼리가 굉장히 복잡하게 날아간다. → 쓰지말자
@DiscriminatorColumn 애너테이션을 사용하면 테이블에 DTYPE 컬럼이 생기는데, 하위 타입의 이름이 들어간다. name 속성으로 컬럼 이름을 바꿀 수 있다.
만약 칼럼에 들어가는 값을 하위 타입의 이름이 아니라 다른 이름으로 설정하고 싶으면 하위 타입에 @DiscromonatorValue 애너테이션을 추가하여 원하는 이름을 name 속성에 넘겨주면 된다.
Mapped Superclass
공통 매핑 정보가 필요할 때 사용한다. ex) BaseEntity ← 얘는 엔티티 아님
특정 필드가 모든 엔티티에서 중복되어서 뭔가 공통적으로 뽑아내고 싶을 때 사용한다.
공통 매핑 정보를 담은 클래스에 @MappedSuperclass 애너테이션을 붙여주고 정보를 받을 클래스가 이를 상속하면 된다.
다양한 연관관계
다대일 단방향
다쪽에서만 일의 참조를 가지고 있는 경우
@JoinColumn
FK의 이름을 설정한다. 생략할 경우 기본 네이밍 전략(테이블명_조인컬럼명)을 사용한다.다대일 양방향
연관관계의 주인이 아닌 쪽에
mappedBy
속성을 준다. 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.일대다 단방향
일대다 단방향 연관관계는 JPA 2.0부터 지원한다.
일대다 단방향 연관관계를 매핑할 때는
@JoinColumn
을 명시해야한다. 그렇지 않으면 JPA는 기본적으로 중간 테이블을 만들어 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용한다.상대 엔티티에 대한 참조는 Team만 가지고 있지만 실제 DB에서 FK는 Member가 가지고 있기 때문에
addMember()
를 호출할 때 Member 테이블에 FK를 설정하기 위한 update 쿼리가 추가적으로 날아간다.성능도 성능이지만, 이렇게 예상치 못한 쿼리가 날아가는 것은 애플리케이션 관리 측면에서도 어려움을 야기한다.
따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 낫다.
일대다 양방향
일대다 양방향 매핑은 JPA 스펙상 지원하지 않는다. 일대다 단방향 매핑 반대편에
@JoinColumn
을 달아주고 insertable과 updateble 속성을 false로 주어서 양방향 매핑이 가능하도록 억지로 설정할 수는 있지만 일대다 단방향 매핑과 마찬가지의 이유로 사용하지 않는 것이 좋다.일대일 연관관계
일대다, 다대일 연관관계에서는 항상 다쪽이 FK를 가진다.
하지만 일대일 연관관계에서는 어느쪽이 FK를 가지든 상관없다. 따라서 주 테이블(자주 사용하는 테이블, 이를테면 Member)에 외래 키를 둘 지, 대상 테이블에 외래 키를 둘 지 선택해야한다.
단방향
양방향일 경우 mappedBy 속성을 통해서 연관관계의 주인을 정해주면 된다.
주의할 점
다대다 연관관계
데이터베이스에서는 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서
@ManyToMany
애너테이션으로 다대다 연관관계를 만들면 JPA는 중간 테이블을 만드는 방식으로 관계를 풀어낸다.Member - Member_Product - Product
Member_Product는 Member와 Product의 id를 외래키로 갖는 테이블.
하지만 실무에서는 이 연결 테이블에 추가적인 정보를 넣을 일이 많기 때문에
@ManyToMany
애너테이션을 사용하는 일은 거의 없다. 대신 연결 테이블을 엔티티로 승격시켜서, M:N 관계를 1:N, N:1 관계로 풀어낸다. (강의자료 OrderItem 참고)상속관계 매핑
객체는 상속관계가 있지만 DB에는 그런게 없음. 슈퍼타입, 서브타입이라는 논리 모델링 기법이 객체의 상속과 유사하긴 하다.
이를 실제 물리 모델로 구현하는 방법에는 세 가지가 있다.
JPA는 세 가지 방법을 모두 지원한다. 부모 클래스에
@Inheritance
애너테이션을 주고 strategy 속성을 주어 각 방법을 선택할 수 있다.각각 테이블로 변환 (JOINED)
조인 전략을 사용한다. 하위 타입들을 모두 테이블로 만들고 조인을 통해 데이터를 가져온다. 인서트는 각각의 테이블에 일어난다.
그러나 성능 측면에서 그렇게까지 나쁘지는 않다고 한다. 이 전략을 선택하는게 무난하다.
통합 테이블로 변환 (SINGLE_TABLE)
단일 테이블 전략. 하나의 테이블에 다 때려넣는다. JPA의 기본 전략이며 성능 측면에서 좋다.
@DiscriminatorColumn
애너테이션이 없어도 자동으로 DTYPE이 생성된다.단순한 데이터의 경우 선택하면 좋다.
서브타입 테이블로 변환 (TABLE_PER_CLASS)
상위 타입의 데이터를 서브 타입 테이블이 다 가지고 있는다. 상위 타입을 추상 클래스로 만들면 테이블이 생성되지 않고 하위 타입의 테이블만 생성된다.
인서트할 때는 단순하고 좋아보이지만 부모 타입으로 조회할 경우(em.find(Item.class)) 조회 쿼리가 굉장히 복잡하게 날아간다. → 쓰지말자
@DiscriminatorColumn
애너테이션을 사용하면 테이블에DTYPE
컬럼이 생기는데, 하위 타입의 이름이 들어간다. name 속성으로 컬럼 이름을 바꿀 수 있다.만약 칼럼에 들어가는 값을 하위 타입의 이름이 아니라 다른 이름으로 설정하고 싶으면 하위 타입에
@DiscromonatorValue
애너테이션을 추가하여 원하는 이름을 name 속성에 넘겨주면 된다.Mapped Superclass
공통 매핑 정보가 필요할 때 사용한다. ex) BaseEntity ← 얘는 엔티티 아님
특정 필드가 모든 엔티티에서 중복되어서 뭔가 공통적으로 뽑아내고 싶을 때 사용한다.
공통 매핑 정보를 담은 클래스에
@MappedSuperclass
애너테이션을 붙여주고 정보를 받을 클래스가 이를 상속하면 된다.