JPQL

gotcha!!·2023년 9월 18일
0

JPA

목록 보기
14/16

JPQL(Java Persistence Query Language)

  • JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화 해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.

JPQL 문법

  • 엔티티와 속성은 대소문자 구분(Member,age)
  • JPQL 키워드는 대소문자 구분 X (select, from, where)
  • 엔티티 이름을 사용함(select m from Member m)
  • 별칭은 필수(as는 생략가능)

 

집합과 정렬

select
	COUNT(m),
    SUM(m.age),
    AVG(m.age),
    MAX(m.age),
    MIN(m.age)
from Member m
  • GROUP BY, HAVING
  • ORDER BY

 

TypeQuery, Query

  • TypeQuery : 반환 타입이 명확할 때 사용
  • Query : 반환 타입이 명확하지 않을 때 사용
TypeQuery<Member> query = em.createQuery("select m from Member m", Member.class);
Query query = em.createQuery("select m.username, m.age from Member m");

 

결과 조회 API

  • query.getResultList(): 결과가 하나 이상일 때 리스트를 반환한다. 하지만 결과가 없으면 빈 리스트를 반환

  • query.getSingleResult() : 결과가 정확하게 하나일 때, 단일 객체를 반환
    혹시나 결과가 없으면 javax.persistence.NoResultException
    결과가 둘 이상이면 javax.persistence.NonUniqueResultException

 

파라미터 바인딩 - 이름 기준(권장), 위치 기준

이름 기준
select m from Member m where m.usernmae = :username
query.setParameter("username", usernameParam);
위치 기준

select m from Member m where m.username = ?1
query.setParameter(1, usernameParam);

 

프로젝션

  • select 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
  • select m from Member m 엔티티 프로젝션
  • select m.team from Member m 엔티티 프로젝션
  • select o.address from Order o 임베디드 타입 프로젝션
  • select m.username, m.age from Member m 스칼라 타입 프로젝션

프로젝션 - 여러 값 조회

select m.username, m.age from Member m
  1. Query 타입으로 조회한다.
  2. Object[] 타입으로 조회한다.
  3. new 명령어로 조회한다
    • 단순 값을 DTO로 바로 조회
    • 패키지 명을 포함한 전체 클래스 명 입력
    • 순서와 타입이 일치하는 생성자 필요
List<MemberDTO> result = em.createQuery("select new jpqlbook.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
               .getResultList();

MemberDTO memberDTO = result.get(0);

System.out.println("MemberDTO = " + memberDTO.getUsername());
System.out.println("MemberDTO = " + memberDTO.getAge());

 

페이징 API

JPA는 페이징을 다음 두 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터의 수
for(int i = 0; i<100; i++){
    Member member = new Member();
    member.setUsername("member" + i);
    member.setAge(i);
    em.persist(member);
}

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

List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
        .setFirstResult(1)
        .setMaxResults(10)
        .getResultList();


System.out.println("resultListSize = " + resultList.size());

for (Member member : resultList) {
    System.out.println("memberUserName " + member.getUsername());
}

 

조인

내부 조인
select m from Member m [INNER] join m.team t
외부 조인
select m from Member m left [OUTER] join m.team t
세타 조인
select count(m) from Member m, Team t where m.username = t.name

조인 - ON

  • 조인 대상을 필터링
  • 연관관계가 없는 엔티티 외부 조인

조인 대상 필터링

  • ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL
select m from Member m left join m.team t on t.name = 'A'
SQL
select m.*, t.* from
Member m left join Team t on m.Team_ID = t.id and t.name = 'A'

연관관계가 없는 엔티티 외부 조인

  • ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL
select m,t from Member m left join team t on m.username = t.name
SQL
select m.*,t.* from Member m left join Team t on m.username = t.name

 

서브 쿼리

  • 나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2)
  • 한 건이라도 주문을 한 고객
select m from Member m where (select count(o) from Order o where m = o.member) > 0

