반응형

JPA

https://jsonobject.tistory.com/605

 

Jackson 라이브러리는 기본적으로 엔티티의 Getter 메서드를 사용하여 JSON을 생성합니다.

따라서 Getter 메서드가 있고, 그 메서드가 연관 엔티티에 대한 접근을 포함하고 있다면,

Jackson은 해당 메서드를 호출하게 됩니다.

 

FetchType.LAZY로 설정된 연관 엔티티가 있을 경우,

Jackson이 해당 연관 엔티티의 Getter 메서드를 호출하면, JPA는 연관된 엔티티를 로드하려고 시도합니다.

만약 이 시점에서 JPA 세션이 닫혀 있으면 **LazyInitializationException**이 발생하게 됩니다.

 

해결방법

→ 연관엔티티가 있다면 Jackson 라이브러리가 Getter를 동작시키기 때문에 DTO를 사용해줘야 함

 

Jackson의 @JsonIgnore 어노테이션 사용: LAZY로 설정된 연관 엔티티의 Getter 메서드에

@JsonIgnore 어노테이션을 추가하여 Jackson이 해당 메서드를 호출하는 것을 방지합니다.

근데 결국, 연관 엔티티를 보내야 되는 경우가 있을 것이고, 그냥 DTO를 사용하는 방법이 제일 좋다.

반응형
반응형

 


 

JPA 테스트 중에 분명 fetch LAZY로 설정했는데, 

 

sql을 보니 LAZY Entity까지 조인을 하면서 마치 EAGER처럼 동작을 하는 문제가 있었다.

 

 


 

 

@OneToMany(mappedBy = "user1", fetch = FetchType.LAZY)
private List<UserParty> userParty;

 

@Test
void test9(){
    itemRepository.findByItemNameLikeAndUser1_NameLike("%2", "c%").forEach(System.out::println);
    System.out.println("-----");
    // lazy 인데 왜 userParty 쿼리가 나갈까

}

 

itemRepository에서 user1을 사용하는 쿼리 메서드

 

User1 ---> UserParty는 분명 LAZY인데 마치 EAGER처럼 UserParty를 전부 가져왔다.

 

 

문제 상황은 [ Item ---> User1 ---> UserParty ] 까지 전이되어 전부가져옴

 

원하는 것은 [ Item ---> User1 ] 까지

 


 

해결은 단순했다.

 

@OneToMany(mappedBy = "user1", fetch = FetchType.LAZY)
@ToString.Exclude
private List<UserParty> userParty;

 

Entity 상단에 설정된 @ToString 때문에 getter가 동작하여 EAGER처럼 계속 조회해서 사용하는 것이었다.

 

@ToString.Exclude를 붙여서 getter를 호출하지 않게 설정하고 LAZY로 정상 작동되게 했다.

 

fetch LAZY 를 설정하면 ToString.Exclude 를 같이 사용

반응형
반응형

App과 DB 사이에 있는 가상의 DB라고 생각하는 것이 직관적인 것 같다.

 

EntityManager Factory가 스레드마다 DB에 접근 시 EntityManager를 생성한다. 

EntityManager가 영속성 컨텍스트를 관리한다.

 

 

https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80

 

JPA 영속성 컨텍스트란?

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.em.persist

velog.io

 

 


목차

  • 1차 캐시
  • 더티 체킹
  • 쓰기 지연
  • 동일성 보장

 

  • 영속성 컨텍스트의 4가지 상태

 

  • transaction

 


영속성 콘텍스트의 기능

 

 

1차 캐시

Map의 형태로 Key는 idValue는 Entity

 

  1. [1차 캐시]에 조회하고자 하는 id가 있는지 확인한다.

(persist 이후 1차 캐시에 있다면 조회 쿼리가 동작하지 않고 찾아온다.)

  2. 1차 캐시에 없다면 [DB]에 있는지 확인한다. (DB select 쿼리가 동작한다.)

1차 캐시를 먼저 확인하고, DB에 접근하는 방식이다. 캐시를 재사용하므로 성능이 향상된다.

 

 

 

Dirty Checking

상태 변화 감지

영속성 콘텍스트의 최초 조회한 상태(스냅샷)가 변경되면 트랜잭션 종료 시점에서 자동으로 해당 Entity를 update 한다.

persist로 Entity의 등록 상태가 아니라 조회를 한 상태여야 한다.

조회한 Entity를 변경하면 detach Entity가 생성 되는데 detach Entity를 merge -> update한다. (자세한 내용은 아래에)

 

 

 

트랜잭션을 이용한 쓰기 지연

영속성 컨텍스트에서 발생하는 sql쿼리를 모아놨다가 트랜잰션이 종료될 때 모든 sql쿼리를 반영한다.

 

@Transactional 안에 save()가 있다면, 즉시 반영이 아니라 해당 메서드가 종료될 때 반영 (@Transactional이 상위이므로)

 

 


