[JPA] JPQL 쿼리

2022. 12. 29. 18:15·Spring/JPA
728x90
반응형

<<객체지향 쿼리 언어 개요 >>

 

결국 DB 조회를 위해선 쿼리를 짤 수밖에 없는데, JPQL, 네이티브 SQL, Query DSL 등 다양한 방법을 지원

 

JPA를 사용하면 결국 Entity 중심으로 개발할 수밖에 없음. 

> 검색을 할때도 테이블이 아닌 Entity 를 대상으로 검색을 해야해함. 

> 그렇다면 뭐,, DB 데이터를 다~ 들고 온다음에 메모리에서 다~ 실행해서 객체로 변환한 다음에 Entity 대상으로 검색을 할 수 있도록 해야하나? > 이거는 말이 안됨. 

> App이 필요한 Data만 DB에서 불러오려면 결국 ! SQL을 찍어야 함. 

 

JPA + SQL 을 지원하는 JPQL 을 지원함. > JPQL은 일반 SQL처럼 DB 테이블 대상으로 하는게 아니고, 객체를 대상으로 한다. 니가 하던게 다 Entity 대상으로 쿼리 짠거임. 하지만 원래 SQL 은 데이터 중심임. 

 

try{
    List<Member> = 
    em.createQuery("select m from Member m where m.username like '%kim%'", Member.class)
        .getResultList();   
    tx.commit
} ...

저기서 말하는 Member 는 객체를 말하는 것이다. 객체에 대한 조회를 하는 것임. 

이런식으로 짜는거임. 하지만 이렇게 String 으로 하나 하나 다 짜는건 버그나기 너무 좋음. 사소한 버그 ㅇㅇ 동적 쿼리를 잘 짤 수 있도록 지원하는게 등장하기 시작 - Criteria, MyBatis 등

 

Criteria - 코드로 SQL을 짤 수 있도록 해줘서 편리하지만, 한거 보면 상당히 복잡함. (동적 쿼리 짜기도 좋음) 

(동적 쿼리란, 코드를 통해서 if 문 걸고 하면서 동적으로 변경될 수 있는 상황을 마련한 코드를 말한다) 

예를 들면, if userGender = girl, sql 이 바뀌고 그런것. 

 

Criteria 는 좋긴 한데 유지 보수는 안좋음. - 나는 안쓸거임. 

 

유명한건 이제 MyBatis와 QueryDsl임. QDSL 은 굉장히 문법이 간단함 ( 많이 써봤으니까 알거임) - 단점 세팅이 빡셈 ㅋㅋㅋ - 나는 아직 안해본 것 같은데, QueryDSL 은 동적쿼리를 짜기도 매우 편리. 실무에서 사용하기 권장한다. 

자바 코드로 JPQL을 작성할 수 있다는게 제일 큰 장점. 컴파일 시 오류 찾을 수 있음.

 

근데 QueryDSL 은 결국 JPQL 빌더임!! 그래서 결론적으로 JPA 쓸거면 JPQL 을 잘 알아야함. 그러면 QDSL은 어떻게 쓰는지를 알려주는 거임. JPQL 마스터 하고 QDSL 설명서 (사이트) 보면 다 들어옴. 

 

참고))) 영컨 = Flush 가 되어야 SQL이 날라감. 이걸 항상 명심해야 함. 은근 개입되어 헷갈릴때가 있더라. 

>> em.persist 만 한 시점에서는 DB에 안들어가 있음. 

>> flush 가 날라가는 경우 1) commit 할 때 2) Query 실행할 때 (JPQL 같은게 실행되면 플러시가 동작함)  (왜냐하면 날라가야 DB에서 조회를 할거 아님) 

 

 

--------------------------

 

<< JPQL 개요 및 기본 >>

 

JPQL (Java Persistence Query Lang . 영속성 관리  QL)

> 말했듯이 엔티티 객체를 대상으로 쿼리잉을 한다. 

> SQL을 추상화해서 특정 DB SQL에 의존하지 않는다. 

> 결국 JPQL 도 당연히 SQL로 치환된다. 

 

- 1분대 모델 참고 - 

- 모델링 다 끝 - 

 

select m from Member as m where m.age > 18

> Member 는 Entity 이고, age 는 속성이다. 

> 대소문자는 반드시 구분한다 

>  별칭 m 은 필수입니다. (as 는 없어도 됨) 

 