서브 쿼리 지원 함수

  • [NOT] EXISTS(subquery) : 서브쿼리에 결과가 존재하면 참
  • {ALL | ANY | SOME} (subquery)
    • ALL 모두 만족하면 참
    • ANY, SOME : 하나라도 만족하면 참
  • [NOT] IN (subquery) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

 

JPQL 타입 표현

  • 문자 : 'HI', 'He''s"
  • 숫자 : 10L(Long), 10D(Double), 10F(Float)
  • Boolean : TRUE, FALSE
  • ENUM : jpqlbook.MemberType.USER(패키지명 포함)
  • 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)

 

ENUM

//             String query = "select m.username, 'HELLO', TRUE from Member m " +
//                     "where m.type = jpqlbook.MemberType.ADMIN";

// 파라미터로 전달
             String query = "select m.username, 'HELLO', TRUE from Member m " +
                     "where m.type =:userType";
             

             List<Object[]> resultList = em.createQuery(query)
                     .setParameter("userType", MemberType.ADMIN)
                     .getResultList();

             System.out.println("resulListSize = " + resultList.size());

             for(Object[] objects: resultList){
                 System.out.println("Object[] " + objects[0]);
                 System.out.println("Object[] " + objects[1]);
                 System.out.println("Object[] " + objects[2]);
             }

 

TYPE

em.createQuery("select i from Item i where type(i) = Book", Item.class);

 

 

조건식 - CASE

String query =
        "select " +
                "case when m.age <= 10 then '학생요금'" +
                "when m.age >= 60 then '일반요금'" +
                "else '무료'" +
                "end " +
                "from Member m";
List<String> resultList = em.createQuery(query, String.class)
        .getResultList();

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

 

COALESCE, NULLIF

  • COALESCE : 하나씩 조회해서 null이 아니면 반환

ex) 사용자 이름이 없으면 "이름 없음" 반환

select coalesce(m.username , "이름 없음") from Member m

 

  • NULLIF : 두 값이 같으면 null 반환 , 다르면 첫 번째 값 반환

ex) 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환

select NULLIF(m.username, '관리자') from Member m

 

JPQL 기본 함수

 

경로 표현식

 

경로 표현식 용어 정리

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

 

경로 표현식 특징

  • 상태 필드: 경로 탐색의 끝 부분, 탐색 불가능
  • 단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색 가능
  • 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 불가능
    - 컬렉션 값 연관 경로는 FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.

 

경로 표현식 예제

  • 상태 필드
    select m.username from Member m
  • 단일 값 연관 필드
    select m.team from Member m
  • 컬렉션 값 연관 필드
    select m.username from Team t join t.mebers m

 

묵시적 조인 시 주의사항

항상 내부 조인을 통해 일어난다.
컬렉션은 경로 탐색의 끝이므로 명시적 조인을 통해 별칭을 얻어서 경로를 탐색해야한다.
경로 탐색은 주로 select, where 절에서 사용하지만, 묵시적 조인으로 인해 sql의 from(join)절에 영향을 준다.

가급적 묵시적 조인 대신 명시적 조인을 사용하자 실무에서는 수 많은 쿼리로 묵시적 조인이면 조인이 일어나는 상황을 파악하기가 힘들다.


fetch join(jpql 전용 기능)


  • SQL 조인 종류가 아니다
  • JPQL에서 성능 최적화를 위해 제공하는 기능이다.
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능(즉시 로딩과 비슷)

회원 1,2는 팀A에 소속되어있고, 회원 3은 팀B에 소속되어있다.

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

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

             Member member1 = new Member();
             member1.setUsername("member1");
             member1.changeTeam(teamA);
             em.persist(member1);

             Member member2 = new Member();
             member2.setUsername("member2");
             member2.changeTeam(teamA);
             em.persist(member2);

             Member member3 = new Member();
             member3.setUsername("member3");
             member3.changeTeam(teamB);
             em.persist(member3);

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

             String query = "select m from Member m";

             List<Member> resultList = em.createQuery(query, Member.class)
                     .getResultList();


             for (Member m : resultList) {
                 System.out.println("Member = " + m.getUsername() + " , Team = "
                         + m.getTeam().getName());
             }
                    

