공부/JPA

객체지향 쿼리 언어1 - 기본 문법

Stair 2024. 12. 30. 14:51
반응형

JPA는 다양한 쿼리 방법을 지원한다.

- JPQL

- JPA Criteria

- QueryDSL

- 네이티브 SQL

- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate

 

JPQL

- 가장 단순한 조회 방법이다.

  - EntityManager.find()

  - 객체 그래프 탐색(a.getB().getC())

 

만약 나이가 18살 이상인 회원을 모두 검색하고 싶다면 어떻게 해야 할까?

JPA를 사용하면 엔티티 객체를 중심으로 개발하게 되는데 문제는 검색 쿼리이다.

검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.

모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능 하다.

애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.

 

JPQL은 위와 같은 문제를 해결하기 위한 SQL을 추상화한 객체 지향 쿼리 언어이다. 특징은 다음과 같다.

1. SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원

2. JPQL은 엔티티 객체를 대상으로 쿼리한다. <-> SQL은 데이터베이스 테이블을 새앙으로 쿼리

 

간단하게 JPQL을 알아보자.

List<Member> result = em.createQuery(
        "select m From Member m where m.name like '%kim%'",
        Member.class
).getResultList();//이 Member는 Entity

for (Member member : result) {
    System.out.println("member = " + member);
}

tx.commit();

 

select m From Member m <--테이블이 아닌 엔티티를 가리킨다.

 

이 부분을 보면 SELCT ~ FROM에  Member as m인 Member객체 자체를 조회해 오라고 쿼리가 짜여져 있다.

JPQL은 엔티티 객체를 대상으로 검색하기 때문에 이런 쿼리가 가능하다.

 

JPQL

- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이다.

- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

- JPQL을 한마디로 정의하면 객체 지향 SQL이다.

 

 

하지만 이런 JPQL은 동적 쿼리를 짜기 어렵다. 쿼리가 문자열 자체이기 때문에 슬라이싱 하고 더하며 쿼리를 만들어 내야 하는데 이런 쿼리를 짜는 것은 버그도 많고  쉽지 않기 때문이다.

 

Creteria

JPQL의 해결하기 위한 Criteria라는 것이 있다.

//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

Root<Member> m = query.from(Member.class);

CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("name"), "kim"));
List<Member> resultList = em.createQuery(cq)
        .getResultList();

 

Criteria는 자바 코드로 쿼리를 짜는 것이다. 그렇기에 잘못 치게되면, 컴파일 오류가 난다.

또한 동적 쿼리를 짜기에 JPQL보다 훨씬 유리하다.

 

Criteria

- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음

- JPQL 빌더 역할

- JPA 공식 기능

- 단점 : 너무 복잡하고 실용성이 없다.

- Criteria 대신에 QueryDSL 사용 권장

 

사실 Criteria를 실무에서 쓰기에 쉽지 않다. 그렇기에 깊게 배우지 않아도 된다.

 

 

QueryDSL

QueryDSL은 초기 세팅이 복잡하지만, SQL과 거의 비슷하고,  동적 쿼리를 짜기에 유리하며, Criteria보다 쉽다.

- 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.

- JPQL 빌더 역할을 한다.

- 컴파일 시점에 문법 오류를 찾을 수 있다.

- 동적쿼리 작성이 편리하다.

- 단순하고 쉽다.

- 실무 사용이 권장된다.

 

QueryDSL을 잘 하기 위해서는 JPQL을 잘 알아야 한다. JPQL을 잘 하게 되면 QueryDSL은 거의 공짜로 날먹 가능하다.

그렇기에 JPQL을 배우고 잘 할 줄 알아야 하는 것이다.

 

 

네이티브 SQL

- JPA가 제공하는 SQL을 직접 사용하는 기능

- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능이다.

- 예)오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트

 

 

JDBC 직접 사용, SpringJdbcTemplate 등

- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, MyBatis등을 함께 사용 가능하다.

- 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 해야한다.

  ex) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시 한다.

 

 

 

JPQL(Java Persistence Query Language)

실무에서는 왠만하면 JPQL과 QueryDSL로 쿼리를 짜게 된다.

 

JPQL은 객체지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

JPQL은 결국 SQL로 변환된다(결국 DB는 SQL만 받기 때문에)

 

JPQL문법

JPQL은 SQL과 마찬가지로, 셀프웨구해오 순으로 작성된다.

(SELECT - FROM - WHERE - GROUP BY - HAVING - ORDER BY)

이전에 확인했듯이 JPQL은 실행 시점에 자동으로 flush 하게 된다.

 

JPQL의 문법 정리

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

- 엔티티와 속성은 대소문자를 구분한다. (Member, age)

- JPQL 키워드는 대소문자를 구분하지 않는다. (SELECT, FROM, where 등)

- 테이블 이름이 아닌 엔티티 이름을 사용한다(Member)

- 별칭은 필수이다.(m) (as는 생략 가능)

 

집합과 정렬

