본문 바로가기
Develop/JPA

[JPA/기본편] 객체지향 쿼리

by J-rain 2024. 3. 20.

 

객체지향 쿼리

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 하지만 이 기능만으로 애플리케이션을 개발하기는 어렵다.

객체지향 쿼리 소개

다음은 JPA가 공식 지원하는 기능이다.

  • JPQL
  • Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.

다음은 JPA가 공식 지원하지는 않지만 알아둘 가치가 있다.

  • QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
  • JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하면 JDBC를 직접 사용할 수 있다.

 

JPQL

특징

  • 객체지향 쿼리 언어
  • 테이블 대상이 아닌 엔티티 객체를 대상으로 쿼리
  • SQL을 추상화하여 특정 DB SQL에 의존하지 않음
  • SQL로 변환되어 실행
  • JPQL API는 대부분 메소드 체인 방식으로 설계되어있어서 연속으로 작성 가능

 

1. 쿼리 작성

select 구문 구조

select [결과로 리턴할 엔티티 혹은 엔티티의 특정 필드]
from [조회 대상 엔티티]
where [조건]
-- 아래는 옵션
groupby [그룹화할 기준]
having [각 그룹에 대한 조건]
orderby [정렬 기준]

기본 문법

  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다. 반면에 select,from과 같은 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    • @Entity(name="xxx") 어노테이션으로 엔티티명을 별도로 설정한 경우에는 엔티티 클래스명이 아닌 해당 name 속성명을 사용해야 한다.
@Entity(name="members")
class Member{

}

select m.username from members m -- (O)
select m.username from Member m -- (x)
  • 별칭 필수
    • from절에서 엔티티에 대해 별칭을 설정해야한다. ( as 는 생략할 수 있다.)
select m.username from Member as m-- (O)
select username from Member-- (x)

 

 

TypeQuery, Query

  • 작성한 JPQL 실행 위해 쿼리 객체 필요
    • 반환할 타입 명확하게 지정 가능하면 TypeQuery 객체 사용
    • 반환 타입 명확하게 지정 불가능하면 Query 객체 사용
public void useTypeQuery(EntityManager em) {
  TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

  List<Member> resultList = query.getResultList();

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

...

public void useQuery(EntityManager em) {
  Query query = em.createQuery("SELECT m.username, m.age FROM Member m");

  List resultList = query.getResultList();

  for (Object o: resulstList) {
    Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환System.out.println("username = " + result[0]);
    System.out.println("age = " + result[1]);
  }
}

결과 조회

  • 아래 메소드들 호출시 실제 쿼리 실행하여 DB 조회함
    • query.getResultList(): 결과를 반환. 없으면 빈 컬렉션 반환
    • query.getSingleResult(): 결과가 정확히 하나일 때 사용
      • 결과 없으면 javax.persistence.NoResultException 발생
      • 결과가 1개보다 많으면 javax.persistence.NoUniqueResultException 발생

 

파라미터 바인딩

  • JPQL은 이름 기준 파라미터 바인딩 지원
    • JDBC는 위치 기준 파라미터 바인딩만 지원
  • 파라미터 바인딩을 사용하지 않고 직접 문자 더해 만들어 넣으면 SQL 인젝션 공격 당할 수 있음
  • 또한 파라미터 바인딩 방식 쓰면 파리미터 값 달라도 같은 쿼리로 인식해 JPQL을 SQL로 파싱한 결과를 재사용 할 수 있음

이름 기준 파라미터

  • 파라미터를 이름으로 구분하는 방법
  • 이름 기준 파라미터는 앞에 : 붙임
Strign usernameParam = "User1";

List<Member> resulstList = em
  .createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class) // :username이 이름 기준 파라미터 정의
  .setParameter("username", usernameParam);     // setParameter로 파라미터 바인딩
  .getResultList();

 

 

위치 기준 파라미터 (권장은 x)

  • 파라미터를 위치 기준으로 구분하는 방법
  • ? 다음에 위치 값 주면 됨
  • 위치 값은 1부터 시작
