[스프링 JPA] - JPQL 기본 및 심

sonnng·2023년 10월 13일
0

Spring

목록 보기
19/41

1. JPQL - 경로 표현식

  • 경로 표현식이란?

    .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username //-> 상태 필드
 from Member m 
 join m.team t //-> 단일 값 연관 필드
 join m.orders o //-> 컬렉션 값 연관 필드
where t.name = '팀A';



  • 경로 표현식 용어정리
    • 상태 필드(state field): 단순히 값을 저장하기 위한 필드(ex: m.username)
    • 연관 필드(association field): 연관관계를 위한 필드
      • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
      • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

  • 1) 상태 필드
select m.username, m.age from Member m

  • 2) 단일 값 연관경로 탐색

[JPQL]

String query = "select o.member from Order o";

String qeuryAn = "select m.team from Member m";
                    //묵시적 내부 조인(단일 값 연관경로)

[SQL]에서 실행되는 쿼리

select m.* 
 from Orders o 
 inner join Member m on o.member_id = m.id

select m.* 
 from Member m 
 inner join Team t on m.team_id = t.id

  • 3) 컬렉션 연관경로 탐색

[JPQL]

String query = "select t.members from Team t";
//묵시적 내부 조인(컬렉션 값 연관경로)

//컬렉션 값 연관경로 .. 탐색불가..컬렉션 자체가 반환되기에
//대안 : from절을 통한 명시적 조인을 통해 가능

String queryDv = "select m.username from Team t join t.members m";
//명시적 조인 (컬렉션 값 연관경로) - join키워드 직접 사용

//별칭을 통해 탐색 가능

//결론: 묵시적 조인 쓰지 않는다. 쿼리 튜닝하기도 어렵다. 명시적 조인 추천

List resultList = em.createQuery(query, Collection.class).getResultList();

for(Object s : resultList){
    System.out.println(s);
}


  • 명시적 조인: join 키워드 직접 사용

    select m from Member m join m.team t

  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)

    select m.team from Member m

  • 경로 탐색을 사용한 묵시적 조인 시 주의사항

    • 항상 내부 조인
    • 컬렉션은 경로 탐색의 끝 => 명시적 조인을 통해 별칭을 얻어야함
  • 가급적 묵시적 조인 대신에 명시적 조인 사용




2. JPQL - 페치 조인(fetch join) - 실무에서 정말정말 중요함


[페치조인(fetch join)]

  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
  • EARER LOADING과 유사하다고 보면 된다.

[JPQL]

select m from Member m join fetch m.team

[SQL]

SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
  • 즉시로딩과 똑같은 쿼리가 나간다.
  • fetch join은 원하는 시점에 조인 쿼리가 나갈 수 있다는것이 차별점



[엔티티 페치 조인 사용 코드- 다대일 관계]

조건 : 회원1 - 팀A, 회원2-팀A, 회원3-팀B

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

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

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

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

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


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

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

em.flush();
em.clear();

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for(Member memberPer : members){
    System.out.println("username = "+memberPer.getUsername()+", "+
            "teamName = "+memberPer.getTeam().getName());
}

[출력]

Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as id1_3_1_,
            member0_.age as age2_0_0_,
            member0_.TEAM_ID as TEAM_ID5_0_0_,
            member0_.type as type3_0_0_,
            member0_.username as username4_0_0_,
            team1_.name as name2_3_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id
username = 회원1, teamName =A
username = 회원2, teamName =A
username = 회원3, teamName =B



[컬렉션 페치 조인 사용 코드- 일대다 관계]

조건 : 회원1 - 팀A, 회원2-팀A, 회원3-팀B

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

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

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

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

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


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

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

em.flush();
em.clear();

String jpql = "select t from Team t join fetch t.members";
List<Team> teamList = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teamList) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        System.out.println("-> username = " + member.getUsername()+ ", member = " + member);
    }
}

[출력]

Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members */ select
            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
