[3] JPA 프로그래밍 (13) - JPQL(객체지향 쿼리 언어) 1 (개요 / 기본 문법)

김정욱·2021년 3월 10일
0

[3] JPA 프로그래밍

목록 보기
13/15
post-thumbnail

개요

  • JPA는 다양한 쿼리 방법을 지원
    • JPQL
      : SQL과 유사한 객체 중심의 쿼리 언어
    • QueryDSL
      : SQL과 같은 쿼리를 생성하는 빌더
        (JPQL의 단점인 동적쿼리 등을 커버하기 위한 용도로 사용)
    • JPA Criteria
      : JPA에서 표준으로 지원하는 SQL생성 빌더 (하지만, 복잡해서 안씀)
    • Native SQL
      : 실제 SQL (MySQL, Oracle등)
    • JDBC API 직접사용 / My Batis / SpringJdbc Template

Criteria

  • JPA에서 표준으로 등록되어 있지만, 사용이 불편해서 잘 쓰지 않음
  • 대안으로 QueryDSL을 사용
//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("username"), “kim”));
List<Member> resultList = em.createQuery(cq).getResultList();

QueryDSL

  • 문자가 아닌 Java 코드JPQL을 작성
  • JPQL 빌더
  • 컴파일 시점에서 오류를 찾을 수 있음
  • 설정하는 과정이 필요하다! (의존성 추가 등등)
  • 동적쿼리 사용이 편리(JPQL의 단점)
  • 실무사용에서 거의 필수적 사용
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list =
  query.selectFrom(m)
    .where(m.age.gt(18))
    .orderBy(m.name.desc())
    .fetch();

Native SQL

  • JPQL에서 실제 SQL을 사용하는 것
  • JPQL과 QueryDSL로 해결할 수 없는 기능을 구현할 때 사용
String sql =SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";

List<Member> resultList =
          em.createNativeQuery(sql, Member.class).getResultList();

JDBC 직접사용 / SpringJdbcTemplate 등

  • 역시, JPQL과 QueryDSL로 해결할 수 없는 기능을 구현할 때 사용
  • 영속성 컨텍스트를 적절한 시점에 강제 flush() 해줘야 함
    • 왜?
      : 해당 기술들은 JPA와 상관없는 기능들이라서 JPA 로직상 영속성 컨텍스트를 통해서 영속화를 통해 바로 실제 DB에 접근하지 않기에 올바른 로직으로 수행하기 위해 적절한 flush()를 해줘야 함

JPQL 개요 & 기본 규칙

[ 설명 ]

  • Entity 객체를 대상으로 사용하는 객체 지향 쿼리 언어
  • 일반 SQL테이블 중심, JPQ객체 중심
  • SQL과 유사한 문법, ANSI 표준 기능을 제공
  • SQL을 추상화해서 특정 DataBase에 의존하지 않음
    (방언을 바꿔도 알아서 JPA가 해줌)

[ 기본 규칙 ]

  • SQL과 매우 유사한 문법
select_문 :: =
  select_절
  from_절
  [where_절]
  [groupby_절]
  [having_절]
  [orderby_절]

update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
  • Entity와 속성은 대소문자 구분 O (Member, age)
  • JPQL 키워드는 대소문자 구분 X (SELECT, from)
  • 테이블 이름이 아닌 Entity 이름을 사용
  • 별칭은 필수로 작성 (as키워드는 생략 가능)
select m from Member as m where m.age > 18

JPQL 사용

[ 집합과 정렬 ]

  • 집합
select
  COUNT(m) // 회원 수
  SUM(m.age) // 나이 합
  AVG(m.age) // 평균 나이
  MAX(m.age) // 최대 나이
  MIN(m.age) // 최소 나이
from Member m
  • 정렬
select
  m from
Member m
group by m.name // 이름으로 그룹
order by m.id // 정렬은 id 순서

[ TypeQuery / Query ]

  • TypeQuery
    : 반환 타입이 명확할 때 사용
 /* 반환 타입이 정확히 Member 클래스 --> 반환 클래스 파라미터로 지정! */
TypedQuery<Member> query =
   em.createQuery("SELECT m FROM Member m", Member.class);
  • Query
    : 반환 타입이 명확하지 않을 때 사용
 /* 반환 타입이 정확히 Member 클래스 --> 2번째 파라미터 없음 */
Query query =
   em.createQuery("SELECT m.name, m.age FROM Member m");

[ 결과 조회 API ]

  • query.getResultList()
    : 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없는 경우 -> 빈 리스트 반환
  • query.getSingleResult()
    : 결과가 정확히 하나 일 때, 단일 객체 반환
    • 결과가 없는 경우 -> 오류 javax.persistence.NoResultException
      (추후 java 8Optional로 처리 가능)
    • 결과가 둘 이상인 경우 -> 오류 Javax.persistence.NonUniqueResultException

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

  • 파라미터 바인딩 방법
    : 기본적으로 :변수명 + setParameter()를 사용
    • 이름 기준 : 변수 이름을 기준으로 바인딩
    • 위치 기준 : 파라미터 위치 번호를 기준으로 바인딩