@Test
@DisplayName("JPQL Query Example")
void test1() {
    // ...
        JpqlMember member = new JpqlMember();
        member.setUsername("JPQL 1");
        member.setAge(20);
        em.persist(member);

        // 참고하면 좋은 내용
        // Member Generic 을 정확하게 가져온다. Type 정보를 명확하게 지정했기 때문.
        TypedQuery<JpqlMember> typedQuery = em.createQuery("select m from JpqlMember m", JpqlMember.class);

        // 그렇게 안되는 경우 (String, Integer 를 동시에 불러옴) >> Query 객체를 사용 // 반환 타입이 명확하지 않을 경우
        Query query = em.createQuery("select m.username, m.age from JpqlMember m");

        // 되는 경우
        TypedQuery<String> typedQuery2 = em.createQuery("select m.username from JpqlMember m", String.class);

        // 결과를 반환해보자
        for (JpqlMember jpqlMember :
                typedQuery.getResultList()) {
            System.out.println("jpqlMember.getUsername() = " + jpqlMember.getUsername());
        }
    // ...
}

 

특징

- em.createQuery  는 Query 를 반환할수도, TypedQuery 를 반환할 수도 있다. Query 를 반환할 경우는 위 상황처럼 반환 타입이 명확하지 않고 여러 개를 반환할 경우이다. TypedQuery 는 위처럼 명확하게 반환하려는 타입이 있을 경우이다. 

 

- getResultList() vs getSingleResult();

말그대로 한개여도, List<ResultType> list = typedQuery.getResultList(); 를 반환하게 된다. 만약 없으면 빈 리스트를 초기화해서 반환해주게 되어서, Null Pointer Exception 이 발생하지 않는다. 

반면 getSingleResult() 는 반드시 한개만 반환되어야 하며, 반환 값을 조회하지 못했으면 Null Pointer exception 이 발생한다. 심지어 둘 이상이면 NonUniqueResultException 이 발생한다. >> 써주려면 try catch 로 묶어서 사용해야하는 단점. 

 

==== Parameter Binding

- 위치기반은 걍 알지도 마라. Parameter 이름기반 바인딩!

@Test
@DisplayName("JPQL Example : Parameter binding")
void test2() {
    // ...
        JpqlMember member = new JpqlMember();
        member.setUsername("안녕하세요");
        member.setAge(20);
        em.persist(member);

        // Query 호출시 flush 자동 발생이므로 굳이 직접 안해줘도 됨
        TypedQuery<JpqlMember> query = em.createQuery("select m from JpqlMember m where m.username = :username", JpqlMember.class);
        query.setParameter("username", "안녕하세요");

        JpqlMember singleResult = query.getSingleResult();
        System.out.println("singleResult.getUsername() = " + singleResult.getUsername());

        tx.commit();
        ...
}

 

위와 같이 Param 이 들어갈 곳을 지정해준다음에, 나와서 해당 쿼리에 Params를 등록하게 된다. 위는 QUery 를 보여주려 한거고, 일반적으로 아래와 같이 호출한다. 

JpqlMember singleResult1 = em.createQuery("select m from JpqlMember m where m.username = :username", JpqlMember.class)
        .setParameter("username", "안녕하세요")
        .getSingleResult();

 

------=====--=-===

 

<<select 및 프로젝션>>

 

