0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] JPQL 기본 개념과 예제

힘들면힘을내는쿼카 2023. 2. 28. 17:19
728x90
반응형

[JPA] JPQL 기본 개념과 예제

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

JPA를 사용하면 엔티티 객체를 중심으로 개발을 해야합니다.
하지만 JPA만으로 100%의 문제를 해결할 수 없습니다. 🥲
그래서 다양한 쿼리 방법을 지원하는데요

  • JPQL
  • Querydsl
  • 네이티브 SQL
  • JDBC API 직접 이용
    • MyBatis, SpringJdbcTemplate

그 중에서 JPQL에 대해서 알아봅시다.

JPQL(Java Persistence Query Language)

순수 JPA를 사용할 때 가장 단순한 조회 방법은 다음과 같았습니다.

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

EntityTransaction tx = em.getTransaction();

tx.begin();

// 조회
em.find(Entity.class, Id);

tx.commit();

그런데 여기에서 조건을 추가하려면 어떻게 해야할까요? 🤔
예를 들어 나이가 20살 이상인 회원을 모두 검색하고 싶다면 어떻게 해야할까요?

순수 JPA의 한계

  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
    • JPA는 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건을 포함한 SQL이 필요!!

이러한 문제(검색 쿼리)를 해결하기 위해서 JPQL이 등장했습니다.

JPQL의 특징

  • JPASQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공
    • SQL을 추상화해서 특정 데이터베이스 SQL에 의존 🙅‍♂️
  • SQL 문법와 매우 유사
    • ANSI 표준: SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • 엔티티 객체를 대상으로 쿼리 생성
    • SQL은 데이터베이스 테이블을 대상으로 쿼리 생성

한마디로 JPQL테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리 입니다.

이제 JPQL을 이용해서
나이가 20살 이상인 회원을 모두 검색하는 JPQL을 작성해 봅시다.

실험

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

결과

Hibernate: 
    /* select
        m 
    from
        Member m 
    where
        m.age >= 20 */ select
            member0_.id as id1_6_,
            member0_.createDate as createda2_6_,
            member0_.lastModifiedDate as lastmodi3_6_,
            member0_.city as city4_6_,
            member0_.street as street5_6_,
            member0_.zipcode as zipcode6_6_,
            member0_.age as age7_6_,
            member0_.name as name8_6_ 
        from
            Member member0_ 
        where
            member0_.age>=20

JPQL(select m from Member m where m.age >= 20)이 SQL로 변환된것을 확인할 수 있습니다.

JPQL 문법

이제 기본적인 문법에 대해서 소개하겠습니다.

  • select m from Member as m where m.age > 20;
  • 엔티티와 속성은 대소문자 구분 🙆‍♂️
  • JPQL 키워드는 대소문자 구분 🙅‍♂️
  • 엔티티 이름 사용 🙆‍♂️
  • 테이블 이름 🙅‍♂️
  • 별칭은 필수(as 생략 가능)

TypeQuery와 Query

  • TypeQuery
    • 반환 타입이 명확할 때 사용
  • Query
    • 반환 타입이 명확하지 않을 때 사용
// 반환 타입이 Member로 명확
TypedQuery<Member> typeQuery = 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
    • 결과가 정확히 하나(단일 객체 반환)
    • 결과가 없음(NoResultException)
    • 결과가 하나 이상(NonUniqueResultException)
// 결과가 없어도 exception 발생 X
List<Member> members = em.createQuery("select m from Member m", Member.class)
        .getResultList();

// 결과가 없으면 exception 발생 O
Member member = em.createQuery("select m from Member m", Member.class)
        .getSingleResult();

파라미터 바인딩

이름기준 또는 위치 기준으로 바인딩이 가능합니다.

이름 기준

// 이름기반 바인딩
List<Member> members = em.createQuery("select m from Member m" +
                " where m.age < :age", Member.class)
        .setParameter("age", 20)
        .getResultList();

위치 기준
위치가 바뀌면 장애로 이어질 가능성이 크기 때문에 이름 기준 바인딩을 사용하는 것을 권장

// 위치기반 바인딩
List<Member> members = em.createQuery("select m from Member m" +
                " where m.age < :age", Member.class)
        .setParameter(1, 20)
        .getResultList();

프로젝션

