EntityManager에서만 Detach 상태를 만들 수 있고, JpaRepository를 이용해서는 불가능하다.
(JPA에서는 굳이 직접 Detach 상태로 만드는 것이 의미가 없다고 생각했는지)
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 기존 엔티티를 가져와서 Detached 상태로 변경
MyEntity detachedEntity = entityManager.find(MyEntity.class, entityId);
entityManager.detach(detachedEntity);
// Detached 상태의 엔티티 수정
detachedEntity.setName("Updated Name");
// 수정된 내용이 반영된 새로운 엔티티를 영속성 컨텍스트에서 관리
MyEntity managedEntity = entityManager.merge(detachedEntity);
managedEntity.setAnotherProperty("Updated Property");
transaction.commit();
기존 Entity는 detached가 되고, update한 내용인 Entity는 managed 상태가 된다.
트랜잭션 - 지연 쓰기, 캐시
✔ @Transactional
트랜잭션이 종료되면 영속성 콘텍스트를 DB에 반영하고, 종료한다. (생명주기가 같다고 본다.)
반영할 sql을 영속성 콘텍스트에 모아두었다가 한 번에 처리해주는 역할
✔ flush()
영속성 컨텍스트를 DB와 동기화한다. (Managed와 DB를 맞춘다.)
(영속성 컨테이너에 Entity는 소멸되지 않는다.)
1. 트랜잭션 종료
2. flush() 호출
3. jpql 쿼리 동작
✔ AutoFlush
jpql 쿼리 실행시 자동으로 flush() -> jpql 동작
jqpl을 사용하면 DB에 접근하게 되는데, 영속성 컨텍스트 내용을 반영하고, 다시 가져온다.
동작에 이상없도록 하기 위한 JPA의 설정이다.
✔ save()
save()는 하나의 트랙잭션이다(@Transactional Propagation required가 달려있다.)
트랜잭션이므로 호출 시점에서 영속성 콘텍스트 내용을 DB에 반영하고, 종료한다.
하지만 상위 @Transactional로 묶여있다면 save는 DB에 저장하겠다는 의미가 아니다.
상위 트랜잭션에 묶여 사용된다. (Propagation)
save에서 persist & merge의 이해
persist - 새로운 객체를 영속성 콘텍스트에서 등록 관리한다. 트랜잭션 종료 시 insert
merge - detach Entity를 manage상태로 만든다. 트랜잭션 종료 시 update
Detach Entity는 단순히 볼게 아니다.
처음 key 1을 갖고있는 Entity는 manage상태이고
같은 키를 갖는 새로 생성한 Entity는 detach 상태가 된다. (키가 같기 때문에)
키가 같은 Entity는 detach상태이고, merge가 동작하여 update가 된다.
기존의 Entity를 조회하여 set...()으로 값을 변경했을 때도 마찬가지로 detach Entity상태가 되고, merge 동작하게 된다.
void 영속성상태테스트(){
// 해당 key 가 없으면 insert
MyUser myUser = new MyUser();
myUser.setId(1L);
myUser.setName("Park");
myUserRepository.save(myUser); // insert
System.out.println(myUserRepository.findById(1L));
// 해당 key 가 있으면 update
MyUser myUser1 = new MyUser();
myUser1.setId(1L);
myUser1.setName("Park123");
myUserRepository.save(myUser1);
System.out.println(myUserRepository.findById(1L));
// 해당 key 가 있으면 update
MyUser myUser2 = myUserRepository.findById(1L).orElseThrow(RuntimeException::new);
myUser2.setName("Park456");
myUserRepository.save(myUser2);
System.out.println(myUserRepository.findById(1L));
}
첫번째 save는 insert
두번째, 세번째 save 는 update가 동작한다. save()에서 select로 해당 키를 가져와서 merge -> update한다.
간단하게 새로운 id라면 insert, 기존 DB에 있는 id라면 update
✔ 예외처리
checked - 컴파일 타임 때 알 수 있으므로 무조건 예외 처리된다는 전제하에 RollBack 되지 않고 DB에 반영된다. (실제 예외가 발생해도 commit)
unchecked - 개발자가 놓칠 수 있으므로 예외 발생 시 RollBack
✔ Transaction Propagation
(default) - required 상위 트랜잭션이 있다면 유지해서 사용, 없다면 생성
support - 상위 트랜잭션이 있다면 사용, 없다면 없이 진행
notSupport - 해당 영역은 무조건 트랜잭션 없이 사용
requires_new 트랜잭션을 새로 생성해서 사용
nested 같은 트랜잭션을 사용하지만 종속된 트랜잭션은 상위에 영향을 주지않는다. 상위로부터는 영향받는다.
RDB에서는 한 곳에서 외래키를 갖고 있는데 자바에서는 양방향 매핑시 양쪽 모두 서로의 Entity를 갖고 있음
RDB에서는 해당 필드(칼럼)이 없는데 자바에는 존재
관점이 다르기 때문에 발생하는 문제로 RDB 외래키를 관리해주는 Entity를 누구로 할지 결정해야한다.
(2)
자바는 단방향 매핑이면 참조타입이 없는 한쪽에서 다른쪽으로 접근하는 것이 불가능,
RDB는 외래키를 이용해서 조인하면 서로 접근이 가능
Q&A
무조건 외래 키가 있는 쪽이 연관 관계의 주인 ?
- 대부분의 경우엔 그렇다. 하지만 외래키가 없는 Entity가 관계의 주인이 될 수 있다.
외래키가 없는 곳을 굳이 연관 관계의 주인으로 지정하는 이유?
- 편하다. 외래 키가 없음에도 연관 관계 CRUD가 완벽 동작
원래 외래키가 있는 곳에서의 CRUD는?
- 양방향 매핑이라면 전부 정상 동작한다. 외래 키가 없는 곳에서 CRUD를 하기 위함이다.
단점은 없는지?
- 외래 키 설정을 위한 추가 update 쿼리가 동작한다. (성능 저하)
mappedBy로 외래 키가 있는 반대편을 지정하는 이유?
- 코드 가독성 측면에서 수많은 Entity를 볼 때 양방향 매핑이 돼있음을 알림,
JPA에게 반대편 Entity가 주인임을 알림(매핑 테이블 생성과 검색을 방지),
외래 키가 있는 곳에서만 외래 키를 관리하여 복잡도를 줄이기 위함
✔ 단방향 관계
한쪽만 매핑 어노테이션을 설정한 것
@ManyToOne, OneToMany 알맞게 설정하고
@JoinColumn으로 외래키를 갖고 있는 쪽을 가르킨다.
(1) 다대일
RDB와 JAVA 관점을 일치시키는 방식.
RDB 외래 키가 있는 곳을 연관 관계의 주인으로 설정
(2) 일대다
RBD와 JAVA 관점이 불일치되는 방식.
외래 키가 없는 곳에만 매핑 어노테이션 설정
외래키가 있는 Entity에는 매핑 어노테이션을 설정하지 않는다. 단순히 long boardId
RDB 외래키가 없는 곳을 연관 관계의 주인으로 설정
다쪽에 있는 외래 키를 수정하기 위해 추가적인 update쿼리가 동작한다.
이 방식보다는 차라리 양방향 매핑을 사용하므로, 많이 사용하는 방식은 아니다.
* 양방향에서의 관계 주인 (CRUD의 가능 여부)
✔ 양방향 관계
양쪽이 매핑 어노테이션을 설정한 것
모든 테이블을 양방향 관계로 설정하면 복잡성이 증가하므로 필요할 때만 양방향 관계 지정
사실 단방향 만으로도 Entity 간의 관계 설정은 완료된 것이다.
(RDB는 한쪽만 외래키를 갖는 식으로 완벽히 동작해왔다.)
✔ 외래 키가 없는 Entity에서 반대쪽 Entity의 외래 키를 인식하게 하는 방법은 두 가지
(1) mappedBy
외래 키는 반대쪽 Entity가 갖고 있음을 알려준다.
mappedBy가 없으면?
RDB에서 외래 키가 없는 테이블에 외래 키를 생성하게 된다. (조회 필드를 외래 키로 잘못 인식해버린다.)
JPA가 양방향인 것을 모르고 해당 필드가 외래키로 인식되어 매핑 테이블을 생성하고, 그곳에서 관계 정보를 찾으려고 한다.
JPA가 만든 양쪽 Entity id를 갖는 매핑 테이블에는 당연히 아무런 정보가 들어가 있지 않아 찾지 못한다.
(2) JoinColumn
반대쪽 Entity를 가리키면 연관 관계의 주인을 외래 키가 없는 곳으로 설정
이 방식을 권장하지 않는 이유는추가적인 쿼리를 소모하게 된다.
두 Entity의 데이터를 새로 추가시킨다고 했을 때 외래키가 없는 Entity로 save()하면
Entity1 insert ---> Entity2 insert ---> 외래키가 있는 Entity를 update
insert 만으로 끝날 동작을, 반대편 Entity 외래 키를 update 하며 추가적인 쿼리가 소모된다.
성능을 희생하고, 편의를 증가
두 Entity 모두 CRUD가 가능해진다.
양방향 매핑 두 가지 방법
(1) JoinColumn(외래 키가 있는 곳에 설정), mappedBy(외래키가 없는 곳에 설정)
-> mappedBy가 있는 Entity는 외래키를 관리하지 않는다.
연관 관계의 주인을 외래키가 있는 곳으로 설정, 권장되는 방식
외래키가 존재하는 Entity에서 연관 Create, Select, Update, Delete가 가능
mappedBy 쪽에서도 cascade 설정하면 Insert 빼고는 다 전이된다.
@Entity
public class Troop {
@OneToMany(mappedBy="troop")
public Set<Soldier> getSoldiers() {
...
}
@Entity
public class Soldier {
@ManyToOne
@JoinColumn(name="troop_fk")
public Troop getTroop() {
...
}
(2) JoinColumn(외래 키가 없는 곳에 설정)
연관 관계의 주인을 외래키가 없는 곳으로 설정, 편의를 위해 성능을 희생시키는 방식
양쪽 모두 Create, Select, Update, Delete가 가능 (복잡도 증가)
@Entity
public class Troop {
@OneToMany
@JoinColumn(name="troop_fk") //we need to duplicate the physical information
public Set<Soldier> getSoldiers() {
...
}
@Entity
public class Soldier {
@ManyToOne
@JoinColumn(name="troop_fk", insertable=false, updatable=false)
public Troop getTroop() {
...
}
양방향 매핑 시에는 순환 참조, 순환 삭제 등 신경 써줘야 할 사항들이 생겨 복잡도가 증가한다.
그래서 mappedBy로 조회만 하고, 나머지는 연관 관계의 주인으로 처리하는 방식이 권장된다.