@Entity
public class MEMBER {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
if(!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
@Entity
public class MEMBER {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
@JoinColumn
을 명시하지 않으면, JPA
는 조인 테이블을 기본 전략으로 사용해서 매핑한다.UPDATE
쿼리가 추가 발생한다.Member member1 = new Member("member1");
Member member2 = new Member("member2");
Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(member1);
em.persist(member2);
em.persist(team1);
transaction.commit();
//INSERT member1
//INSERT member2
//INSERT team1
//회원 테이블에 있는 외래 키 Team.id를 갱신하기 위해 팀 엔티티의 Team.members를 이용해야 한다.
//이를 통해 UPDATE 쿼리가 두 번 발생
단방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
단방향
양방향
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
다대다 관계는 정규화된 테이블 2개로 표현할 수 없다.
중간에 연결 테이블을 추가해야 한다.
Member - Member_Product - Product
Member
와 Product
는 다대다 관계Member_Product
는 회원이 주문한 상품을 의미한다.객체는 테이블과 다르게 컬렉션을 통해 객체 2개로 다대다 표현이 가능하다.
@JoinTable
을 통해 연결 테이블과 매핑했다.Member_Product
엔티티를 따로 생성할 필요가 없어진다.@JoinTable.name
: 연결 테이블명@JoinTable.joinColumns
: 회원과 매핑할 조인 칼럼 정보 지정@JoinTable.inverseJoinColumns
: 반대 방향인 상품과 매핑할 조인 칼럼 정보 지정@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<Product>();
}
@Entity
public class Product {
@Id @GeneratedValue
@Column(name = "PRODUCT_ID")
private Long id;
private String name;
}
@Entity
public class Product {
@Id @GeneratedValue
@Column(name = "PRODUCT_ID")
private Long id;
private String name;
@ManyToMany(mappedBy = "products")
private List<Member> members;
}
@ManyToMany
를 사용 XMember - MemberProduct
: 일대다 관계
MemberProduct - Product
: 다대일 관계
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
}
@Entity
public class Product {
@Id @GeneratedValue
@Column(name = "PRODUCT_ID")
private Long id;
private String name;
}
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
public class MemberProductId implements Seriallizable {
private String member;
private String product;
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
@Id
와 @JoinColumn
을 동시에 사용해서 기본 키, 외래 키를 한번에 매핑JPA
에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.@IdClass
를 사용해서 식별자 클래스를 지정한다.Serializeable
구현equals
와 hashCode
구현MemberProduct
는 Member
와 Product
로부터 기본 키를 받아서 자신의 기본 키로 사용한다.//조회
MemberProductId memberProductId = new MemberProductId();
memberProductid.setMember("member1");
memberProductid.setProduct("productA");
MemberProduct memberProduct = em.find(MemberProduct.class, memberProudctId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
ORDER_ID
라는 기본 키를 새로 만들고, MEMBER_ID
와 PRODUCT_ID
는 외래 키로만 사용한다.
@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
매핑이 단순해지고 이해하기 쉬워진 것을 확인할 수 있다.
//조회
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Product product = order.getProduct();
조회도 식별자 클래스를 사용하지 않아 간편해진 것을 확인할 수 있다.
관계형 데이터베이스에는 객체의 상속 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 있기 때문에, 객체의 상속 구조와 이 기법을 매핑하는 것이 ORM의 상속 관계 매핑이다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때 3가지 방법을 선택할 수 있다.
엔티티를 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략
따라서 조회할 때 조인을 자주 사용한다. 테이블은 타입의 개념이 없으므로 타입 구분 컬럼을 추가해야 한다.
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance
를 사용해야 한다.@DiscriminatorColumn(name = "DTYPE")
@DiscriminatorValue("M")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
private String author;
private String isbn;
}
null
값을 허용해야 한다.@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
}
null
허용해야 한다.InheritanceType.TABLE_PER_CLASS
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
...
}
public class Movie extends Item {
...
}
@Entity
public class Book extends Item {
...
}
BaseEntity
의 매핑 정보를 물려받았다.@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
// ID 상속
// NAME 상속
private String email;
...
}
@Entity
public class Seller extends BaseEntity {
// ID 상속
// NAME 상속
private String shopName;
...
}
JPA는 복합 키를 지원하기 위해 @IdClass
와 @EmbeddedId
2가지 방법을 제공한다.
@IdClass
@EmbeddedId
@IdClass
를 통해 ParentId
클래스를 식별자 클래스로 정의@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1; // ParentId.id1과 연결
@Id
@Column(name = "PARENT_ID2")
private String id2; // ParentId.id2와 연결
private String name;
...
}
public class ParentId implements Serializable {
private String id1;
private String id2;
public ParentId() {}
public ParentId(String id1, String id2){
this.id1 = id1;
this.id2 = id2;
}
// equals
// hashCode
}
@JoinColumns
를 통해 여러 컬럼을 매핑한다.referencedColumnName
은 name
과 값이 같으면 생략 가능@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1",
referencedColumnName = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2",
referencedColumnName = "PARENT_ID2")
})
private Parent parent;
}
Parent
엔티티에서 식별자 클래스인 ParentId
를 직접 사용하고 @EmbeddedId
를 붙여주면 된다.@Embeddable
을 붙여준다.@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
}
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// equals
// hashCode
}
//저장할 때 식별자 클래스를 직접 생성해서 parent에 세팅해준다.
Parent parent = new Parent();
ParentId parentId = new ParentId("id1", "id2");
parent.setId(parentId);
parent.setName("name");
em.persist(parent);
영속성 컨텍스트는 엔티티의 식별자를 키로 사용하여 엔티티를 관리한다. 이때, equals()
와 hashCode()
를 통해 비교하기 때문에, 식별자 객체의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 찾을 수 없는 등 심각한 문제가 발생할 수 있다. 따라서 복합 키는 equals()
와 hashCode()
를 필수 구현해야 한다.
@IdClass와 식별 관계
@Id
와 @ManyToOne
을 같이 사용했다.@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
@Entity
@IdClass(ChildId.class)
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@Id
@Column(name = "CHILD_ID")
private String childId;
private String name;
}
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// 기본 생성자
// equals
// hashCode
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID")
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id
@Column(name = "GRANDCHILD_ID")
private String id;
private String name;
}
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
}
@EmbeddedId와 식별 관계
@MapsId
를 사용한다.@MapsId
는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") // ChildId.parentId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
private String name;
}
@Embeddable
public class ChildId implements Serializable {
private String parentid; // @MapsId("parentId")로 매핑
@Column(name = "CHILD_ID")
private String id;
// equals
// hashCode
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
private String name;
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childid; // @MapsId("childId")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
// equals
// hashCode
}
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
public class GrandChild {
@Id
@GeneratedValue
@Column(name = "GRANDCHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
}
@MapsId
를 사용할 때 속성 값은 비워두면 된다.@Entity
public class Board {
@Id
@GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
public class BoardDetail {
@Id
private Long boardId;
@MapsId // BoardDetail.boardId 매핑
@OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
private String content;
}
SQL
이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.조인 테이블 방식은 별도의 테이블을 사용해서 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다.
@JoinTable
name
: 매핑할 조인 테이블 이름joinColumns
: 현재 엔티티를 참조하는 외래 키inverseJoinColumns
: 반대 방향 엔티티를 참조하는 외래 키@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
optional = false
: 연관된 엔티티가 무조건 존재해야 한다.@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne(optional = false)
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
)
private Parent parent;
}
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
@Table
을 사용해서 BOARD
테이블과 매핑@SecondaryTable
을 사용해서 BOARD_DETAIL
테이블과 매핑pkJoinColumns
: 매핑할 다른 테이블의 기본 키 컬럼 속성@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@Column(table = "BOARD_DETAIL")
private String content;
}