Programming-[Backend]/JPA

[JPA기본] 12. 객체지향 쿼리 언어 : JPQL-기본문법

컴퓨터 탐험가 찰리 2021. 10. 24. 20:23
728x90
반응형

 

 

1. JPA에서 제공하는 SQL 방식 소개

JPA는 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL, JDBC API 직접 사용 등의 여러 쿼리 방법을 지원한다.

 

JPQL

JPQL은 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다. 다만 SQL은 DB 테이블을 대상으로 쿼리하는데, JPQL은 엔티티를 대상으로 쿼리한다는 차이가 있다. select * 이 아니라 select m from Member m 이라고 사용하는 점에서도 이를 유추할 수 있다. 그리고 SQL을 추상화했으므로 특정 데이터베이스 vendor에 의존하지 않는다는 장점이 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tx.begin();
 
    try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      List<Member> results = em.createQuery("select m from Member m ", Member.class)
              .getResultList();
 
      System.out.println("results = " + results);
 
      tx.commit();
 
    } catch (Exception e) {
cs

 


criteria

JPQL은 단순 문자열이기 때문에, 변수값을 받고 조건을 체크하는 등 동적 쿼리를 하기가 어렵다. 그래서 대안으로 나온 것이 criteria이다. 메소드를 제공해주고, 단순 String이 아닌 자바 코드로 작성되기 때문에 오타를 내면 컴파일 시점에 미리 오류를 잡아주고, 동적 쿼리를 짜기 유리하다는 장점이 있다. 다만 문법이 SQL문과 달라서 알아보기 어렵다. 그리고 너무 복잡해서 실무에 적용이 힘들다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tx.begin();
 
    try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      //Criteria 사용준비
      CriteriaBuilder cb = em.getCriteriaBuilder();
      CriteriaQuery<Member> query = cb.createQuery(Member.class);
 
      Root<Member> m = query.from(Member.class);
      CriteriaQuery<Member> criteriaQuery = query.select(m).where(cb.equal(m.get("name"), "%kim%"));
 
      List<Member> results = em.createQuery(criteriaQuery).getResultList();
      System.out.println("results = " + results);
 
      tx.commit();
 
    } catch (Exception e) {
cs

 


Querydsl

SQL과 비슷하고, 오타를 내더라도 바로 에러를 잡아준다. 그래서 criteria 대신 사용하기를 권장한다. 동적쿼리도 BooleanExpression 등을 적용하여 사용하면 된다.
다만 QueryDsl도 JPQL 기반이며, 하나의 라이브러리이기 때문에 이번에는 기본이 되는 JPQL의 문법에 대해 알아본다.

 


네이티브 SQL

네이티브 SQL은 특정 데이터베이스 vendor에 종속적인 쿼리가 필요한 경우 또는 복잡한 데이터를 조회해야 하는 경우 사용한다.
em.createNativeQuery 문법을 사용하며, DB에 실제 SQL을 전송하는 방법이다.

 


JDBC, SpringJdbcTemplate 등

바로 사용할 수도 있지만, 이럴 경우 tx.commit, query 나갈 때 자동적으로 발생되는 flush가 나가지 않기 때문에 em.flush()를 적절히 해주어야 한다.

 

 


 

2. JPQL 기본 문법

 

 

 

기본 작성 규칙

  • 기본적인 규칙은 다음과 같다.
  • 엔티티와 속성은 대소문자를 구분한다.
  • JPQL 키워드는 대소문자 구분을 하지 않는다.
  • 별칭(alias)은 필수이며, as는 생략가능하다.

 

TypeQuery와 Query

반환 타입이 명확할때는 TypedQuery를 쓴다. 타입 정보를 받을 수 없을때는 Query를 사용한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      Query select_m_from_member_m = em.createQuery("select m from Member m");
      TypedQuery<Member> select_m_from_member_m1 = em.createQuery("select m from Member m", Member.class);
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

getResultList()와 getSingleResult()

.getResultList() : 결과가 1개 이상일때. 만약 결과가 없으면 빈 리스트를 반환한다.
.getSingleResult() : 결과가 정확히 1개 일때. 결과가 없으면 NoResultException, 1개가 아니면 NotUniqueException이 발생한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      List result = em.createQuery("select m from Member m").getResultList();
 
      tx.commit();
 
    } catch (Exception e) {
cs


파라미터 바인딩

이름기준 - 콜론(:) 뒤에 변수명을 쓰면 된다. :username, 그리고 query.setParameter("username", "member1")과 같이 parameter를 설정한다.
위치기준 - ?1, query.setParameter(1,usernameParam); 으로 쓸 수 있으나 위치가 변경되거나 숫자를 잘못 쓸 경우가 있으므로 사용하지 않는걸 권장한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      //이름 기준 파라미터 set
      List result = em.createQuery("select m from Member m where m.id = :id ")
              .setParameter("id"1)
              .getResultList();
      //위치 기준 파라미터 set
      List result2 = em.createQuery("select m from Member m where m.id = ?1 ")
              .setParameter(11)
              .getResultList();
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 

 


 

3. 프로젝션(Projection)

 

프로젝션은 SELECT 절에 조회할 대상을 지정하는 것을 의미한다. 위 예제처럼 전체 Member 엔티티의 필드값을 가져오는 것이 아니라 특정 필드값만 SELECT로 가져오고 싶거나, 필드값들의 조합값을 가져오고 싶을 때 프로젝션을 사용한다.

 

엔티티 타입 프로젝션

일반적인 엔티티 프로젝션을 하면, 엔티티에 포함된 모든 필드가 영속성 컨텍스트에서 관리된다. 아래 코드에서 불러온 Member 객체의 name값을 변경 후 조회해보면, 변경된 값이 조회되는 것을 확인할 수 있다. 즉 영속성 컨텍스트에서 name 값을 관리하고 있다는 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tx.begin();
 
    try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      List<Member> result = em.createQuery("select m from Member m ", Member.class)
              .getResultList();
 
      Member findMember = result.get(0);
      findMember.setName("변경됨!");
 
      System.out.println("findMember.getName() = " + findMember.getName());
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 

 

스칼라 타입 프로젝션

엔티티의 각 필드값들을 선택적으로 조회하는 것을 스칼라 프로젝션이라고 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      List<Member> result = em.createQuery("select m.name, m.period from Member m ", Member.class)
              .getResultList();
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 

특정 DTO 타입으로 프로젝션

다른 곳에서 사용하기 위해서 DTO 타입으로 조회할 필드값들을 설정해놓고, 그것으로 바로 쿼리문을 만들 수 있다. MemberDTO를 생성해놓고, 쿼리문을 날리면서 MemberDTO.class 타입으로 조회하면 된다. 여기서 select 문을 사용할 때, new 키워드를 사용하고 해당 클래스가 있는 위치를 java 아래 패키지를 모두 적어주어야 한다.

 

그리고 select문으로 조회할 필드값들을 포함한 생성자를 MemberDTO에 만들어줘야한다. 이 생성자는 순서와 타입이 일치하는 생성자여야 한다! 쿼리문에서 필드 종류를 다르게 조회할 때마다 생성자가 필요할 수 있어 불편할 수 있을 것 같다. 다만, 한번 생성한 쿼리를 자주 바꾸지는 않으므로 큰 문제는 되지 않을 것 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
public class MemberDTO {
 
  private String name;
  private Period period;
 
  public MemberDTO(String name, Period period) {
    this.name = name;
    this.period = period;
  }
}
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tx.begin();
 
    try {
 
      Member member = new Member();
      member.setName("member1");
      member.setHomeAddress(new Address("city1""road1"1234));
 
      em.persist(member);
 
      List<MemberDTO> resultList = em.createQuery("select new com.example.jpamain.domain.MemberDTO(m.name, m.period) from Member m "MemberDTO.class)
              .getResultList();
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 


 

4. 페이징

 

 

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

 

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

setMaxResults(int maxResult) : 조회할 데이터 수

 

 

Member 엔티티를 리스트로 작성하고, 하나씩 persist 한 다음 쿼리문으로 조회해본다. 순서대로 결과가 출력되도록 order by 구문을 추가해준다. 그냥 실행하면 member 객체 자체가 출력된다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
tx.begin();
 
    try {
//리스트로 생성
      List<Member> members = IntStream.range(0100).boxed().map(i -> {
        Member member = new Member();
        member.setName("member " + i);
        member.setAge(i);
        member.setHomeAddress(new Address("city1""road1"1234));
//리스트 전체를 persist는 못하고, 엔티티 객체 1개씩만 persist 가능하다!
        em.persist(member);
        return member;
      }).collect(Collectors.toList());
 
      em.flush();
      em.clear();
 
      List<Member> resultList = em.createQuery("select m from Member m order by m.age desc ", Member.class)
              .setFirstResult(0)
              .setMaxResults(10)
              .getResultList();
 
      System.out.println("resultList.size() = " + resultList.size());
      for (Member member1 : resultList) {
        System.out.println("member1 = " + member1);
      }
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 

정확히 age 별로 order by가 되었는지 확인하기 위해서 Member 클래스에 ToString을 Override 한다. 이 때 유의점은, 연관관계를 갖는 Team 등은 꼭 필요하지 않다면 삭제해준다. 왜냐하면 앞서 배운 바와 같이 Team 엔티티에서도 ToString을 사용하고 있으면 무한 루프가 되어 StackOverFlow가 발생할 수 있기 때문이다.

 

Member.class 일부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @Override
  public String toString() {
    return "Member{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
//            ", period=" + period +
//            ", homeAddress=" + homeAddress +
//            ", favoriteFoods=" + favoriteFoods +
//            ", addressHistory=" + addressHistory +
//            ", workAddress=" + workAddress +
//            ", team=" + team +
            '}';
  }
cs

 

toString Override 후 결과

 

 

방언별로 다르게 쿼리문 생성

위 예제에서 생성된 쿼리문의 일부는 아래와 같다. limit ? offset ? 을 주도록 되어있는데, setFirstResult(0)으로 하여 딱 처음부터 시작하게 되면 offset 자체가 작성되지 않는다.

 

 

persistence.xml 에서 database 방언을 변경하고, setFirstResult로 1로 바꾸어서 다시 실행해보자. rownum 등의 변경된 방언으로 바꿔주는 것을 확인할 수 있다.

 

persistence.xml 의 일부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect"/>
            <!-- 옵션 -->
<!--            <property name="hibernate.show_sql" value="true"/>-->
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="false"/>
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
cs

...

 

참고로 Oracle의 경우 일반적인 페이징을 위해 위와 같이 select from 이 3 depth로 들어가야 하는 것을 확인할 수 있다. 상당히 불편하다.

 

 


 

5. Join

 

기본 join 종류

조인에는 내부 조인(inner join), 외부 조인(outer join), 세타조인(theta join)이 있다. 간단하게 말하면 외부조인은 Member 데이터가 5개 있는데 Team을 가진 데이터가 3개 밖에 없더라도, 5개를 전부 행으로 표현하고 나머지 2개는 null 값으로 처리하는 방식이다. 내부조인은 Team을 갖는 데이터인 3개만 표시하는 방식이다. 세타조인은 아래 강의에서의 예제와 같이 연관관계와 상관없이 원하는 정보가 일치하는 데이터를 모두 불러오는 방식이다.

 

Theta Join : select count(m) from Member m, Team t where m.username = t.name

 

참고로, Member m, Team t라고 해서 엔티티를 전부 한번에 불러오는 것을 카르테시안 곱으로 불러온다고 표현한다.

 

 

inner join 쿼리 작성 예시

참고로 inner는 생략될 수 있다. left join도 사실 left outer join인데 outer가 생략된 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tx.begin();
 
    try {
      Team team = new Team();
      team.setName("teamA");
      em.persist(team);
 
      Member member = new Member();
      member.setName("member1");
      member.setAge(10);
      member.changeTeam(team);
      em.persist(member);
 
      String query= "select m from Member m inner join m.team t where m.id = 1";
      List<Member> result = em.createQuery(query, Member.class).getResultList();
 
      tx.commit();
 
    } catch (Exception e) {
cs

 

 

ON 절 활용

 

필터링

다음과 같이 on절을 활용하면 join 시에 조건을 걸 수 있다. member에서 team을 조인해올 때, team.name = 'A'인 대상들만 조인해오게 된다.

 

String query= "select m from Member m left join m.team t on t.name = 'A' ";

 

 

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

세타 조인에서 살펴본 바와 같이, 전혀 연관관계가 없는 대상을 외부 조인할 때 on절을 사용한다.

 

String query= "select m, t from Member m left join Team t on m.name = t.name";

 

 


 

6. 서브 쿼리

 

SELECT, WHERE 절에 서브쿼리가 가능하다

SQL과 같이 select, where 절에 서브쿼리를 넣을 수 있다. 강의에서의 예제 코드는 다음과 같다. 3번째 줄에서 같은 Member 엔티티를 조회해도 alias를 다르게 줌으로써 2번 조회할 수 있다는 것도 참고하자.

 

1
2
3
4
5
6
7
--나이가 평균보다 많은 회원(select 서브쿼리)
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
cs

 

서브쿼리 지원함수

exist, all, any(some) 등 서브쿼리 지원함수도 제공한다. exists는 조건문을 감싸는 형태인 반면에, ALL과 ANY는 비교연산자의 한 쪽 절에만 작성한다는 것도 참고하자.

 

  • [not] exist : 서브 쿼리에 결과가 존재하면 참
  • all : 모두 만족하면 참
  • any, some : 조건을 하나라도 만족하면 참
  • [not] in : 서브 쿼리 결과 중 하나라도 같은 것이 있으면 참

 

1
2
3
4
5
6
7
8
9
10
11
--팀A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name='팀A')
 
--전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o
where o.orderAmount > ALL(select p.stockAmount from Product p)
 
--어떤 팀이든 팀에 소속된 회원
select m from Member m
where m.team = ANY(select t from Team t)
cs

 

FROM 절 서브쿼리는 JPQL에서 불가능

from 절의 서브쿼리는 보통 join 등으로 해결할 수 있는데, 해결이 어려운 경우에는 따로 방법이 없다. 개인적으로는 Native sql을 쓰거나, 쿼리를 따로해서 자바 코드로 처리하는 방법이 최선일 것 같다.

 

 

 

 


 

7. 조건식

 

 

조건식은 CASE 식으로 사용한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--기본 CASE식
select
    case when m.age <= 10 then '학생요금'
    case when m.age >= 60 then '경로요금'
         else '일반요금'
    end
from Member m
 
--단순 CASE식
select
    case t.name
        when 'teamA' then 'incentive110%'
        when 'teamB' then 'incentive120%'
        else 'incentive 105%'
    end
from Team t
cs

 

 

COALESCE, NULLIF 조건문도 사용할 수 있다.

 

  • COALESCE : 첫 번째 파라미터의 값이 null이면 두 번째 파라미터 값 반환
  • NULLIF : 두 파라미터의 값이 같으면 null, 다르면 첫 번째 파라미터 값 반환
1
2
3
4
5
6
7
--사용자 이름이 없으면 '없음'을 반환
select coalesce(m.username, '없음')
from Member m
 
--사용자 이름이 '관리자' 이면 null 반환, 나머지는 본인의 이름 반환
select NULLIF(m.username, '관리자')
from Member m
cs

 

 

 


 

참조

 

1. 인프런_자바 ORM 표준 JPA 프로그래밍 - 기본편_김영한 님 강의

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21735?tab=curriculum

728x90
반응형