또한 JPQL은 ANSI 표준에서 지원하는 합, 최대, 최소, 평균, 카운트 및 GroupBy, OrderBy 등을 지원한다.

select

    COUNT(m), //회원수

    SUM(m.age), //나이 합

    AVG(m.age), //평균 나이

    MAX(m.age), //최대 나이

    MIN(m.age) //최소 나이

from Member m

 

TypeQuery : 반환 타입이 명확할 때 사용한다.

Query : 반환타입이 명확하지 않을 때 사용한다.

아래와 같은 코드를 보자.

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
Query query3 = em.createQuery("select m.username, m.age from Member m");

Member 객체타입으로 명확하게 반환하거나 Member의 name(name이 String이기 때문에)이 String타입으로 명확하게 반환될 땐  TypeQuery가 사용된다

 

하지만 아래의 query3을 보면

select m.username, m.age from Member m

m.username : String이고

m.age : int 타입이다.
따라서 반환 타입이 명확하지 않기에 Query를 사용한다.

 

결과조회 API

query.getResultList() : 결과가 하나 이상일 떄, 리스트를 반환한다.

  - 결과가 없으면 빈 리스트를 반환한다.

query.getSingleResult() : 결과가 정확하게 하나일 때, 단일 객체를 반환한다.

  - 결과가 없으면 NoResultException이 터지고, 둘 이상일땐 NonUniqueResultException이 터진다.

TypedQuery<Member> query = 
        em.createQuery("select m from Member m", Member.class);
Member result = query.getSingleResult();
System.out.println("result = " + result);

 

파라미터 바인딩 - 이름 기준, 위치 기준

파라미터는 이름 기준이나 위치 기준으로 바인딩할 수 있다.

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

TypedQuery<Member> query =
        em.createQuery("select m from Member m where m.username= :username", Member.class);
query.setParameter("username", "member1");
Member result = query.getSingleResult();
System.out.println("result = " + result.getUsername());

위의 메서드들은 메서드 체이닝을 통해 엮는것이 좋다.

Member result = em.createQuery("select m from Member m where m.username= :username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult();
System.out.println("result = " + result.getUsername());

 

위치 기반 바인딩 <-- 왠만하면 쓰지 말자, 위치가 변경될 시 장애로 이어질 수 있다.

 

 

프로젝션

- select 절에 조회할 대상을 지정하는 것이다.

- 프로젝션의 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)

- SELECT m FROM Member m -> 엔티티 프로젝션(Member가 엔티티이기에)

- SELECT m.team FROM Member m -> 엔티티 프로젝션(team이 엔티티이기에)

- SELECT m.address FROM Member m -> 임베디드 타입 프로젝션 (address가 임베디드 타입이기에)

- SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션

- DISTINCT로 중복 제거가 가능하다.

 

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);

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

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

Member findMember = result.get(0);
findMember.setAge(20);

만약 영속성 컨텍스트를 flush하고 clear 한 후 createQuery를 통해 컬렉션을 가져온 후 그 속에 있는 값을 수정하면 어떻게 될까?

em.createQuery로 긁어온 값은 전부 영속성 컨텍스트에 올라가지기 때문에 긁어온 값을 수정하게 되면, DB에 값이 반영된다.

SELECT * FROM MEMBER;

AGE  TEAM_ID  ID  USERNAME  

20 null 1 member1

(1 row, 4 ms)

 

 

만약 m.team을 select하고 싶을땐 어떡할까?

List<Team> result = em.createQuery("select m.team from Member m", Team.class)
        .getResultList();

이럴땐 result 결과를 Team으로 받아야한다.

대신 조심해야 할 점은, 다른 테이블에 있기에 조인 쿼리가 나간다는 점이다.

JPQL은 SQL이랑 최대한 비슷한 쿼리를 맞춰 나가야 한다. JOIN은 성능에 줄 수 있는 요소도 많고, 튜닝할 요소도 많기 때문에 한눈에 볼 수 있어야 하기 때문이다.

List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class)
        .getResultList();

이렇게 해야 JPQL을 보고 예측이 되기 때문이다.

 

그렇기에 JOIN은 명시적으로 표기해 주는 것이 좋다.

 

 

 

em.createQuery("select distinct m.username, m.age from Member m")
        .getResultList();

위처럼 여러 타입의 값이 나열되어 있을 때는 다음과 같은 세가지 방법으로 조회할 수 있다.

1. Query 타입으로 조회하기

2. Object[] 타입으로 조회하기

3. new 명령어로 조회하기

  - 단순 값을 DTO로 바로 조회 SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

  - 패키지 명을 포함한 전체 클래스 명 입력

  - 순서와 타입이 일치하는 생성자 필요

 

우선 쿼리 타입으로 조회하기는 다음과 같다.

타입을 명기하지 못하기에 Object로 돌리는 것이다.

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

Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("result = " + result[0]);
System.out.println("result = " + result[1]);

result = member1
result = 10

 

두번째로는 Object[]타입으로 조회하기이다.