/* 이름 기준 파라미터 바인딩 -- 이름으로 바인딩 위치를 찾음 */
em.createQuery("select m from Member m where m.name=:name", Member.class)
       .setParameter("name","정욱").getResultList();

/* 위치 기준 파라미터 바인딩 -- 파라미터 위치 순서에 맞게 지정 */
em.createQuery("select m from Member m where m.name=?1", Member.class)
       .setParameter(1,"정욱").getResultList();

[ 프로젝션 ]

단순 값 조회

  • 프로젝션 종류
    • 엔티티 프로젝션 (조회 대상-> 엔티티 )
      : SELECT m FROM Member m
    • 임베디드 타입 프로젝션 (조회 대상-> 임베디드 타입)
      : SELECT m.address FROM Member m
    • 스칼라 타입 프로젝션 (조회 대상 -> 스칼라 타입)
      : SELECT m.name, m.age FROM Member m
      (스칼라 타입 : 숫자, 문자등 기본 데이터 타입)

여러 값 조회

  • 방법
    • Query 타입으로 조회 (타입 캐스팅 해야함 --> 불편)
    • Object[] 타입으로 조회 (과정이 번잡함 --> 불편)
    • new 명령어로 조회 --> 자주 방법 사용
/* Query 타입으로 조회 --> 타입이 정해져있지 않아서 Object 배열로 들억가 있음 */
Query query = em.createQuery("select m.username, m.age from Member m"); 
Object o = query.getResultList().get(0);
Object[] result = (Object[]) o;
System.out.println(result[0]); // m.username
System.out.println(result[1]); // m.age


/* Object[] 타입으로 조회 */
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList(); 
Object[] result = resultList.get(0);
System.out.println(result[0]); // m.username
System.out.println(result[1]); // m.age


/* new 명령어로 조회 */
public class UserDTO{
  private String username;
  private int age;
  /* 생성자가 있어야 값을 받을 수 있음 */
  public UserDTO(String username, int age){
  this.username = username; this.age = age; 
  } 
} // 데이터를 받을 DTO 생성

/* new jpabook.jpql.UserDTO(m.username, m.age) 
   이렇게 pacakage명을 전부 입력해줘야 함 */
TypedQuery<UserDTO> query = em.createQuery("select new jpabook.jpql.UserDTO(m.username, m.age) from Member m", UserDTO.class); 
List<UserDTO> resultList = query.getResultList();

UserDTO userDTO = resultList.get(0);
System.out.println(userDTO.getUsername); // m.username
System.out.println(userDTO.getUser); // m.age

참고

  • 엔티티 프로젝션의 경우 가져온 모든 결과영속성 컨텍스트에 의해 관리
    --> 값을 수정하면 persist없이 바로 update query가 생성

[ 페이징 ]

설명

  • 특정 데이터의 구역을 나눠서 가져오는 페이징이라고 함
    (1페이지, 2페이지 데이터를 구분해서 가져오는 것)
  • JPA사용으로 매우 간편해짐
    (JPA가 아니면 3중 쿼리나, 추가 옵션 이런게 많음)
  • 페이징 메서드
    • setFirstResult(int startPosition)
      : 조회 시작 위치 (0부터 시작)
    • setMaxResults(int maxResult)
      : 조회할 데이터 수
//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
    .setFirstResult(10) // 11번째부터 (0부터 시작이니까)
    .setMaxResults(20) // 20개를 가져와라
    .getResultList();

방언별 페이징 API

: 페이징 수행시 실제나가는 쿼리

  • MySQL 방언
SELECT
  M.ID AS ID,
  M.AGE AS AGE,
  M.TEAM_ID AS TEAM_ID,
  M.NAME AS NAME
FROM
  MEMBER M
ORDER BY
  M.NAME DESC LIMIT ?, ?
  • Oracle 방언
SELECT * FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
  FROM
  ( SELECT
      M.ID AS ID,
      M.AGE AS AGE,
      M.TEAM_ID AS TEAM_ID,
      M.NAME AS NAME
    FROM MEMBER M
    ORDER BY M.NAME
    ) ROW_
  WHERE ROWNUM <= ?
  )
WHERE ROWNUM_ > ?

[ 조인 ]

Inner / outer / theta JOIN

  • 내부조인(inner join) , 외부조인(outer join)
/* inner join */
List<Member> innerJoin = em.createQuery
        ("select m from Member m inner join m.team t", Member.class)
        .getResultList();

/* 결과 */
Hibernate:
        select
            생략 ..
        from
            Member member0_
        inner join
            Team team1_
                on member0_.team_id=team1_.team_id

===============================================================

/* outer join */
List<Member> leftOuterJoin = em.createQuery
        ("select m from Member m left join m.team t", Member.class)
        .getResultList();