프로젝션 : select 절에 조회할 대상을 지정하는 것을 말한다. (그냥 항상 하던 거임

> select m from (엔티티 프로젝션)

> select m.team from (엔티티 프로젝션) >> 조회하는 곳이랑 다른 타입을 하면 Join 절이 나가게 됨

> select m.address from (임베디드 타입 프로젝션) 

> select m.username, m.age from ( 기본값, 스칼라 타입 프로젝션) 

> distinct 절로 중복 제거 ㄱㄴ

> 등등

 

>>>>> select 절은 조회 받는 모든 데이터들을 영속성 컨텍스트로 관리시켜준다 <<<<<

 

@Test
@DisplayName("JPQL Example : Select Projection 기능")
void test3() {
    // ...
        JpqlMember jpqlMember = new JpqlMember();
        jpqlMember.setUsername("Fucker");
        jpqlMember.setAge(10);
        em.persist(jpqlMember);

        // DB에 저장
        em.flush();
        em.clear(); // 영컨 초기화

        List<JpqlMember> members = em.createQuery("select m from JpqlMember m", JpqlMember.class)
                .getResultList();

        members.get(0).setUsername("Bitch");

        tx.commit();
    //...
}

위와 같이 select Query 로 받아오면 영속성 컨텍스트에서 관리를 시작해주고, set으로 변경사항을 적용 후 commit 하면 쓰기 지연 저장소에서 update query 가 나가게 된다. 

 

@Test
@DisplayName("JPQL Example : Select Projection Embedded")
void test4() {
        // ...
        JpqlOrder order = new JpqlOrder();
        order.setOrderAmount(1);
        JpqlAddress address = new JpqlAddress("city", "address");
        order.setAddress(address);
        em.persist(order);

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

        List<JpqlAddress> resultList = em.createQuery("select o.address from JpqlOrder o", JpqlAddress.class)
                .getResultList();

        for (JpqlAddress singleAddress :
                resultList) {
            System.out.println("singleAddress.getStreet() = " + singleAddress.getStreet());
        }

        tx.commit();
        // ...
}

위와같이 임베디드 타입에 대해서도 받아올 수 있다. 

JpqlAddress 만을 따로 from 해서 가져올 수는 없다. Entity 가 아니기 때문. 주인 Entity 를 통해서 불러와야 함. 

 

 

<참고로, m.username, m.age 처럼 여러개의 타입일 경우 어떻게 반환해야 하는가?>

 

1. Object 배열로 받아오는 방법.

List resultList = em.createQuery("select m.username, m.age from JpqlMember m").getResultList();

Object o = resultList.get(0);
Object [] result = (Object []) o;

System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

 

2. Object 배열로 타입 캐스팅을 해서 받아오는 방법

List<Object []> resultList = em.createQuery("select m.username, m.age from JpqlMember m").getResultList();

Object [] result = resultList.get(0);

System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

 

3. new 명령어 (DTO) 로 빠르게 조회하는 방법.

빠르게 Dto 를 선언해서 가져올 수 있다 ( 원하는 것을 가져오는 가장 좋은 방법이며, QueryDsl 에서도 이와 같이 했떤 경험이 있는데, 역시 결국 QueryDsl 은  JPQL 을 만들어주기 위함임) 

 

@Test
@DisplayName("Select projection : DTO 로 가져오기")
void test5() {
// ...
        JpqlMember jpqlMember = new JpqlMember();
        jpqlMember.setUsername("Fucker");
        jpqlMember.setAge(10);
        em.persist(jpqlMember);

        // DB에 저장
        em.flush();
        em.clear(); // 영컨 초기화

        List<MemberDto> resultList = em.createQuery("select new jpql.MemberDto(m.username, m.age) from JpqlMember m"
                , MemberDto.class).getResultList();

        MemberDto resultDto = resultList.get(0);
        System.out.println("resultDto.getUsername() = " + resultDto.getUsername());
        System.out.println("resultDto.getAge() = " + resultDto.getAge());
        
        tx.commit();
    //...
}

 

@Data
@AllArgsConstructor
public class MemberDto {

    private String username;
    private int age;

}

 

이와 같이 DTO 를 new 로 선언해서 빠르게 가져올 수 있음. 

> 패키지 명을 포함한 전체 클래스명 입력해야 함

> 생성자와 똑같은 순서에 유의해야함

 

<< 페이징 >>

 

Paging : 페이징이란 몇 개에서 부터 몇개까지 가져올래, 이게 다임. 

 

@Test
@DisplayName("JPQL PAGING")
void test1() {
//...
        for (int i = 0; i < 100; i++) {
            JpqlMember member1 = new JpqlMember();
            member1.setName("Member" + i);
            member1.setAge(i + 10);
            em.persist(member1);
        }

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

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

        System.out.println("resultList.size() = " + resultList.size());
        for (JpqlMember m :
                resultList) {
            System.out.println("JpqlMember = " + m);
        }
        tx.commit();
//    ...
}

 

하려고 하는 것 : 현재 JpqlMember 를 age 기준으로 desc 오더로 정렬하라

그리고 0번째가 아닌 첫번째부터 시작해서  10번째 순서인 애들을 출력해라 ( limit, offset 으로 쿼리가 나감) 

>> 이거 다른 DB 들 보면 진짜 복잡하게 해결하는 경우 많은데, JPA는 이렇게 간단하게 해줌. 

 

 

<< JOIN >>

 

조인은 동일하다 >> 객체스타일로 문법이 나가긴 함. 

 

@Test
@DisplayName("JPQL Join Test")
void test() {
//...
        JpqlTeam team1 = new JpqlTeam();
        team1.setName("Team 1");
        em.persist(team1);

        JpqlMember member1 = new JpqlMember();
        member1.setName("Member 1");
        member1.setAge(10);
        member1.changeTeam(team1);

        em.persist(member1);

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

        String query = "select m from JpqlMember m inner join m.team t";
        List<JpqlMember> resultList = em.createQuery(query, JpqlMember.class)
                .getResultList();
        //...
}

이와 같은 상황에서 SQL 이 출력되는 것을  보면, 

select (all _ val) from member m inner join team t on t.team_id = m.member_id; 이런식으로 나가는 것을 볼 수 있음. 

 

******** ON 조건 절 지원

 

1. 조인 대상을 필터링 할 수 있다. 

2. 연관관계 없는 엔티티 외부 조인 가능 (이거 대문에 네이티브 쿼리 짜는 경우 많은데, 이거 지원 함) 

 

>> 뭔소리냐? 

 

1번 예시 >> 회원과 팀을 조인하면서 팀 이름이 A 인 팀만 조인, 

sql 을 이렇게 만들고 싶은 거임 : select m.*, t.* from Member m join Team t on m.team_id = t.team_id and t.name = 'A';

추가적인 ON 절로 내보내는 것 

 

 

******** 서브쿼리

> 쿼리 안에 또다른 쿼리가 있는 것. ~IN, ~NOT IN 등 결과에 ~ 가 있으면 이런식으로 많이 씀. 

> JPA 는 WHERE / HAVING 절에서만 서브 쿼리 가능. Hibernate 에서 SELECT 도 지원해줌

select 예시 : "select (select avg(m1.age) From Member m1) as avAge from Member m join Team t on  m.username = t.username)?

> FROM 절의 서브 쿼리는 현재 JPQL 에서 불가능 / 조인으로 풀 수 있으면 풀어서 해결. 

> 잘 모르겠지만, FROM 절 서브쿼리가 ㅈㄴ 쓰고 싶다? >> 네이티브 SQL을 씀. 아니면 불러온 다음에 앱에서 조작. 근데 보통 그 방법 외에는 없는 경우는 잘 없긴 하다고 함. .

 

********** JPQL 타입 표현과 기타식

********** 조건식 (CASE 식) 

기본 CASE : 특정 조건이 만족하면 결과를 ~게 낸다

단수 CASE : EXACT 매칭일 시 결과를 ~게 낸다. 

>> DB에 있는 값이 아니더라도, 다음과 같은 CASE 문을 통해서 원하는 값들을 Parsing 할 수 있다

 

String query =
        "select "
                + "case when m.age <=10 then '학생요금' "
                + "     when m.age >=60 then '경로요금' "
                + "     else '일반요금' end "
                + "from JpqlMember m";

List<String> results = em.createQuery(query, String.class)
        .getResultList();

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

> 위와 같이 하면 지속되던 age = 10 인 예제로는 '학생요금' 만이 출력될 것이다. 

 

>> COALESCE : 하나씩 조회해서 NULL이 아니면 반환, NULL이면 지정 값 반환 or no 반환

String query = "select coalesce(m.username, '이름 없는 회원') from JpqlMember m";
List<String> results = em.createQuery(query, String.class)
        .getResultList();

for (String s :
        results) {
    System.out.println("s = " + s);
}

> 위와 같은 예제에서 m.username 이 NULL 인 회원은 '이름 없는 회원'이, 아닐경우는 m.usernam 이 출력된다. 

>> NULLIF : 두 값이 같으면 NULL 반환. 아닐 경우 원하는 값 반환

String query2 = "select nullif(m2.username, '관리자') from JpqlMember m2";

이렇게 짤 경우 m2.username 이 '관리자'일 경우 NULL 이라고 반환하게 된다. (좀 특이하기도 하고, coalesce 와 비슷한 것 같긴 한데 좀 다름) 

 

>>> nullif 와 coalesce 처럼, 사용 가능한 함수들이 있다 (JPQL 기본) 

: CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX 등등 ( MYSQL DIALECT 에 들어가면 MYSQL 한정 가능한 함수들도 많음) 

size 같은 경우

'다' 연관관계에 매핑되었을 경우 그 묶인 사이즈를 반환함. 

> ex: select sizeof(t.members) from JqplTeam t 라고 하면 t.members 로 반환되는 Members들의 수를 반환함. 

 

728x90
반응형

'Spring > JPA' 카테고리의 다른 글

[JPA] - 나머지 쿼리  (0) 2023.01.04
[JPA] JPQL 쿼리 2 (FETCH JOIN의 등장)  (0) 2023.01.03
[JPA] - 값 타입  (0) 2022.12.29
[JPA] 프록시와 연관관계 - 2  (0) 2022.12.28
[JPA] 프록시와 연관관계 정리  (0) 2022.12.26
'Spring/JPA' 카테고리의 다른 글
  • [JPA] - 나머지 쿼리
  • [JPA] JPQL 쿼리 2 (FETCH JOIN의 등장)
  • [JPA] - 값 타입
  • [JPA] 프록시와 연관관계 - 2
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    OOAD
    analyzer
    SRP
    Design Pattern
    n+1
    GoF
    mockito
    junit
    객체지향
    runtime exception
    lombok
    di
    BEAN
    composition
    디자인 패턴
    김영한
    decorator
    Spring
    Setter
    OOP
    spring boot
    lazy loading
    Java
    spring container
    Composite
    Configuration
    elasticsearch
    단위테스트
    k8s
    JPA
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[JPA] JPQL 쿼리
상단으로

티스토리툴바