엔티티 매핑
객체와 테이블 매핑
@Entity
- @Entity가 붙은 클래스는 JPA가 관리하며 엔티티라고 한다. JPA를 사용해서 테이블과 매핑할 클래스는 필수로 붙여야 한다.
* 기본 생성자가 필수이다.
- final클래스, enum, interface, inner클래스는 사용할 수 없다.
- 저장할 필드에 final을 사용하면 안된다.
- 속성으로는 name이 있는데 JPA에서 사용할 엔티티 이름을 지정하는 것이며 지정하지 않으면 클래스 이름 그대로 간다.
@Table
- 엔티티와 매핑할 테이블을 지정한다.
- name속성이 있는데 여기서 name은 매핑할 테이블 이름이다. 기본값은 엔티티 이름을 사용한다.
데이터베이스 스키마 자동 생성
기존에는 테이블을 미리 만들었는데 JPA를 사용하면 DDL을 애플리캐이션 실행 시점에 자동으로 생성하므로 테이블을 미리 만들어 둘 필요가 없다. 객체화 해서 매핑을 다 미리 해놓으면 애플리케이션이 구동되면서 테이블을 만들어주기 때문이다. 그리고 데이터 베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL을 생성해준다.
persistence.xml
<property name="hibernate.hbm2ddl.auto" value="create" />
이 설정을 추가하고 실행하면
매핑된 애들을 테이블이 존재한다면 테이블을 drop하고 create한다.
* 운영장비에는 절대 create, create-drop, update사용하면 안된다. 개발 초기에 사용하고 안정화 됐거나 운영 서버는 validate(테이블과 객체가 잘 매핑됐는지 확인해서 컬럼이 존재하는지 체크해줌)이나 아예 사용하지 않아야 한다.
유니크 제약조건을 추가 할 수 있는데 DDL생성 기능은 DDL을 자동생성할때만 사용되고, JPA의 실행 로직에는 영향을 주지 않는다. 런타임시 영향 X
필드와 컬럼 매핑
매핑 어노테이션에 따라 테이블이 어떻게 생성되는지 살펴보자. 필드 이름이 'useranme'이지만 테이블 컬럼명은'name'이 되었다. enum타입을 사용할때 반드시 STRING을 사용해야 하는데 그 이유는 ORIGINAL을 사용할 경우 enum의 이름이 아니라 그 이름의 순서가 DB에 저장되기 때문이다. Date타입은 날짜 어노테이션을 붙여야 하지만 LocalDate형은 붙이지 않아도 된다. 또한 자바에서만 사용하고 테이블과 매핑시키지 않을 필드는 @Transient를 붙이면 된다.
연관관계 매핑
객체의 참조와 테이블의 외래 키가 다른 것처럼 객체와 테이블 연관관계 차이를 이해해야 한다.
객체를 테이블에 맞추어 데이터 중심으로 모델링 하면 협력 관계를 만들 수 없다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾지만, 객체는 참조를 사용해서 연관된 객체를 찾는다.
단방향 연관관계(member ==> team 이동 가능)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
양방향 연관관계와 연관관계의 주인
Team.java
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
객체 연관관계는 2개이다.
회원 -> 팀 (단방향)
팀 -> 회원 (단방향)
테이블 연관관계는 1개이다.
회원 <-> 팀 (양방향)
객체의 양방향 관걔는 사실 양방향 관걔가 아니라 서로 다른 단방향 관계 두개이다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
그러나 테이블은 외래 키 하나로 두 테이블의 연관관계를 가진다. (양쪽 조인 가능)
그런데 Member와 Team중 둘 중 한쪽만 외래키를 관리해야 하는데 이때 연관관계 주인이라는 것이 등장한다.
양방향 매핑에는 규칙이 있다.
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
- 연관관계의 주인만이 외래 키를 관리한다. (등록, 수정)
- 주인이 아닌쪽은 읽기만 가능하다.
- 주인은 mappedBy속성을 사용할 수 없다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
그렇다면 둘 중 누구를 주인으로 해야 할까? ******외래 키가 있는 곳을 주인으로 정해야 한다.*****
여기서는 MEMBER테이블에서 TEAM_ID를 FK로 가지고 있다.
외래키를 가지고 있는 입장은 MEMBER이므로 Member를 주인으로 선정한다.
JpaMain.java
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();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
양방향 매핑시 연관관계 주인에 값을 입력해야 한다.
그래서 Meber에 Team을 set했다.
그런데 사실 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다.
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
단방향 매핑만으로도 이미 연관관계 매핑은 완료. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가 된 것이다. JPQL에서 역방향으로 탐색할 일이 많다. 단방향 매핑을 잘 하고 양방ㄹ향은 필요할 때 추가해도 된다. 테이블에서 영향을 주지 않기 때문이다.
단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음
객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
연관관계 주인
- 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 참조가 2군데
- 객체 양방향 관계 참조 2개중에 테이블의 외래 키를 관리할 곳을 지정해야함.
- 연관관계의 주인 : 외래 키를 관리하는 참조
- 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회만 가능
다대일 양방향
- 외래 키가 있는 쪽이 연관관계 주인
- 양쪽을 서로 참조하도록 개발
일대다 양방향
- 1쪽이 연관관걔 주인
- 다쪽에 외래키가 존재함
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조
- @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인테이블 방식을 사용해서 중간에 테이블을 추가한다.
단점 : 엔티티가 관리하는 외래키가 다른 테이블에 있다. 연관관계 관리를 위해 추가로 UPDATE SQL을 실행한다.
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는것을 권장한다.
일대일 정리
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음.
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점 : 값이 없으면 외래 키에 null허용
대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경 할때 테이블 구조 유지
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨.
다대다
- 관계형 데이터베이스는 정규화된 테이블2개로 다대다 관계를 표현할 수 없음.
- 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 함.
- 편리해 보이지만 실무에서 사용 X
- 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 주문시간, 수량 같은 데이터가 들어올 수 있음
- 연결 테이블용 엔티티를 추가해야 한다.
상속관계 매핑
관계형 데이터베이스는 상속관계가 없다.
슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
상속관계 매핑 : 객체의 상속의 구조와 DB의 슈퍼타입 서브타입 관계를 매핑한다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법
- 각각 테이블로 변환 -> 조인 전략
- 통합 테이블로 변환 -> 단일 테이블 전략 (컬럼을 다 때려박기, Type컬럼으로 구분)
- 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
조인전략
Item.java
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
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 int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
Moive.java ==> Item 상속
@Entity
public class Movie extends Item{
private String director;
private String actor;
public String getDirector() {
return director;
}
public void setDirector(String director) {
this.director = director;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
}
실행결과를 보면 Item객체를 만들지 않고 Movie를 만들어도 insert쿼리가 두번생성됨.
장점 :
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용가능
- 저장공간 효율화
단점 :
- 조회 시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시 insert sql 2번 호출
단일테이블전략
Item.java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
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 int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
insert
into
Item
(name, price, actor, director, DTYPE, id)
values
(?, ?, ?, ?, 'M', ?) ==> insert가 한번만 들어가도 되는대신 DTYPE을 생성해서 어떤 종류인지 넣어짐.
장점 :
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함
단점 :
- 자식 엔티티가 매핑한 컬럼은 모두 null허용 (한테이블에 여러 컬림이 몰빵되어있으므로)
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있으며, 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
구현 클래스 테이블 전략
Item.java
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
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 int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
이 전략은 DBA와 ORM전문가 둘다 추전하지 않는다.
장점 :
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- not null 제약 조건 사용 가능
단점 :
- 여러 자식 테이블을 함께 조회 할 때 성능이 느림
- 자식 테이블을 통합해서 쿼리하기 어렵다.
@MaappedSuperclass
공통 매핑 정보가 필요할 때 사용
BaseEntity.java
@MappedSuperclass
public class BaseEntity {
private String cratedBy;
private LocalDateTime createDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
만든사람, 시간, 수정한사람, 수정시간 등을 공통으로 적용하기 위해 사용.
@Entity
public class Member extends BaseEntity{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
Member가 상속받음.
try {
Member member = new Member();
member.setName("user1");
member.setCratedBy("kim");
member.setCreateDate(LocalDateTime.now());
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
member에 BaseEntity필드가 insert됨.
상속관계 매핑이 절대 아니다. 엔티티가 아니므로 테이블과 매핑이 되지 않는다. 부모 클래스를 상속 받는 클래스에 매핑 정보만 제공하므로 조회나 검색이 불가능하다. 직접 생성해서 사용할 일이 없으므로 추상 클래스로 권장한다.
테이블과 관계없고 단순히 엔티티가 공통으로 사용한느 매핑정보를 모으는 역할을 한다. 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
@Entity클래스는 엔티티나 @MappedSupperclass로 지정한 클래스만 상속 가능하다.