teamname =A, team = hellojpa.Team@5d39f2d8
-> username = 회원1, member = Member{id=33, username='회원1', age=20}
-> username = 회원2, member = Member{id=34, username='회원2', age=20}
teamname =A, team = hellojpa.Team@5d39f2d8
-> username = 회원1, member = Member{id=33, username='회원1', age=20}
-> username = 회원2, member = Member{id=34, username='회원2', age=20}
teamname =B, team = hellojpa.Team@458544e0
-> username = 회원3, member = Member{id=35, username='회원3', age=20}



[페치조인(fetch join)과 DISTINCT]

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    • SQL에 DISTINCT를 추가
    • 애플리케이션에서 엔티티 중복 제거(같은 식별자를 가진 애플리케이션에서 중복제거)


 String jpql = "select distinct t from Team t join fetch t.members";
List<Team> teamList = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teamList) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        System.out.println("-> username = " + member.getUsername()+ ", member = " + member);
    }
}

[실행결과]

teamname =B, team = hellojpa.Team@6ad6fa53
-> username = 회원3, member = Member{id=40, username='회원3', age=20}
teamname =A, team = hellojpa.Team@458544e0
-> username = 회원1, member = Member{id=38, username='회원1', age=20}
-> username = 회원2, member = Member{id=39, username='회원2', age=20}

중복되지 않게 출력됨을 알 수 있다.




[페치 조인과 일반 조인의 차이]

  • JPQL은 결과를 반환할 때 연관관계 고려X
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

(fetch join 예시)

//[JPQL]
select t 
from Team t join fetch t.members
where t.name = ‘팀A';

//[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID 
WHERE T.NAME = '팀A';



[페치 조인의 특징과 한계]

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • fetch join은 데이터를 다 조회해야하기 때문에 where절로 fetch join 대상의 별칭을사용하지 않도록 한다.

      예) String jpqls = "select t From Team t join fetch t.members m where m age>10";
      - 이 경우 별도의 쿼리로
    • 유일하게 쓰는 경우

      예) String jpqls = "select t From Team t join fetch t.members m join fetch Order o";
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult setMaxResults)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 일대다의 경우, 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • fetch join은 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • 여기서 말하는 글로벌 로딩전략의 예

      @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용
  • 모든 것을 페치 조인으로 해결할 수 는 없음
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요
    한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
//1
String jpql = "select distinct t from Team t join fetch t.members";
List<Team> teamList = em.createQuery(jpql, Team.class)
        .setFirstResult(0)
        .setMaxResults(1)
        .getResultList();

//일대다 페치조인 주의할 점
//- 둘 이상의 컬렉션은 페치조인할 수 없다.
//- 컬렉션을 페치조인하면 데이터 뻥튀기.. 한번에 데이터 조회후,
//  메모리에서 페이징 API가 적용되므로 경고로그가 나온다.(매우 위험)

//+ 별칭 사용 불가
String jpqls = "select t From Team t join fetch t.members m where m.age>10";

//1의 대안 : 2
String jpqlDv = "select m from Member m join fetch m.team t";
// 위 일대다 페치조인 페이징 API의 대안 : 다대일 페치조인으로 뒤집어서 페이징 api 적용


//1의 대안 : 3
String jpqlDv2 = "select t from Team t"; //+ BatchSize(size = 100)정도
//팀A에 대한 사람이 2명이면, 총 3개의 쿼리가 나간다.
//Team에 대한 쿼리1(LAZY 로딩), Member에 대한 쿼리2개
//잘못하면 N+1문제 발생

//따라서 그에 대한 해결방법 : 1) 페이징 처리(똑같이 n+1문제 발생) 2) BatchSize설정

 List<Team> teamList = em.createQuery(jpqlDv2, Team.class)
                            .getResultList();

for(Team team : teamList) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        System.out.println("-> username = " + member.getUsername()+ ", member = " + member);
    }
}


  1. 코드 3번에서 BatchSize 설정을 안하는 경우