/* 결과 */
Hibernate:
        select
            생략 ..
        from
            Member member0_
        left outer join
            Team team1_
                on member0_.team_id=team1_.team_id
  • 세타 조인(theta join)
/* theta join */
em.persist(new Member("A", 10));
em.persist(new Member("B", 10));
em.persist(new Member("C", 10));

em.persist(new User("A"));
em.persist(new User("B"));

List<Member> resultList = em.createQuery
        ("select m from Member m, User u where m.name = u.name", Member.class)
        .getResultList();

/* 결과 */
Hibernate:
        select
          생략...
        from
            Member member0_
        cross join
            User user1_
        where
            member0_.name=user1_.name

ON 절을 활용한 JOIN

  • ON절 (JPA 2.1부터 지원) 활용
    • 조인 대상 필터링
      : 조인을 하기 전 조인할 대상을 미리 필터링
    • 연관관계 없는 엔티티 외부 조인 (Hibernate 5.1 지원)
      : 서로 연관관계가 없는 엔티티끼리 외부 조인을 할 수 있음
  • 조인 대상 필터링 - 예시
/* 조인 대상 필터링 */
List<Member> resultList = em.createQuery
   ("select m from Member m left join m.team t on t.teamName = m.name", Member.class)
   .getResultList();

// 같은 쿼리를 where로 했을 때
List<Member> resultList = em.createQuery
  ("seelct m from Member m left join m.team t where t.teamName = m.name", Member.class)
  .getResultList();

: on절 뒤에 조건을 추가해서 대상을 필터링

  • 연관관계 없는 엔티티 외부 조인 - 예시
/* 연관관계가 없는 엔티티 조인 */
List<Member> result = em.createQuery
  ("select m from Member m left join User u on m.name = u.name", Member.class)
  .getResultList();

: on절 뒤에 추가적인 조건으로 join 조건을 명시 (서로 관계가 없어도)

[ 서브쿼리(Sub Query) ]

설명

  • JPA에서 서브쿼리를 사용할 수 있지만 WHERE , HAVING에서만 가능
    (FROM절에서 사용할 수 없다)
  • Hibernate에서 지원하면 SELECT 절에서도 사용할 수 있음
  • FROM 절에서 사용할 수없는 문제에 대한 대안
    • 1안 --> JOIN으로 풀어서 해결 (권장)
    • 2안 --> 두개의 쿼리로 분리
    • 3안 --> 나온 결과값을 java코드로 조작해서 필요한 데이터 추출

지원 함수

  • [NOT] EXISTS : 결과가 존재하면 참
    • ALL : 모두 만족하면 참
    • ANY / SOME : 조건을 하나라도 만족하면 참
  • [NOT]IN : 결과중 하나라도 같은것이 있으면 참

예시

/* EXISTS - 팀A 소속인 회원 조회 */
select m from Member m
where EXISTS (select t from m.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)

[ JPQL 타입 표현 ]

  • 문자
    : ' '로 표현
    ex) 'HELLO' , 'She''s'
  • 숫자
    : L(Long) D(Double) F(Float)
    ex) 10L / 10D / 10F
  • Boolean
    : TRUE / FALSE
  • ENUM
    : 패키지명 포함
    ex) jpabook.MemberType.Admin
    • ENUM을 패키지명으로 접근해 사용
    • ENUM을 setParameter로 적용
  • 기타
    EXISTS / IN / AND OR NOT / BETWEEN / LIKE / IS NULL

[ 조건식 - CASE 식 ]

CASE

  • 조건에 따라 case를 나누는 식
  • 방법
    • 기본 CASE 식
      : 구간을 나눠서 값을 검증
    • 단순 CASE 식
      : 특정 값으로 값을 검증 (exact)

COALESCE

: 하나씩 조회해서 null이 아니면 그대로 반환

/* 값이 없는 회원은 '이름 없는 회원' 으로 나옴 */
select coalesce(m.name, '이름 없는 회원')
from Member m

NULLIF

: 값이 같으면 null반환 (반환하지 않는 것), 다르면 그대로 반환

/* 사용자 이름이 '관리자'면 null반환(반환하지 않는 것), 나머지는 그대로 반환 */
select NULLIF(m.name, '관리자') 
from Member m

[ JPQL 함수 ]

표준 함수

  • CONCAT : 문자열 2개 더하는 함수 ( ||로도 표현할 수 있음 )
  • SUBSTRING
  • TRIM : 공백 제거 함수
  • LOWER / UPPER
  • LENGTH
  • LOCATE : 내가 원하는게 원본 어디에 있는지 위치 반환하는 함수
  • ABS / SQRT / MOD
  • SIZE / INDEX

사용자 정의 함수

  • 직접 함수를 만드는 것
  • 과정
    • MyDialect 생성
    • persistence.xml 변경
    • 사용
profile
Developer & PhotoGrapher

0개의 댓글