위 코드는 팀 엔티티는 프록시로 들어오게 되고(지연로딩),
회원1 팀 조회(SQL)
회원2 팀 조회(1차캐시)
회원3 팀 조회(SQL)
결과상 쿼리가 3번이 나가게 된다.
이로 인해 N + 1의 문제 상황이 발생한다.
(1은 첫 번째 날린 쿼리 첫 번째 날린 결과 쿼리를 N)


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

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

             Member member1 = new Member();
             member1.setUsername("member1");
             member1.changeTeam(teamA);
             em.persist(member1);

             Member member2 = new Member();
             member2.setUsername("member2");
             member2.changeTeam(teamA);
             em.persist(member2);

             Member member3 = new Member();
             member3.setUsername("member3");
             member3.changeTeam(teamB);
             em.persist(member3);

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

             String query = "select m from Member m join fetch m.team";

             List<Member> resultList = em.createQuery(query, Member.class)
                     .getResultList();


             for (Member m : resultList) {
                 System.out.println("Member = " + m.getUsername() + " , Team = "
                         + m.getTeam().getName());
             }

             tx.commit();

fetch join을 이용하게 되면 위에서 팀 엔티티가 프록시로 들어오는 것이 아닌, 실제 엔티티로 조회가 된다.
데이터가 기존에 디비에서 가져와서 1차캐시에 저장되어있기 때문에, 글로벌 전략이 지연로딩이어도 쿼리를 한 번에 보내서 데이터를 다 끌어오게 된다.
참고로 fetch join은 글로벌 지연로딩 전략보다 우선한다.


일대다 페치조인

             String query = "select t from Team t join fetch t.members";

             List<Team> resultList = em.createQuery(query, Team.class)
                     .getResultList();


             for (Team t : resultList) {
                 System.out.println("Team = " + t.getName() + " , membersSize = "
                         + t.getMembers().size());
                 
                 for (Member m : t.getMembers()) {
                     System.out.println("-> Member " + m.getUsername());
                 }

             }

             tx.commit();

DB에서는 1:N 관계에서는 데이터가 늘어나게 된다.
JPA는 DB에서 주는 데이터를 가져다 쓸 수 밖에 없으므로, 팀A의 회원이 2명이 있다는 것을 중복으로 보여주게 된다.


String query = "select distinct t from Team t join fetch t.members";

원래 DB에서의 distinct는 모든 값이 다 중복이 되어야지 distinct로 중복 데이터를 없애주는데,
JPA에서는 distinct가 애플리케이션에서 중복되는 엔티티를 제거해주는 역할을 한다.

참고 : 하이버네이트6 부터는 distinct 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.


패치 조인 일반 조인 차이

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다(지연 로딩)
단지 select 절에 지정한 엔티티만 조회한다.

그러나 페치 조인을 사용할 때는 연관된 엔티티도 함께 조회한다(즉시 로딩)
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이기 때문이다.


패치 조인의 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    - @OneToOne, @ManyToOne 같은 단일 값 연관 필드들은 패치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징한다.

페치 조인 특징

  • 연관된 엔티티들을 SQL 한 번으로 조회 -> 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다
  • 최적화가 필요한 곳은 페치 조인 적용을 하자

JPA에서 객체 그래프를 탐색한다는 것은 모든 데이터를 조회되게 설계를 했는데, 그런데 만약에 데이터를 몇개씩만 들고오면 혹시나 cascade나 orphanRemoval같은 것이 적용되어있으면 데이터 변형이 일어날 수 있다.


다형성 쿼리

Type

조회 대상을 특정 자식으로 한정하는 것
ex) Item 중에 Book, Movie를 조회한다

[JPQL]
select i from Item i where type(i) IN (Book, Movie)

[SQL]
select i from i where i.DTYPE in ('Book', 'Movie')