List<Member> members = em
  .createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
  .setParameter(1, usernameParam)
  .getResultList();

 

 

프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것 = projection
  •  SELECT {프로젝션 대상} FROM  으로 대상 선택
  • 대상으로는 엔티티, 임베디드 타입, 스칼라 타입이 있음

 

엔티티 프로젝션

SELECT m FROM Member m         // Member
SELECT m.team FROM Member m    // Team
  • 위 예시처럼 엔티티를 프로젝션 대상으로 사용해서 원하는 객체를 바로 조회
  • 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리됨

임베디드 타입 프로젝션

List<Address> address = em
  .createQuery("SELECT o.address FROM Order o", Address.class)
  .getResultList();
  • 임베디드 타입은 조회의 시작점이 될 수 없음
  • 위 예시처럼 엔티티를 통해서 조회해야 함
  • 임베디드 타입은 값 타입이기 때문에 영속성 컨텍스트에서 관리 X

스칼라 타입 프로젝션

List<String> usernames = em
  .createQuery("SELECT username FROM Member m", String.class)
  .getResultList();
  • 스칼라 타입 역시 영속성 컨텍스트에서 관리 x
  • 숫자, 문자, 날짜와 같은 기본 데이터 타입들
  • AVG, SUM 같은 통계 쿼리도 주로 스칼라 타입으로 조회함

여러 값 조회

List<Object[]> resultList = em
  .createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
  .getResultList();

for (Object[] row: resultList) {
  Member member = (Member) row[0];    // 엔티티
  Product product = (Product) row[1]; // 엔티티
  int orderAmount = (Integer) row[2]; // 스칼라
}
  • 프로젝션에 여러 값 선택시 TypeQuery 대신 Query 써야 함
  • 엔티티 타입도 여러 값 함께 조회 가능하며, 이 엔티티들도 영속성 컨텍스트로 관리 됨

NEW 명령어

TypeQuery<UserDTO> query = em.createQuery("SELECT new com.example.hellojpa.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);

List<UserDTO> resultList = query.getResultList();
  • SELECT 다음에 NEW 명령어 사용하여 반환받을 클래스 지정하고 해당 클래스의 생성자에 JPQL 조회 결과 넘겨줄 수 있음
  • 이렇게 하면 TypeQuery 사용해서 바로 변환된 객체로 리턴받을 수 있음
  • NEW 명령어 사용시 주의점
    • 패키지 명을 포함한 전체 클래스 명 써야함
    • 순서와 타입이 일치하는 생성자 있어야 함

 

페이징 API

  • JPA는 페이징을 아래 두 API로 추상화
    •  setFirstResult(int startPosition)  : 조회 시작 위치(0부터 시작)
    •  setMaxResults(int maxResult)  : 조회할 데이터 수
  • DB dialect에 따라 사용중인 DB에 알맞은 SQL로 자동 변환되어 쿼리 수행됨
    • 최적화 더 하려면 Native SQL 사용해야 함
List<Member> resultList = em
  .createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class)
  .setFirstResult(10);    // 조회 시작 위치는 11번째(0번째가 시작이므로)
  .setMaxResults(20);     // 조회할 데이터 수는 20개
  .getResultList();       // 결과적으로 11~30번 데이터가 조회됨

 

 

조인

• SQL 조인과 기능 동일, 문법만 약간 상이함

 

 

내부 조인

String teamName = "teamA";