영속성 콘텍스트 4가지 상태

 

 

 

 

New 상태

객체를 생성한 상태, 객체를 생성했다고 해서 Managed가 아니다. (persist필요)

영속성 콘텍스트에 Entity가 등록되지 않은 상태

 

Manage 상태

영속성 콘텍스트에 Entity가 등록되어 관리되고 있는 상태

 

Remove 상태

영속성 콘텍스트에 Entity가 등록되어 관리되고 있다가 삭제된 상태

 

Detach 상태

영속성 콘텍스트에서 관리되지 않도록 분리한다. (1차 캐시, Dirty Checking이 동작되지 않는다.)

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 같은 트랜잭션을 사용하지만 종속된 트랜잭션은 상위에 영향을 주지않는다. 상위로부터는 영향받는다.

 

mandatory - 상위 트랜잭션이 있어야하고, 없으면 오류발생

never - 상위 트랜잭션이 없어야하고, 있으면 오류발생

 

 

 

 

 

 

 

 

 

 

 

 

반응형
반응형

JPA 관계 설정

자바 관점에서 생각하지 말고, RDB 관점에서도 생각해야한다.

 

JPA에서는 연관 관계의 주인이라는 개념이 있다.

연관 관계의 주인이란 외래 키를 관리하는 주체이다.

 

 

관점의 불일치

(1)

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로 조회만 하고, 나머지는 연관 관계의 주인으로 처리하는 방식이 권장된다.

 

https://docs.jboss.org/hibernate/stable/annotations/reference/en/html/entity.html#entity-mapping-association

 

Chapter 2. Mapping Entities

In Section 2.4.5.1, “Lazy options and fetching modes” we have seen how to affect the fetching strategy for associated objects using the @Fetch annotation. An alternative approach is a so called fetch profile. A fetch profile is a named configuration a

docs.jboss.org

 

 


Cascade

연관관계 Entity에 대한 영속성 전이 설정

dafault는 아무것도 설정되어있지 않다.

 

persist - 연관 Entity를 영속성 콘텍스트에 manage 되게 전이시킨다.

merge - 연관 Entity를 update 할 수 있게 한다.

detach - 연관 Entity를 영속성 콘텍스트에서 관리하지 않는다.

remove - 연관 Entity를 함께 제거한다.

 

 

 

orphanRemoval

연관 Entity만 삭제하고 싶을 때 사용한다.

 

set...(null)을 할 경우 외래키가 없는 것으로 update 된다.

이때 자동으로 연관 설정이 없는 Entity를 DB에서 제거하고자 할 때 사용한다. 

(cascade는 set..(null)로 동작하지 않는다.)

 

 


Entity를 영속성 컨텍스트에서 사용할 때 연관 Entity 설정 (fetch type)

 

LAZY

가장 먼저 lazy를 사용하기 위해선 Entity가 영속성 컨테이너에 관리되어지고 있어야 한다.

연관 관계 Entity가 필요할 때만 get()으로 연관 Entity를 select를 통해 나중에 가져온다.

 

EAGER

연관 관계 Entity를 영속성 컨테이너로 전부 즉시 가져온다.

 

LAZY가 보편적 권장 사항

 

 

 

 

✔ N+1 

1대 N 관계에서 1인 Entity를 findAll()했을 경우 Many Entity와 조인해서 한번에 가져올 것을 기대한다.

 

하지만, 실제 동작은 1인 Entity를 먼저 select 한 후, 1인 Entity의 id를 이용해서

매번 from N where Entity_id = id(1인 Entity의 id값)로 N 테이블에서 여러 번의 select로 id값을 확인한다.

 

[1회] select로 id 개수 확인 ---> [N회] id 개수만큼 select 동작

 

 

여러 번의 select보다 조인을 사용하고 싶을 때

1.  jpql - join fetch 이용

(join과의 차이: 연관 Entity도 한번에 가져온다)

2.  @EntityGraph에서 join 할 Entity 지정

(중복 데이터가 있을 수 있다. distinct사용)

 

 

 

 

✔ sql select의 실행 횟수 vs 조인된 데이터의 양 

두 가지를 고려하여 어떤 것이 더 효율적인가를 고려해야 한다.

 

sql select의 실행 횟수가 너무 많다면, DB를 반복적으로 접근해서 IO가 발생, 느려진다. -> 조인을 하는 선택을 한다.

가져온 데이터의 양이 너무 과하면 -> sql select를 필요한 것만 수행, 연관 Entity 정보만 가져오는 것을 선택한다.

 


참고

 

https://catsbi.oopy.io/f3204fd9-954c-44d7-ab18-2ca56000c5e5

 

다양한 연관관계 매핑

연관관계 매핑시 고려사항 3가지

catsbi.oopy.io

 

반응형

+ Recent posts