스프링 데이터 JPA-03.관계 매핑, 엔티티 상태와 Cascade
JPA 프로그래밍 4. 관계 매핑
관계에는 항상 두 엔티티가 존재
- 둘 중 하나는 그 관계의 주인(owning)이고
- 다른 쪽은 종속된(non-owning) 쪽입니다.
- 해당 관계의 반대쪽 레퍼런스를 가지고 있는 쪽이 주인.
단방향에서의 관계의 주인은 명확합니다.
- 관계를 정의한 쪽이 그 관계의 주인입니다.
단방향 @ManyToOne
- 기본값은 FK 생성
단방향 @OneToMany
- 기본값은 조인 테이블 생성
양방향
- FK 가지고 있는 쪽이 오너 따라서 기본값은 @ManyToOne 가지고 있는 쪽이 주인.
- 주인이 아닌쪽(@OneToMany쪽)에서 mappedBy 사용해서 관계를 맺고 있는 필드를 설정해야 합니다.
양방향
- @ManyToOne (이쪽이 주인)
- @OneToMany(mappedBy)
- 주인한테 관계를 설정해야 DB에 반영이 됩니다.
실습
@Entity //(name = "myAccount") // 미설정시 클래스 이름, 객체 안에서 사용되는 이름
//@Table(name = "account") // 미설정시 엔티티 네임과 동일
public class Account {
@Id
@GeneratedValue // 자동으로 생성되는 값, 리포지터리 통해 저장할때 자동으로 생성된 값을 씀
private Long id;
@Column(nullable = false)
private String username;
private String password;
@OneToMany(mappedBy = "owner")
private Set<Study> studies = new HashSet<>();
@Entity
public class Study {
@Id @GeneratedValue
private long id;
private String name;
@ManyToOne
private Account owner;
@Component
@Transactional
public class JpaRunner implements ApplicationRunner {
@PersistenceContext
EntityManager entityManager; // jpa 가장 핵심적인 빈
@Override
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setUsername("jaeuk");
account.setPassword("jpa");
Study study = new Study();
study.setName("Spring Data JPA");
// 01. 이 둘은 한 세트(객체지향적으로 보았을 때)
//account.getStudies().add(study);
//study.setOwner(account); // 이 줄만 있어도 db insert는 된다.
// 02. 그래서 일반적으로 이렇게 사용한다.
account.addStudy(study);
Session session = entityManager.unwrap(Session.class);
session.save(account);
session.save(study);
}
}
Account
// 일반적으로 이렇게 사용한다.
public void addStudy(Study study) {
this.getStudies().add(study);
study.setOwner(this);
}
// remove도 마찬가지
public void removeStudy(Study study) {
this.getStudies().remove(study);
study.setOwner(null);
}
JPA 프로그래밍 5. 엔티티 상태와 Cascade
Cascade 엔티티의 상태 변화를 전파 시키는 옵션. ex. sutdy 상태가 a->b로 변할때 account의 상태도 a->b로 변경을 시키고 싶을때.
엔티티의 상태?
- Transient: JPA가 모르는 상태
DB에 들어갈지 말지 모르는 상태. 이 상태로 가다가 Garbage로 그냥 사라질수도 있다.
- Persistent: JPA가 관리중인 상태 (1차 캐시, Dirty Checking, Write Behind, …)
.save()를 했다고 해서 바로 그 상태로 DB에 들어가는 것은 아니다.
이쯤 되었으면 DB에 싱크가 되어야겠다.판단했을 때
가장 최후의 순간, 꼭 필요할 때에만 DB에 접근. (실습 참고) Detached: JPA가 더이상 관리하지 않는 상태.
- Removed: JPA가 관리하긴 하지만 삭제하기로 한 상태.
엔티티의 상태 변화
처음 new Object() 생성하면 Transient
Transient 상태에서 .save() 하면 Persistent 상태 Transient 아닌 경우 혹은 get(), load() 등.. Persistent 상태
Persistent 상태에서 clear(), close() .. 하면 Detached 상태
Detached 상태에서 update(), merge(), saveOrUpdate() 등.. 하면 다시 Persistent 상태
Persistent 상태에서 delete() 하면 Removed 상태.
실습 및 예제 코드로 설명
@Override
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setUsername("jaeuk");
account.setPassword("jpa");
Study study = new Study();
study.setName("Spring Data JPA");
account.addStudy(study);
Session session = entityManager.unwrap(Session.class);
session.save(account);
session.save(study);
Account jaeuk = session.load(Account.class, account.getId());
System.out.println("======== print =========");
System.out.println(jaeuk.getUsername());
}
실행결과
Hibernate:
select
nextval ('hibernate_sequence')
Hibernate:
select
nextval ('hibernate_sequence')
======== print =========
jaeuk
Hibernate:
insert
into
account
(password, username, id)
values
(?, ?, ?)
Hibernate:
insert
into
study
(name, owner_id, id)
values
(?, ?, ?)
System.out.println(“======== print =========”); System.out.println(jaeuk.getUsername()); 해당 구문이 제일 마지막에 있는데,
실행결과를 보면 시퀸스만 조회하고 먼저 실행된 것이 보인다.
기존 jdbc라면 insert 2번 후 다시 select를 했을 텐데
해당 트랜잭션 내에서 판단했을 때
해당 값은 저 값인걸 이미 가지고 있으니까 굳이 select 하지 않고 그냥 있는 값을 먼저 출력 후
insert를 진행하는 것을 볼 수 있다.
즉, 객체들을 관리하면서 정말 필요한 순간, 최종적으로 꼭 필요하다고 판단되는 순간에만 DB에 접근을 한다.
또한 Dirty Checking, Write Behind등 .. 다양한 장점들이 훨씬 더 많다.
@Override
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setUsername("jaeuk");
account.setPassword("jpa");
Study study = new Study();
study.setName("Spring Data JPA");
account.addStudy(study);
Session session = entityManager.unwrap(Session.class);
session.save(account);
session.save(study);
Account jaeuk = session.load(Account.class, account.getId());
jaeuk.setUsername("haenuk"); // 확인
System.out.println("======== print =========");
System.out.println(jaeuk.getUsername());
}
실행결과
Hibernate:
select
nextval ('hibernate_sequence')
Hibernate:
select
nextval ('hibernate_sequence')
======== print =========
haenuk
Hibernate:
insert
into
account
(password, username, id)
values
(?, ?, ?)
Hibernate:
insert
into
study
(name, owner_id, id)
values
(?, ?, ?)
Hibernate:
update
account
set
password=?,
username=?
where
id=?
하지도 않은 update 구문이 실행된 것을 볼 수 있다.
그렇다면 username을 여러번 수정한다면?
...
Account jaeuk = session.load(Account.class, account.getId());
jaeuk.setUsername("haenuk");
jaeuk.setUsername("ungeen");
jaeuk.setUsername("hello");
jaeuk.setUsername("jaeuk");
System.out.println("======== print =========");
System.out.println(jaeuk.getUsername());
...
최종적으로는 jauek 이기 때문에, update가 실행되지 않는다. (updata가 필요하지 않다는 걸 판단)
Dirty Checking과 Write Behind의 조합
- Dirty Checking : 객체의 변경사항을 계속 감지
- Write Behind : 객체의 상태 변화를 데이터베이스에 최대한 늦게, 최대한 가장 필요한 시점에 적용
최종적으로 살펴보니까, 결국 이름은 jaeuk이야. 그러면 update를 할 필요가 없다. 판단하고 실행하지 않는다.
@Entity
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "post")
private Set<Comment> comments = new HashSet<>();
public void addComment(Comment comment){
this.getComments().add(comment);
comment.setPost(this);
}
...
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;
private String title;
@ManyToOne
private Post post;
...
@Override
public void run(ApplicationArguments args) throws Exception {
Post post = new Post();
post.setTitle("Jpa 공부중...");
Comment comment = new Comment();
comment.setComment("언제 공부는 끝나시나요..");
post.addComment(comment);
Comment comment1 = new Comment();
comment1.setComment("그러게 말이에요..");
post.addComment(comment1);
Session session = entityManager.unwrap(Session.class);
session.save(post);
System.out.println("======== print =========");
}
이상태로 저장을 하면 post 만 저장이 된다.
하지만 cascade 옵션을 사용하면
@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST)
private Set<Comment> comments = new HashSet<>();
// Persistent(JPA가 관리중) 상태를 전파 (앞서 설명한 엔티티 상태)
수정 후 실행하면 post 만 save했는데, comment도 2건 insert되는 걸 볼 수 있다.
delete의 경우도 마찬가지로
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
private Set<Comment> comments = new HashSet<>();
// Removed(JPA가 관리하긴 하지만 삭제하기로 한 상태)를 전파
post id:1 인것만 삭제하더라도, post id:1인 코멘트들도 같이 삭제가 되는 것을 볼 수 있다.
post id:1 인 코멘트들에게 Removed 상태를 전파하여 최종적으로 삭제가 필요하면 삭제가 되는 것.
보통은 cascade = CascadeType.ALL 옵션으로 두고 전부 사용한다.