List<Member> members = em
  .createQuery("SELECT m 
                FROM Member m 
		INNER JOIN m.team t 
		WHERE t.name = :teamName", Member.class)
  .setParameter("teamName", teamName)
  .getResultList();
  • INNSER JOIN 사용, INNER 는 생략 가능
  • JPQL 은 연관 필드를 사용하여 조인함
    • 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드
    • 위 예제의 경우 m.team 이 연관필드임

외부 조인

SELECT m
FROM Member m
LEFT OUTER JOIN m.team t
  • OUTER 는 생략 가능
  • 마찬가지로 연관필드 가지고 조인

세타 조인

SELECT count(m)
FROM Member m, Team t
WHERE m.username = t.name   // WHERE 절에서 전혀 관계 없는 Member.username 과 Team.name 을 조인함
  • WHERE 절을 사용해서 세타 조인 가능
  • 내부 조인만 지원함
  • 세타 조인을 사용해 전혀 관계없는 엔티티도 조인 가능

 

서브 쿼리

  • WHERE, HAVING 절에만 사용 가능, SELECT, FROM 절에선 못 씀
    • 하이버네이트 HQL은 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

// 한 건이라도 주문한 고객 조회 (컬렉션 크기 값 구할 수 있는 size 기능 활용, 실행 쿼리는 위에거랑 동일)
SELECT m 
FROM Member m
WEHRE m.orders.size > 0

 

 

서브 쿼리 함수

  • EXISTS
    • [NOT] EXISTS (subquery)
    • subquery에 결과 존재하면 참, NOT 붙으면 반대
  • ALL, ANY, SOME
    • {ALL | ANY | SOME} (subquery)
    • ALL : 조건 모두 만족하면 참
    • ANY, SOME : 조건 하나라도 만족하면 참
  • IN
    • [NOT] IN (subquery)
    • subquery 결과 중 하나라도 같은 것 있으면 참

조건식

JPQL 타입 표현

종류 설명 예제
문자 작은 따옴표 사이에 표현작은 따옴표 표현하려면 연속 두개 사용(‘’) ‘HELLO’‘She’’s’
숫자 L(Long)D(Double)F(Float) 10L10D10F
날짜 DATE { d ‘yyyy-mm-dd’ }
TIME { t ‘hh-mm-ss’ }
DATETIME { ts ‘yyyy-mm-dd hh:mm:ss.f’ }
{ d ‘2021-03-03’ }
{ t ‘11-11-11’ }
{ ts ‘2021-03-03 11-11-11.111’ }
m.createDate = { d ‘ 2021-03-03’ }
Boolean True, False  
Enum 패키지명 포함 전체 이름 사용 jpabook.MemberType.Admin
엔티티 타입 엔티티의 타입을 표현. 주로 상속과 관련하여 사용 TYPE(m) = Member

 

JPQL 기타

SQL과 문법이 식

• EXISTS, IN

• AND, OR, NOT

• =, >, >=, <, <=, <>

• BETWEEN, LIKE, IS NULL

CASE 식

  • 특정 조건에 따라 분기할 떄 사용
  • 기본 CASE
CASE
  {WHEN <조건식> THEN <스칼라식>}+
  ELSE <스칼라식>
END

// 예제
SELECT
  CASE 
    WHEN m.age <= 10 then '학생요금'
    WHEN m.age >= 60 then '경로요금'
    ELSE '일반요금'
  END
FROM Member m
  • 단순(심플) CASE
    • 조건식 사용 X, 문법 단순
    • 자바의 switch case 문과 비슷
CASE <조건대상>
  {WHEN <스칼라식1> THEN <스칼라식2>}+
  ELSE <스칼라식>
END

// 예제
SELECT
  CASE t.name
    WHEN 'teamA' then '인센티브110%'
    WHEN 'teamB' then '인센티브120%'
    ELSE '인센티브105%'
  END
FROM Team t

 

JPQL 기본 함수

  • CONCAT
String query = "select concat('a', 'b') from Member m";

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

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

// s = ab 출력된다.
  • SUBSTRING
String query = "select substring(m.username, 2, 3) from Member m";
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX (JPA 용도)

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

[JPA/기본편] 값 타입  (0) 2024.03.20
[JPA/기본편] 프록시  (0) 2024.03.20
[JPA/기본편] 상속 관계 매핑  (0) 2024.03.20
[JPA/기본편] 연관관계 매핑  (0) 2024.03.20
[JPA/기본편] 연관관계  (1) 2024.03.20

댓글