[스프링 JPA] - 조인

sonnng·2023년 10월 15일
0

Spring

목록 보기
20/41

목차

01 Fetch Join, Join의 차이점 및 일반조인으로 n+1문제를 해결하지 못하는 이유

02 JPQL과 영속성 컨텍스트 자세히 알기

03 Spring Jpa에서 더티 체킹이란?



1. Join, Fetch Join 차이점 요약


[1] 일반 Join

  • Fetch Join과 달리 연관 Entity에 Join을 걸어도 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회(SELECT)해 영속화
  • 따라서 데이터는 필요하진 않지만 연관Entity가 검색조건에 필요한 경우에 주로 사용

[2] Fetch Join

  • 조회의 주체가 되는 Entity와 Fetch Join이 걸린 연관 Entity도 함께 SELECT하여 모두 영속화
  • FetchType이 LAZY인 Entity를 참조하더라도 영속성 컨텍스트에서 관리되므로 N+1 문제 해결


[1] 일반 Join


Team team1 = new Team();
team1.setName("Team1");
em.persist(team1);

Team team2 = new Team();
team2.setName("Team2");
em.persist(team2);

Member member1 = new Member();
member1.setUsername("member1");
member1.setAge(20);
member1.setMemberType(MemberType.ADMIN);

member1.changeTeam(team1);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setAge(20);
member2.setMemberType(MemberType.ADMIN);

member2.changeTeam(team1);
em.persist(member2);


Member member3 = new Member();
member3.setUsername("member3");
member3.setAge(20);
member3.setMemberType(MemberType.ADMIN);

member3.changeTeam(team1);
em.persist(member3);

Member member4 = new Member();
member4.setUsername("회원4");
member4.setAge(20);
member4.setMemberType(MemberType.ADMIN);

member4.changeTeam(team2);
em.persist(member4);

Member member5 = new Member();
member5.setUsername("회원5");
member5.setAge(20);
member5.setMemberType(MemberType.ADMIN);

member5.changeTeam(team2);
em.persist(member5);

String query = "select distinct t From Team t join t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();

tx.commit();
Hibernate: 
/* select
    distinct t 
From
    Team t 
join
    t.members */ select
        distinct team0_.id as id1_3_,
        team0_.name as name2_3_ 
    from
        Team team0_ 
    inner join
        Member members1_ 
            on team0_.id=members1_.TEAM_ID

Team의 칼럼인 id와 name만을 가져오는 모습이 확인된다.




// TeamService.java
@Transactional
public List<Team> findAllWithMemberUsingJoin(){
  return teamRepository.findAllWithMemberUsingJoin();
}

// FetchJoinApplicationTests.java
@BeforeEach
public void init(){
  teamService.initialize();
}

@Test
public void joinTest() {
  List<Team> memberUsingJoin = teamService.findAllWithMemberUsingJoin();
  System.out.println(memberUsingJoin);
}
  • 다른 블로그를 살펴보니 Spring Data Jpa로 @Query를 이용해 Repository에서 일반 join을 만들고 Test코드에서 List를 출력해보면 LazyInitializationException이 발생
    • 일반적인 원인은 Session(Transaction)없이 Lazy Entity를 사용하는 경우가 주됨
  • 쿼리에서는 members와도 join이 되었음에도 LAZY Entity 대상인 members의 초기화가 이뤄지지 않아서 발생한 문제
  • 즉, toString()으로 아직 초기화되지 않은 members에 접근하면서 LazyInitializationException 발생
  • 일반 join :: 쿼리에 join을 걸어주지만, join대상에 대한 영속화까지 관여X
  • 따라서 영속성 컨텍스트에서는 SELECT대상인 Team만이 대상이 됨을 알 수 있음


[2] Fetch Join



Team team1 = new Team();
team1.setName("Team1");
em.persist(team1);

Team team2 = new Team();
team2.setName("Team2");
em.persist(team2);

Member member1 = new Member();
member1.setUsername("member1");
member1.setAge(20);
member1.setMemberType(MemberType.ADMIN);

member1.changeTeam(team1);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setAge(20);
member2.setMemberType(MemberType.ADMIN);

member2.changeTeam(team1);
em.persist(member2);


Member member3 = new Member();
member3.setUsername("member3");
member3.setAge(20);
member3.setMemberType(MemberType.ADMIN);

member3.changeTeam(team1);
em.persist(member3);

Member member4 = new Member();
member4.setUsername("회원4");
member4.setAge(20);
member4.setMemberType(MemberType.ADMIN);

member4.changeTeam(team2);
em.persist(member4);

Member member5 = new Member();
member5.setUsername("회원5");
member5.setAge(20);
member5.setMemberType(MemberType.ADMIN);

member5.changeTeam(team2);
em.persist(member5);