지금까지는 엔티티 전체를 조회하는 방법에 대해서 배웠습니다.
그런데 엔티티 전체가 정말로 필요할까요?
정말로 딱 필요한 데이터만 조회하기 위한 방법으로 프로젝션을 사용하면 됩니다.^^

  • SELECT 절에 조회할 대상을 지정하는 방법
  • 프로젝션 대상
    • 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자… 기본 데이터 타입)
    • 엔티티 프로젝션(영속성 컨텍스트 관리)
      • select m from Member m
      • select m.team from Member m
      • select m.team from Member m이것보다는 select t from Member m join m.team t 으로 하는게 좋음(SQL 예측과 가독성에서 이점)
    • 임베디드 타입 프로젝션
      • select m.address from Member m
    • 스칼라 타입 프로젝션
      • select m.uesrname, m.age from Member m
    • DISTINCT로 중복 제거

스칼라 타입 프로젝션은 어떻게 값을 가져오지?🤔

Query 타입으로 조회 후 Object[] 타입으로 변환

tx.begin();

Member member = new Member();
member.setUsername("춘식이");
member.setAge(3);
member.setAddress(new Address("화성시", "화성로", "942"));
em.persist(member);

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

// Query 타입으로 조회
Query query = em.createQuery("select m.username, m.age from Member m");

// Object -> Object[] 로 변환
List resultList = query.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;

System.out.println("username="+result[0]); // 춘식이
System.out.println("age="+result[1]); // 3

tx.commit();

new 명령어로 조회
MemberDto 생성

@Data
@AllArgsConstructor
public class MemberDto {
    private String username;
    private int age;
}

DTO로 바로 조회가 가능합니다! 👍
패키지 명을 포함한 전체 클래스 명을 입력해야 합니다.
(순서와 타입이 일치하는 생성자 필요)

List<MemberDto> resultList =
        em.createQuery(
                "select new com.study.MemberDto(m.username, m.age)" +
                " from Member m", MemberDto.class)
        .getResultList();

페이징 API

  • setFirstResult(int startPosition)
    • 조회 시작 위치(0부터 시작)
  • setMaxResult(int maxResult)
    • 조회할 데이터 갯수
List<Member> result = em.createQuery("select m from Member m" +
                " order by m.age desc", Member.class)
        .setFirstResult(0) // 조회 시작 위치
        .setMaxResults(10) // 조회할 데이터 갯수
        .getResultList();

Hibernate가 정말로 좋은게 방언에 맞게 쿼리를 생성한다는 점 입니다.!
MySQL 방언

Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.age desc */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.team_id as team_id7_0_,
            member0_.username as username6_0_ 
        from
            Member member0_ 
        order by
            member0_.age desc limit ?

Oracle 방언

Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.age desc */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.team_id as team_id7_0_,
            member0_.username as username6_0_ 
        from
            Member member0_ 
        order by
            member0_.age desc fetch first ? rows only

조인

조인을 모르면 개발을 할수 없다는 말이 있습니다. ㅎㅎㅎ
그만큼 조인은 중요하다는 의미겠죠?
조인에 대해서 알아봅시다.

내부 조인
select m from Member m [inner] join m.team t
team에 데이터 없으면 조회 안됨

String sql = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(sql, Member.class)
        .getResultList();
Hibernate: 
    /* select
        m 
    from
        Member m 
    inner join
        m.team t */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.team_id as team_id7_0_,
            member0_.username as username6_0_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.team_id=team1_.id

inner join Team team1_ on member0_.team_id=team1_.id이 발생한 것을 확인할 수 있습니다.

외부 조인
select m from Member m left [outer] join m.team t
team에 데이터 없어도 조회 됨(단, teamnull)

String sql = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(sql, Member.class)
        .getResultList();
Hibernate: 
    /* select
        m 
    from
        Member m 
    left outer join
        m.team t */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.team_id as team_id7_0_,
            member0_.username as username6_0_ 
        from
            Member member0_ 
        left outer join
            Team team1_ 
                on member0_.team_id=team1_.id

left outer join Team team1_ on member0_.team_id=team1_.id이 발생한 것을 확인할 수 있습니다.

세타 조인
select count(m) from Member m, Team t where m.username = t.name
연관관계가 없는 것을 조인하는 SQL