Treat

ex) 부모인 Item과 자식 Book이 있다.
Book에서 저자가 김씨인 사람인 book item을 가져와라

단일 테이블일 경우 

[JPQL]
select i from Item i where treat(i as Book).author = 'kim'

[SQL]
select i.* from Item i where i.DTYPE = 'Book' and i.author = 'kim'

엔티티 직접 사용

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.

[JPQL]
select count(m.id) from Member m
select count(m) from Member m

== 

[SQL]

select count(m.id) as mid from Member m

엔티티, 엔티티 식별자를 파라미터로 전달할 때

em.createQuery("select m from Member m where m = :member").setParamter("member", member).getResultList(); // 엔티티로 전달
em.createQuery("select m from Member m where m.d = :memberId").setParameter("memberId" , memberId).getResultList(); // 식별자로 전달

[SQL]
select m.* from Member m where m.id = ?

엔티티, 엔티티 식별자(외래키)를 파라미터로 전달할 때

em.createQuery("select m from Member m where m.team = :team").setParameter("team", team).getResultList();

em.createQuery("select m fro Member m where m.team.id = :teamId").setParameter("teamId" , teamId).getResultList();

[SQL]
select m.* from Member m where m.team_id = ?

Named 쿼리

미리 쿼리를 정의해서 이름을 부여해두고 사용하는 JPQL이다. 정적 쿼리에 적합하고, 어노테이션이나 XML에 정의를 한다.
애플리케이션 로딩 시점에 초기화 후 재사용을 하고, 로딩 시점에 쿼리를 검증한다.

어노테이션 정의

@Entity
@NamedQuery(
	name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
public class Member {
.....
}

XML 정의

XML이 항상 우선권을 가지고, 운영 환경에 따라 다른 XML을 배포할 수 있다.


벌크 연산

JPA는 변경 감지 기능으로 엔티티의 변화를 감지한다.
근데 만약 100개의 데이터가 변경되었다면, 이를 업데이트 하기 위해서 100번의 업데이트 sql을 실행하게된다.
그러나 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리로 100번의 업데이트 sql을 피할 수 있다.

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • excuteUpdate()의 결과는 영향받은 엔티티의 수를 반환한다.
  • UPDATE, DELETE 지원
기존 회원의 나이를 모두 20살로 바꾸자

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

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

             Member member1 = new Member();
             member1.setUsername("member1");
             member1.changeTeam(teamA);
             em.persist(member1);

             Member member2 = new Member();
             member2.setUsername("member2");
             member2.changeTeam(teamA);
             em.persist(member2);

             Member member3 = new Member();
             member3.setUsername("member3");
             member3.changeTeam(teamB);
             em.persist(member3);
             
             // 일대다

             int i = em.createQuery("update Member m set m.age = '20'")
                     .executeUpdate();

             System.out.println("i = " + i);

             tx.commit();

벌크 연산으로 기존의 회원이 모두 20살로 변경되었고, 데이터베이스에는 20살로 적용되어있지만, 영속성 컨텍스트에는 적용이 되어있지 않은 상태이다.
flush가 일어나는 조건은 JPQL 쿼리가 수행될 때, 강제로 flush를 했을 때, commit 시점에 flush가 발생하게 된다.
그리고 flush가 발생한다고 해서 영속성 컨텍스트를 비워주지는 않는다.
그로 인해 em.find(Member.class, member1.getId()), em.find(Member.class, member2.getId()), em.find(Member.class, member3.getId())...을 통해 엔티티를 찾고 age를 찍어봐도 20살이 안나오고 기본값인 0이 나올 것이다.



벌크 연산 코드
.
.
.

em.clear();

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());

그래서 벌크 연산 이후 영속성 컨텍스트를 비워주고 member1의 아이디로 나이를 찍어봤을 때는 데이터베이스에서 member1의 엔티티를 가져와 조회하기에 나이가 20살이 출력이된다.

profile
ha lee :)

0개의 댓글