[우아한테크코스] 8월 4일 TIL

3 minute read

[JPA] 양방향 매핑

양방향 매핑이란?

기존 단방향 매핑을 적용하면 @ManyToOneReservation -> Space 매핑한 경우에는 reservation에서 space만 조회할 수 있다.

하지만 우리는 space에서 reservations도 조회하고 싶다. 왜? findById로 space 얻어오고, space를 바탕으로 reservationRepository로부터 reservations 가져오는게 싫다! 한번만 db에 요청 보내고 처리하고 싶다.

양방향 매핑은 결국 하나의 외래키를 가지고 서로 다른 단방향을 엮어서 만들어낸다.

@ManyToOne
@JoinColumn(name = "space_id", foreignKey = @ForeignKey(name = "fk_reservation_space"), nullable = false)
private Space space;

예를 들어 이렇게 Reservation에 space가 ManyToOne으로 다대일 매핑

@OneToMany(mappedBy = "space")
private List<Reservation> reservations = new ArrayList<>();

Space에 Reservation list가 OneToMany로 일대다 매핑되어 있다.

우리가 원하는 것은 결국에 space에 reservation을 추가하면 Reservation의 space 컬럼에, Space의 reservations에 추가한 Reservation이 추가되는 것이다. space만 조회했을 때 reservations를 얻고 싶다. .getReservation().add(Reservation) 없이 가능한가?

=> 변경감지는 각 객체의 값에 따라 일어난다. 그렇다면 추가할 때는 .getReservation().add(Reservation)을 해야하는게 맞지 않을까? 하나의 영속성 컨텍스트를 가져가면서 트랜잭션마다 flush.. 물론 하나의 트랜잭션을 마치면 space에 변경감지를 통해 reservation이 들어간다?

==> Reservation save 하면서 Space를 전달한 것이 reservation 상에서는 setSpace가 된 것이나 다름 없고 getReservations 시에 find문을 날린 것은 아닐까? 고로 객체 자체에는 연관관계 설정이 안된거 !?!?

**주의 만약에 spaces.save(new Space(reservation)) 와 같이 등록했으면 하나의 영속성 컨텍스트 안에서는 flush 되지 않아 save한 1차 캐시의내용이 반영되지 않는다.

연관관계의 주인은 보통 다 가 되는 요소가 차지한다. 여기서는 reservation이 연관관계의 주인이 되어 space가 mappedBy 속성을 자신의 컬럼인 reservations에 지정한다. 연관관계의 주인인 reservation만이 이 외래키를 지정하고 수정, 삭제 할 수 있고 space는 조회만 가능하다.

하지만 space만의 기능을 사용하고 reservation은 필요없는 경우가 있다. 100개의 reservation을 쓸데 없이 들고다니는 것은 비효율적이다. 이 때 지연로딩을 사용할 수 있다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "space_id", foreignKey = @ForeignKey(name = "fk_reservation_space"), nullable = false)
private Space space;

위와 같이 설정함으로써 지연로딩을 사용할 수 있다. 기본설정은 EAGER인 즉시로딩이다.

지연로딩이란 reservations에 프록시 객체를 넣어두었다가 호출되는 순간에 쿼리문을 날려 데려오는 것이다.

하지만 지연로딩을 이용해 이 때 reservations을 조회하면 이미 space 조회는 마쳐서 join 문 사용이 안되고 그 때마다 자신의 space를 조회하는 where 절을 가진 쿼리가 날라간다. 같은 space인데도 10개의 reservation이 있다면 그 쿼리문이 10번 날라가야 한다.

이를 fetch join으로 해결할 수 있다.

@Query("select s from Space s join fetch s.reservations")
List<Space> findAll();

위와 같이 적용할 수 있다. 이는 repository의 메서드 상단에 사용할 수도 있고, entityManager를 사용하는 service 메서드 내부에 적용될 수도 있다.

fetch join을 하게 되면 해당 쿼리문을 한번만 날리게 되어 효율적이다. 여기서는 Inner Join으로 sql 문이 날라간다.

=> 우리는 여기서 where 절에 오늘 날짜 이후의 예약을 가져왔으면 좋겠다고 생각했다. 되겠지? 됐으면 좋겠따 ㅎ

즉시로딩을 원하면 @EntityGraph로 해결할 수 있다.

@EntityGraph(attributePaths = "reservations")
@Query("select s from Space s")
List<Space> findAll();

위와 같이 작성하게 되면 즉시로딩으로 reservations 필드를 조회해 가져온다. Left outer join을 사용해 sql문이 작성된다.

하지만 두 경우 다 카테시안 곱이 발생해 reservation만큼 중복 발생한다. 이는 자료구조를 Set으로 두거나, DISTINCT문을 사용해 해결할 수 있다.

혹은 @BatchSize로 그만큼 데이터를 즉시 미리 로딩하는 기능이 있다. 페이징 사용 시 유용하다.

@BatchSize(size = 5)
@OneToMany(mappedBy = "space")
private List<Reservation> reservations;

[JPA] Cascade 옵션

  • PERSIST

    엔티티 영속화할 때 해당 엔티티도 함께 영속화, 해당 엔티티도 같은 옵션 있어야 함

  • MERGE

    트랜잭션 종료 후 detach됐을 때 연관 엔티티 추가하거나 변경 후에 부모가 merge() 하면 변경사항 적용, 이미 영속화된 엔티티에 추가 수정 시 사용

  • REMOVE

    삭제 시 같이 삭제

  • DETACH

    Detach 시 같이 detach

  • ALL

    모든 옵션 적용, 일반적 orphanRemoval = true 있어야 같이 삭제

[JPA] EntityManager 사용하기

직접 트랜잭션 설정이 아니라 영속성 컨텍스트를 다루고 싶다면? 아래와 같은 어노테이션 사용해 주입

@PersistenceUnit
EntityManagerFactory emf;

@PersistenceContext
EnitityManger em;

사용할 수 있는 메서드는 아래와 같다.

em.find();    // 엔티티 조회
em.persist(); // 엔티티 저장
em.remove();  // 엔티티 삭제
em.flush();   // 영속성 컨텍스트 내용을 데이터베이스에 반영
em.detach();  // 엔티티를 준영속 상태로 전환
em.merge();   // 준영속 상태의 엔티티를 영속상태로 변경
em.clear();   // 영속성 컨텍스트 초기화
em.close();   // 영속성 컨텍스트 종료

보통 flush는 직접 호출, 트랜잭션 커밋, JPQL 쿼리 실행 시 자동 호출된다.