String query = "select distinct t From Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class).getResultList();

tx.commit();
Hibernate: 
/* select
    distinct t 
From
    Team t 
join
    fetch t.members */ select
        distinct team0_.id as id1_3_0_,
        members1_.id as id1_0_1_,
        team0_.name as name2_3_0_,
        members1_.age as age2_0_1_,
        members1_.TEAM_ID as TEAM_ID5_0_1_,
        members1_.type as type3_0_1_,
        members1_.username as username4_0_1_,
        members1_.TEAM_ID as TEAM_ID5_0_0__,
        members1_.id as id1_0_0__ 
    from
        Team team0_ 
    inner join
        Member members1_ 
            on team0_.id=members1_.TEAM_ID


// TeamService.java
@Transactional
public List<Team> findAllWithMemberUsingFetchJoin(){
  return teamRepository.findAllWithMemberUsingFetchJoin();
}

//FetchJoinApplicationTests.java
@Test
public void fetchJoinTest() {
  List<Team> memberUsingFetchJoin = teamService.findAllWithMemberUsingFetchJoin();
  System.out.println(memberUsingFetchJoin);
}

[실행결과]

[
    Team(
        id=1,
        name=team1,
        members=[
            Member(
                id=1,
                name=team1member1,
                age=1
            ),
            Member(
                id=2,
                name=team2member2,
                age=2
            ),
            Member(
                id=3,
                name=team3member3,
                age=3
            )
        ]
    ),
    Team(
        id=2,
        name=team2,
        members=[
            Member(
                id=4,
                name=team2member4,
                age=4
            ),
			Member(
                id=5,
                name=team2member5,
                age=5
            )
        ]
    )
]
  • SELECT 대상 : 질의 대상인 Team과 Fetch Join이 걸려있는 Entity(Member)를 포함한 컬럼과 함께 SELECT
  • Fetch Join의 결과 List를 출력해보면, Team과 Member이 담긴 객체의 정보가 출력됨을 확인
  • 영속성 컨텍스트에 올려지는 대상 : 질의 대상인 Team과 Fetch Join이 걸려있는 Entity(Member)


[3] 일반 Join을 쓰는 시점


  • JPA는 기본적으로 DB<->객체와의 일관성을 잘 고려하여 사용하는 것이 필수적
  • 필요 Entity만을 영속성 컨텍스트에 담아서 사용해야 함
  • 따라서 일반 join을 활용해 필요 Entity만을 영속성 컨텍스트에 올려서 사용하는 것이 권장됨
  • 예로, team2member4 라는 이름을 가지는 member가 속해있는 Team조회 라는 요구사항의 경우에는 일반 조인을 이용해 Team만을 조회해오면 됨을 알 수 있음


2. JPA와 영속성 컨텍스트의 이해

user 4개를 persist를 한 후, JPQL를 활용해 user4의 이름을 변경해보고 DB와 영속성 컨텍스트의 내용과 비교해보는 내용



Member member1 = new Member();
member1.setUsername("member1");
member1.setAge(20);
member1.setMemberType(MemberType.ADMIN);

em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setAge(20);
member2.setMemberType(MemberType.ADMIN);

em.persist(member2);


Member member3 = new Member();
member3.setUsername("member3");
member3.setAge(20);
member3.setMemberType(MemberType.ADMIN);

em.persist(member3);

Member member4 = new Member();
member4.setUsername("회원4");
member4.setAge(20);
member4.setMemberType(MemberType.ADMIN);

em.persist(member4);

int updateNUM = em.createQuery(
                "UPDATE Member m SET m.username= 'songhee' where m.username = :name")
        .setParameter("name", "회원4")
        .executeUpdate();

Member findMember = em.find(Member.class, member4.getId());
System.out.println("findMember name = " + findMember.getUsername());

tx.commit();

출력을 해보니 회원4로 그대로 였고, 강의에서는 영속성 컨텍스트에 반영이 되지 않았다고 하였으나 궁금하여 이 내용을 더 찾아보았다.


JPQL과 영속성 컨텍스트의 동작방식

  • JPQL이 실행되기 전에 FLUSH가 실행되어 '영속성 컨텍스트의 변경사항을 db에 반영' 시켜 영속성 컨텍스트와 DB의 일관성을 유지
  • 하지만, update 쿼리는 DB에만 적용이 되지, 영속성 컨텍스트에는 적용되지 않으므로 예상했던 songhee가 나오지 않게 된다.
  • 이 경우 Dirty checking(변경 감지)가 아니라 JPQL로 실행되었기 때문에 영속성 컨텍스트에 자동으로 update 내용이 반영되지 않게 된다.
  • EntityManager의 find를 실행하게 되는데, 우선 영속성 컨텍스트에 해당 데이터가 있는지 없는지 검색하고, 없다면 쿼리를 실행해 영속성 컨텍스트에 값을 초기화해놓는다.
  • 하지만, 이미 영속성 컨텍스트에는 em.clear가 되지 않았기 때문에 값이 존재하므로 find 쿼리는 발생하지 않게 됨을 알 수 있다.


  • 따라서, 영속성 컨텍스트에도 DB와 같은 데이터로 update 시키려면 em.clear()가 필수적이다.