String sql = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(sql, Member.class)
        .getResultList();
Hibernate: 
    /* select
        m 
    from
        Member m,
        Team t 
    where
        m.username = t.name */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.team_id as team_id7_0_,
            member0_.username as username6_0_ 
        from
            Member member0_ cross 
        join
            Team team1_ 
        where
            member0_.username=team1_.name

join Team team1_ where member0_.username=team1_.name이 발생한 것을 확인할 수 있습니다.

ON절 조인(JPA 2.1 지원)

  • 조인 대상 필터링
    e.g) 회원과 팀을 조인 하면서, 팀 이름이 A인 팀만 조인
    JPQL: select m, t 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’;
String sql = "select m, t from Member m left join m.team t on t.name = 'teamA'";
List<Member> result = em.createQuery(sql, Member.class)
        .getResultList();
Hibernate: 
    /* select
        m,
        t 
    from
        Member m 
    left join
        m.team t 
            on t.name = 'teamA' */ select
                member0_.id as id1_0_0_,
                team1_.id as id1_3_1_,
                member0_.city as city2_0_0_,
                member0_.street as street3_0_0_,
                member0_.zipcode as zipcode4_0_0_,
                member0_.age as age5_0_0_,
                member0_.team_id as team_id7_0_0_,
                member0_.username as username6_0_0_,
                team1_.name as name2_3_1_ 
        from
            Member member0_ 
        left outer join
            Team team1_ 
                on member0_.team_id=team1_.id 
                and (
                    team1_.name='teamA'
                )
  • 연관관계 없는 엔티티 외부 조인
    e.g) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
    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;
String sql = "select m, t from Member m left join Team t on m.username = t.name";
List<Query> result = em.createQuery(sql)
        .getResultList();
Hibernate: 
    /* select
        m,
        t 
    from
        Member m 
    left join
        Team t 
            on m.username = t.name */ select
                member0_.id as id1_0_0_,
                team1_.id as id1_3_1_,
                member0_.city as city2_0_0_,
                member0_.street as street3_0_0_,
                member0_.zipcode as zipcode4_0_0_,
                member0_.age as age5_0_0_,
                member0_.team_id as team_id7_0_0_,
                member0_.username as username6_0_0_,
                team1_.name as name2_3_1_ 
        from
            Member member0_ 
        left outer join
            Team team1_ 
                on (
                    member0_.username=team1_.name
                )

서브 쿼리

나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2);

참고:
메인 쿼리와 서브 쿼리가 서로 관계가 없음(메인 쿼리는 m, 서브 쿼리는 m2 사용)
이러한 방식으로 서브 쿼리를 작성해야 성능이 잘 나온다.

한 건이라도 주문한 고객
select m from Member m where where (select count(o) from Order o where m = o.member) > 0;

참고:
메인 쿼리와 서브 쿼리가 서로 관계가 있음(메인 쿼리도 m, 서브 쿼리도 m 사용)

팀A 소속인 회원(exists)
select m from Member m where exists (select t from Team t where t.name = ‘A’);

전체 상품 각각의 재고보다 주문량이 많은 주문들(all)
select o from Order o where o.orderAmount > all (select p.stockAmount from Product p);

어떤 팀이든 팀에 소속된 회원(any)
select m from Member m where m.team = any (select t from Team t);

JPA 서브 쿼리 한계

JPQL 타입 표현

  • 문자: ‘hello’, ‘he”s’
  • 숫자: 10L, 10D, 100F
  • Boolean: TRUE, FALSE
  • ENUM: jpa.MemberType.Admin (패키지명 포함)
    select m.username, 'hello', true from Member m where m.type = jpa.MemberType.Admin;
  • 엔티티 타입: TYPE(i) = Book (상속 관계에서 사용)
    select i from Item i where type(i) = Book;

기타 JPQL

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

회원 나이가 없으면 내 나이가 어때서~ 반환
select coalesce(m.age, '내 나이가 어때서~') from Member m;

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

m.age가 999 이면 null 반환, 나머지는 m.age 반환
select null if(m.age, 999) from Member m;

정리

JPQL은 거의 웬만한 SQL 문법은 지원하니
검색을 통해 찾아보면서 개발하는게 좋다고 생각합니다.🤗

 

 

728x90
반응형