영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(member); // INSERT를 의미하지만 아직 DB에 등록되지 않는다. MANAGER에 저장되어있는 상태
tx.commit(); // commit;을 해야 최종적으로 DB에서 저장된다.
영속성 컨텍스트는 Entity를 식별자 값으로 구분한다.
즉, @Id DB로 말하면 PK값이 있어야 이 컨택스트 안에 등록이 되는겁니다.
tracsaction commit()
을 하면 DB에 요청하는 flush()
선작업이 발생한다.
해당 상태를 준영속 상태라고 합니다. 이미 작업을 마쳤으니, 컨택스트에서 더 이상 관리하지 않는 Entity가 됩니다.
// 엔티티를 영속성 컨텍스트에서 전부 DB로 전송한다.
em.flush();
// 엔티티를 영속성 컨텍스트에서 분리해 준영속 상태로 만든다.
em.detach(member);
// 영속성 콘텍스트를 비워도 관리되던 엔티티는 준영속 상태가 된다.
em.claer();
// 영속성 콘텍스트를 종료해도 관리되던 엔티티는 준영속 상태가 된다.
em.close();
상황에 맞춰서 해당 매서드를 호출에 사용하면 됩니다.
캐시에 등록되며, find()
호출시 캐시에서 먼저 확인한다.
find()
는 쿼리문으로 SELECT 입니다. 조회 할때 먼저 식별자(@Id)로 이전에 작업한 Entity가 캐시에 있으면 캐시를 가져와 사용하게 됩니다.
em.persist(member);
// em.flush(); flush가 호출이 되면 아래의 find()는 db를 재조회해서 가져오게 됩니다.
em.find(Member.class, "member");
Entity의 동일성을 보장한다.
관계형 DB를 호출하게 되면 객체의 동일성을 보장 못합니다. 이 경우 collection값을 비교하는 로직을 새로 구현해야합니다.
Member m1 = DAO.queryforobject("memeber");
Member m2 = DAO.queryforobject("memeber");
m1 == m2 //false
하지만 JPA에선 가능합니다. 그 이유가 캐시로 보관이 되기 때문입니다. 물론 같은 컨택스트 안에서 호출했을때만입니다.
Member m1 = em.find("memeber");
Member m2 = em.find("memeber");
m1 == m2 //true
그 밖에
쓰기 지연 : 매번 db commit;이 발생하지 않고 한번에 모아 commit;이 된다.
변경 감지 : settter를 이용하면 업데이트가 이뤄집니다.
변수 == 변수
값만 매칭이 가능했던 기존 DB방식에서 객체 지향적인 매핑이 가능해집니다.
//만약 Order에 Member값을 참조해야한다면 기존 관계형DB에선 이렇게 사용해야한다.
Member member;
Order order;
String id = order.getMemberId();
String addr = order.getMemberAddr();
int code = order.getMemberZipcode();
하지만 객체를 참조할 수 있기 때문에 이게 가능해집니다. java의 객체지향에 적합해보입니다.
Member member;
Order order;
// good!
Member m = order.Member;
String id = m.getMemberId();
String addr = m.getMemberAddr();
int code = m.getMemberZipcode();
JPA는 DB에서 두테이블을 연관있는 key값으로 Join하는 기능을 다음과 같이 제공합니다.
@ManyToOne
[N:1]@OneToMany
[1:N]@OneToOne
[1:1]@ManyToMany
[N:M]매핑을 선언할 때에는 주인이되는 관계를 설정하는것이 중요합니다. 주인으로 선언된 Entity만이 변경감지(update)에 해당이 되기 때문입니다. 관례상 특정한 이유가 아니라면 외래키(FK)가 존재하는 쪽이 주인으로 선언해주면 됩니다.
다대일
다대일은 하나의 Column값이 참조할 수 있는 값이 여러개일때 사용합니다.
@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID") // 실제 테이블에 join할 column이름을 입력합니다.
private Member member; // 이게 주인이 되는 겁니다.
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name ="MEMBER_ID") // 참조되는 대상, 외래키의 부모
private Long id;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
private String a; private String b;
//... 더많은 값 생략
// 위에 Order Entity는 Member아이디와 맺어져있으므로 Member안에 값들은 전부 조회가능해집니다.
}
일대다
위에 소스에 있으니 해당 케이스로 설명을 하겠습니다. 주로 이 방식은 주인보다는 연관된 Entity에 사용이 됩니다.
@OneToMany(mappedBy = "member") //mappedBy로 주인이되는 '변수값'을 넣어준다 Column값 x
private List<Order> orders = new ArrayList<>(); // 해당맴버에 전체 주문을 알 수 있습니다.
일반적인 DB라면 Order를 Select문으로 검색했을때 해당건에 Member 당사자만 나와야하는게 맞지만(한 건), 우리는 어플리케이션을 개발하다보면 그 반대의 조건을 검색해야할때가 빈번히 나타납니다.
이 경우에서는 Member의 전체 주문건(여러건)이 그 대상입니다. 이러한 방식을 양방향 관계라고합니다.
만약 양방향 조회가 필요없다? 하면 해당부분은 안쓰셔도 됩니다.
일대일
일대일은 뭐 별거없습니다. 그냥 1==1로 매핑 합니다. 예를들면 회원과 락커서랍장 관계를 매핑할때 사용할 수 있겠네요. 마찬가지로 외래키를 가지는 쪽이 주인이 되며, 일대일의 경우에는 그 값이 UNIQUE KEY
가 됩니다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name ="MEMBER_ID")
private Long id;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name ="LOCKER_ID")
private Long id;
@OneToOne(mappedBy = "locker")
private Member member;
// 마찬가지로 조회될때 Locker에서 member를 조회할 일이 없으면 작성 안해도 됩니다.
// 이건 일대일이라, 진짜 양방향이 필요없어 보이네요. 그래도 객체의 멋을 위해서라면...
}
눈치채셨겠지만, 주인이되는 Entity에서는 @JoinColumn이 필요하고, 양방향으로 선언해줄때는 옵션 값으로 mappedBy가 필요합니다.
다대다
이건 거의 실무에서 사용이 불가능합니다. 물리적으로 DB에서 다수대 다수를 매핑하기 위한 방법은 딱하나 뿐입니다. 연결해줄 수 있는 테이블을 하나 더 만들어서 관리하는 것이죠.
다음과 같이 매핑되는 곳이 ID부분이기 때문에 혹여나 컬럼을 추가하게 되면 조회도 안됩니다...
따라서 컬럼을 새로 만들고(이 부분은 어쩔 수 없습니다. 만들어야해요.) 위에서 사용한 다대일 일대다를 두개씩 만들어 사용하는게 낫습니다. 이렇게 하면 일대다쪽에서는 당연히 하위 다른 컬럼들도 확인이 가능해집니다.
각 Annotation에는 옵션들이 있습니다. 추후에 작성해서 넣겠습니다.
고급맵핑에는 3가지 전략 있습니다.
@Inheritance(strategy=InheritanceType.XXX)
그리고 다음의 어노테이션을 옵션? 같이 쓸수 있습니다.
@DiscriminatorColumn(name=“DTYPE”)
@DiscriminatorValue(“XXX")
JOINED
//Item class
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item { // 추상객체로 만들어져있다.
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
// Album class
@Entity
public class Album extends Item{
private String artist;
}
다음과 같이 Album, Item 관계를 Join으로 해서 테이블이 두개가 생기게 됩니다.
부모테이블이 Item이 되며, 자식이 Album이 되는겁니다.
이때 외래키는 자식쪽에서 자동으로 받아와 사용됩니다. (DTYPE는 아래서 설명)
- 장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용가능
- 저장공간 효율화- 단점
- 조회시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시 INSERT SQL 2번 호출-
SINGLE_TABLE
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
// 이하 자식 class 생략
이 전략은 한테이블에 모든 자식의 컬럼을 다 넣는 방식입니다. 그냥 한 Entity에 작성하는 거랑 같습니다. (DTYPE는 아래서 설명)
- 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
TABLE_PER_CLASS
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
이 전략은 부모의 값을 갖는 자식을 테이블로 각각 만드는 것입니다.
이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
- 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- not null 제약조건 사용 가능- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
- 자식 테이블을 통합해서 쿼리하기 어려움
Discriminator
@DiscriminatorColumn(name=“DTYPE”)
@DiscriminatorValue(“XXX")
하나는 부모쪽에서 사용되고, 하나는 자식쪽에서만 사용됩니다.
컬럼을 하나 더 만들어 부모쪽에는 Type을 관리하는 컬럼을, 자식쪽에서는 그 컬럼에 무슨 값을 넣을 건지를 선언해주면 됩니다.
이렇게 함으로써 Item만 조회해서 어떤 Entity랑 관계있는지 시각적으로 확인가능합니다.
아래 예제만 보면 아실겁니다.
이 어노테이션은 TABLE_PER_CLASS전략에서는 무의미합니다. 전략의 특징상 필요한 값이 다들어가있기 때문입니다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name=“DTYPE”) //DTYPE가 default입니다.
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
// Album class
@Entity
@DiscriminatorValue(“A") // 마찬가지로 default는 Class 명이 됩니다. Album이 되겠네요.
public class Album extends Item{
private String artist;
}
@MappedSuperclass
이것도 하나의 맵핑입니다. 다만 Join과는 관계없어서 위에서 뺐습니다.
이건 단순히 공통된 컬럼을 생성하고 싶을때 사용합니다.
예를들면 create date, modified date 같은게 있겠네요
@MappedSuperclass
public abstract class BaseEntity { // 마찬가지로 추상으로 만들어줍니다.
private LocalDateTime createDate;
private LocalDateTime lastModifiedDate;
public LocalDateTime getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDateTime createDate) {
this.createDate = createDate;
}
public LocalDateTime getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
// 특정 Entity에 상속받으면 됩니다.