int updateNUM = em.createQuery(
            "UPDATE Member m SET m.username= 'songhee' where m.username = :name")
    .setParameter("name", "회원4")
    .executeUpdate();

em.clear();


Member findMember = em.find(Member.class, member4.getId());
System.out.println("findMember name = " + findMember.getUsername());

tx.commit();

이렇게 되면 변경사항이 적용됨을 알 수 있었다.



3. 더티체킹

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin(); //트랜잭션 시작
try{

    Member member1 = new Member();
    member1.setUsername("member1");
    member1.setAge(20);
    member1.setMemberType(MemberType.ADMIN);

    em.persist(member1); //Identity전략 중 AUTO는 유일하게 persist하는 시점에 insert쿼리가 나간다

    member1.setAge(10); //엔티티만 변경

    tx.commit(); //트랜잭션 커밋


}catch(Exception e){
    tx.rollback();
    e.printStackTrace();
}finally {
    em.close();
}
emf.close();

이러한 코드가 있을 때, 별도로 DB에 update하는 코드가 없는 것을 확인할 수 있다.

  1. 트랜잭션 시작
  2. 엔티티 조회
  3. 엔티티에서 값 변경
  4. 트랜잭션 커밋


Hibernate: 
/* update
hellojpa.Member */ 
update
    Member 
set
    age=?,
    TEAM_ID=?,
    type=?,
    username=? 
where
    id=?

직접적인 upate 코드가 없어도 update 쿼리가 실행됨을 알 수 있는데, 이유는 Dirty checking 때문이다.


  • Dirty Checking은 상태변경검사 라는 뜻으로,

    jpa는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 DB에 자동으로 반영해준다.


*이때, 변화가 있다의 기준은 최초 조회 상태를 기준으로,

jpa에서는 엔티티 조회 당시 엔티티 조회 상태를 그대로 스냅샷으로 만들어 놓고

트랜잭션이 끝나는 시점에 스냅샷과 비교해 차이가 있다면 UPDATE 쿼리를 실행한다.


*이러한 Dirty checking은 jpa에서 관리하는 영속성 컨텍스트의 대상만 적용되며, 아래는 해당되지 않는다.

  • detach 엔티티(준영속 상태)
  • DB에 반영되기 전 처음 생성된 엔티티(비영속 상태)

이 둘은 값을 변경해도 Dirty checking되지 않아서 자동으로 DB에 값이 반영되지 x



[Spring Data JPA와 @Transactional을 함께 사용하는 경우]

@Slf4j
@RequiredArgsConstructor
@Service
public class Service {

    private final SendRepository sendRepository;

    @Transactional
    public void update(Long id, String sendNo) {
        Send send = sendRepository.getOne(id);
        send.changeSendNo(sendNo);
    }
}


Hibernate: 
/* update
hellojpa.Member */ 
update
    Send s 
set
    time=?,
    Order_ID=?,
    type=?,
    username=? 
where
    id=?

UPDATE 쿼리가 실행


변경된 필드만 DB에 UPDATE 하고 싶을 때는 @DynamicUpdate 활용

  • JPA에서는 전체 필드를 업데이트하는 방식을 기본값으로 사용
    • 장점
      • 데이터베이스 입장에서 쿼리 재사용이 가능하다
        • 동일한 쿼리를 받으면 이전에 파싱된 쿼리를 재사용
    • 단점
      • 필드가 많은 경우 update가 부담이 될 수 있다.
        • 이 경우 @DynamicUpdate 활용


EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

EntityTransaction tx = em.getTransaction();
tx.begin(); //트랜잭션 시작
try{

    Member member1 = new Member();
    member1.setUsername("member1");
    member1.setAge(20);
    member1.setMemberType(MemberType.ADMIN);

    em.persist(member1); //Identity전략 중 AUTO는 유일하게 persist하는 시점에 insert쿼리가 나간다

    member1.setAge(10); //엔티티만 변경

    tx.commit(); //트랜잭션 커밋


}catch(Exception e){
    tx.rollback();
    e.printStackTrace();
}finally {
    em.close();
}
emf.close();

이 예제에서 Member 엔티티 클래스에 @DynamicUpdate를 붙여주면

Hibernate: 
/* update
hellojpa.Member */ 
update
    Member 
set
    age=? 
where
    id=?

로 변경부분만 update가 나감을 알 수 있다.

0개의 댓글