Hibernate: 
    /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_
teamname =A, team = hellojpa.Team@40021799
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
-> username = 회원1, member = Member{id=58, username='회원1', age=20}
-> username = 회원2, member = Member{id=59, username='회원2', age=20}
teamname =B, team = hellojpa.Team@1b32cd16
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?

총 3번의 쿼리가 나간다.



  1. 코드 3번 + BatchSize 설정을 하는 경우
Hibernate: 
    /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_
teamname =A, team = hellojpa.Team@588ffeb
Hibernate: 
    /* load one-to-many hellojpa.Team.members */ select
        members0_.TEAM_ID as TEAM_ID5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as TEAM_ID5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
-> username = 회원1, member = Member{id=63, username='회원1', age=20}
-> username = 회원2, member = Member{id=64, username='회원2', age=20}
teamname =B, team = hellojpa.Team@67ec8477
-> username = 회원3, member = Member{id=65, username='회원3', age=20}

1번의 쿼리가 나간다.




3. JPQL - 엔티티 직접 사용(기본키 값)

  • JPQL에서 엔티티를 직접 사용 => SQL에서 해당 엔티티의 기
    본 키 값을 사용하는 것과 같다.

[JPQL]

  1. 엔티티를 파라미터로 전달하는 경우나
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql) 
 .setParameter("member", member)
 .getResultList();

  1. 식별자를 직접 전달하는 경우나
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql) 
 .setParameter("memberId", memberId)
 .getResultList();

실행되는 SQL은 같다.

select m.* from Member m where m.id=?



4. JPQL - 엔티티 직접 사용(외래키 값)

  • JPQL에서 엔티티를 직접 사용 => SQL에서 해당 엔티티의 외래키 값을 사용하는 것과 같다.

[JPQL]

  1. 엔티티를 파라미터로 전달하는 경우나
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
 .setParameter("team", team)
 .getResultList();

  1. 식별자를 직접 전달하는 경우나
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
 .setParameter("teamId", teamId)
 .getResultList(); 

실행되는 SQL은 같다.

select m.* from Member m where m.team_id=?



5. JPQL - Named 쿼리(정적 쿼리)

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 어노테이션 or XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후(캐싱해서 계속) 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증
  • 다만, XML이 항상 우선권을 가진다
  • But, spring data jpa에서는 @Query로 모두 캐싱해두고 쿼리를 검증하는 기능까지 있어서 훨씬 유용
@Entity
@NamedQuery(
 name = "Member.findByUsername",
 query="select m from Member m where m.username = :username")
public class Member {
 ...
}
List<Member> resultList = 
 em.createNamedQuery("Member.findByUsername", Member.class)
 .setParameter("username", "회원1")
 .getResultList();

이렇게 사용하면 된다.




6. JPQL - 벌크연산

  • 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

    JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행..변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행
  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 실행
  • 벌크 연산을 이용할 때 활용 방법

    1) 벌크 연산을 먼저 실행 or 2) 벌크 연산 수행 후 영속성 컨텍스트 초기화

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

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

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

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

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


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

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

//이 사이에서 flush가 자동으로 일어난다,
//(flush 자동으로 일어나는 조건 : commit되기 전 or 쿼리가 실행되기 전)

//db에 직접 벌크연산(한번에 update or delete 연산이 나가는 것) 발생
//벌크연산일때는 영속성 컨텍스트에 반영되지 않음
int resultCount = em.createQuery("update Member m set m.age = 20")
        .executeUpdate();
System.out.println("resultCount = "+resultCount); //3 출력

//영속성 컨텍스트에는 반영이 안되어있음(clear을 해야 데이터가 날아가는데
//flush한다고 해서 데이터가 없어지는 게 아님)

Member findMember = em.find(Member.class, member1.getId()); //0출력

em.clear();

Member findMember = em.find(Member.class, member1.getId()); //20출력

tx.commit();

0개의 댓글