얘는 그냥 제네릭에 Object[]타입을 선언해버린다.

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

Object[] result = resultList.get(0);
System.out.println("result = " + result[0]);
System.out.println("result = " + result[1]);

result = member1
result = 10

 

마지막으로 제일 깔끔한 방법인 new 명령어로 조회하는 방법이 있다.

public class MemberDTO {

    private String username;
    private int age;

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

 DTO클래스를 하나 생성하여준다. 이 DTO를 통해 조회할 예정이다.

순서와 타입이 일치하는 생성자가 필수이다.

List<MemberDTO> result = em.createQuery(
                "select new hellojpa.MemberDTO(m.username, m.age) from Member m",
        MemberDTO.class).getResultList();

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

 

 

 

 

페이징 API

일반 DB의 SQL은 페이징이 정말 복잡하지만 JPA에서 제공하는 페이징은 정말 간단하다.

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

1. setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)

2. setMaxResult(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> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
        .setFirstResult(1)
        .setMaxResults(10)
        .getResultList();

System.out.println("result.size() = " + result.size());

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

Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.age desc */ select
            m1_0.id,
            m1_0.age,
            m1_0.TEAM_ID,
            m1_0.username 
        from
            Member m1_0 
        order by
            m1_0.age desc 
        offset
            ? rows 
        fetch
            first ? rows only
result.size() = 10
result = [Member{id=99, username='member98', age=98}, Member{id=98, username='member97', age=97}, Member{id=97, username='member96', age=96}, Member{id=96, username='member95', age=95}, Member{id=95, username='member94', age=94}, Member{id=94, username='member93', age=93}, Member{id=93, username='member92', age=92}, Member{id=92, username='member91', age=91}, Member{id=91, username='member90', age=90}, Member{id=90, username='member89', age=89}]

 

 

위와 같은 결과가 나온다.

 

 

조인

조인도 역시 엔티티를 중심으로 하는 것이기에 객체 스타일로 문법이 짜여져있다.

 

내부 조인 : SELECT m FROM Member m [INNER] JOIN m.team t -- team이 없으면 데이터가 안나옴

외부 조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t -- team이 없어도 team은 null로 하여 조회됨

세타 조인 : select count(m) from Member m, Team t where m  -- 연관관계가 없어도 긁어옴

 

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

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);

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

String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

System.out.println("result.size() = " + result.size());

 

 

String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

 

 

String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

System.out.println("result.size() = " + result.size());

 

 

조인 - ON절

ON절을 활용한 조인(JPA 2.1부터 지원)

- 1. 조인 대상 필터링

- 2. 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

String query = "select m from Member m left join m.team t on t.name='teamA'";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

 

String query = "select m from Member m left join Team t on m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

 

 

서브 쿼리

서브 쿼리는 쿼리 내부에 서브로 쿼리를 만드는 것이다.

ex ) 나이가 평균보다 많은 회원

select m from Member m where m.age > (select avg(m2.age) from Member m2)

ex ) 한 건이라도 주문한 고객

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) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

 

JPA에서의 서브 쿼리는 WHERE, HAVING절에서만 서브 쿼리를 사용할 수 있다.

- SELECT절도 가능하다(* 하이버네이트에서 지원하기 떄문에)

FROM절의 서브 쿼리는 현재 JPQL에서 불가능하다.

  - 조인으로 풀 수 있으면 풀어서 해결하도록 하자.

 

JPQL 타입 표현

- 문자 : 'HELLO', 'SHE''s'

- 숫자 : 10L(Long), 10D(Double), 10F(Float)

- Boolean : TRUE, FALSE

- ENUM(*자바 패키지명을 다 넣어야한다.) : hellojpa.MemberType.Admin

-엔티티 타입 : TYPE(m) = Member(상속 관계에서 사용한다)

 

 

조건식 - CASE 식

 

기본 CASE식

select

    case when m.age <= 10 then '학생요금'

            when m.age >= 60 then '경로요금'

            else '일반요금'

    end

from Member m

 

단순 CASE식

select

    case t.name

            when '팀A' then '인센티브 110%'

            when '팀B' then '인센티브 120%'

            else '인센티브105%'

    end

from Team t

 

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

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

 

 

 

 

 

JPQL 기본 함수(JPQL이 제공하는 표준 함수)

- CONCAT

- SUBSTRING

- TRIM

- LOWER, UPPER

- LENGTH

-LOCATE

- ABS, SQRT, MOD

- SIZE, INDEX

 

사용자 정의 함수 호출

- 하이버네이트는 사용 전 방언에 추가해야 한다.

  - 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.

select function('group_concat', i.name) from Item i

 

반응형

'공부 > JPA' 카테고리의 다른 글

객체지향 쿼리 언어2 - 중급 문법  (1) 2025.01.03
값 타입  (0) 2024.12.23
프록시와 연관관계 정리  (0) 2024.12.21
JPA 고급 매핑  (2) 2024.12.08
다양한 연관관계 매핑  (0) 2024.12.03