자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 10 정리

정종일·2023년 6월 19일
0

Spring

목록 보기
15/18

객체지향 쿼리 언어


객체지향 쿼리 소개

데이터는 데이터베이스에 있으므로 SQL 로 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM 을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. 이를 해결하기 위한것이 JPQL이다.

  • 테이블 이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
  • SQL을 추상화 해서 특정 데이터베이스 SQL에 의존하지 않음

JPA가 공식지원하는 기능은 아래와 같다

  • JPQL
  • Criteria Query
  • Native SQL

JPA가 공식지원하지는 않지만 알아둘 가치가 있는 기능

  • QueryDSL
  • JDBC 직접사용, MyBatis 같은 SQL 매퍼 프레임워크 사용

JPQL

JPQL은 SQL보다 간결하다

String jpql = "select m from Member as m where m.username = "kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
// 실행한 JPQL
select m
from Member as m
where m.username = 'kim'

// 실행된 SQL
select
		member.id as id,
		member.age as age,
		member.team_id as team,
		member.name as name
from
		Member member
where
		member.name = 'kim'

Criteria Query

Criteria는 JPQL을 생성하는 빌더 클래스다. 문자가 아닌 query.select(m).where .... 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 장점이 있다.

JPQL에서는 오타가 있어도 컴파일이 성공하며 서버에 배포가능하다. 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다. 그에반해 Criteria는 문자가 아닌 코드로 JPQL을 작성한다. 따라서 컴파일 시점에 오류를 발견할 수 있다.

장점

  • 동적쿼리 작성 편리
  • IDE 사용으로 코드 자동완성
  • 컴파일 시점에서의 오류 발견

단점

  • 코드가 한눈에 들어오지 않는다
  • 복잡하고 장황하여 사용하기 불편하다

QueryDSL

Criteria 처럼 JPQL 빌더 역할을 한다. 코드기반이며 단순하고 사용하기 쉽다.

🔊 QueryDSL 은 JPA 표준이 아닌 오픈소스 프로젝트이다.

우리가 프로젝트에 사용하고 있는 것도 이것 !

Native SQL

SQL을 직접 사용할 수 있는 기능을 지원하는 것이 Native SQL이다.

JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용할 때 사용한다. 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 하는 것. 즉, 데이터베이스를 변경하면 코드도 변경되어야 한다.

JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용

// JDBC Connection을 획득하는 방법
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
		
		@Override
		public void execute(Connection connection) throws SQLException {
				// work...
		}
});

JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시 해야함. 위 방법들은 JPA를 우회해야 하는데 이렇게 되면 JPA가 전혀 인식을 하지 못하는 문제가 발생한다. 즉, 데이터 무결성을 훼손할 수 있다는 이야기이다.

이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 해결된다.

Spring framework를 사용하면 JPA와 마이바티스를 손쉽게 통합할 수 있다. 또한 AOP를 적절히 활용해서 JPA를 우회하여 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결 가능하다.

JPQL


  • 객체지향 쿼리언어
  • 테이블을 대상으로 쿼리하는 것이 아닌 엔티티 객체를 대상으로 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
  • JPQL은 결국 SQL로 변환

기본 문법과 쿼리 API

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있음.

EntityManager.persist() 메소드를 사용하면 되므로 INSERT문이 없다.

SELECT문

  1. 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분
    • JPQL 키워드는 대소문자를 구분하지 않음
  2. 엔티티 이름
    • JPQL에서 사용한 Member는 클래스명이 아닌 엔티티명
    • 엔티티 명은 @Entity(name = "xxx") 로 지정 가능
    • default 는 기본 클래스 명
  3. 별칭은 필수
    • JPQL은 별칭이 필수
    • 별칭없이 작성하면 잘못된 문법으로 오류발생
    • as는 생략 가능
🔊 하이버네이트는 HQL(Hibernate Query Language)을 제공 JPA 구현체로 하이버네이트를 사용하면 HQL도 사용 가능 HQL은 별칭 없이 사용가능

TypeQuery, query

작성한 JQPL을 실행하려면 쿼리 객체를 만들어야 한다.

  • 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용
  • 반환할 타입을 명확하게 지정할 수 없으면 Query 객체를 사용
// TypeQuery
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
// Query
Query query = em.createQuery("select m from Member m");

타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리

결과 조회

  • query.getResultList() : 결과를 예제로 반환. 결과가 없다면 빈 컬렉션 반환
  • query.getSingleResult () : 결과가 하나일 때 사용
    • 결과가 없으면 javax.persistence.NoResultException 발생
    • 결과가 1개보다 많으면 javax.persistence.NonUniqueResultException 발생

파라미터 바인딩

  • JDBC는 위치기준 파라미터 바인딩만 지원
  • JPQL은 이름 기준 파라미터 바인딩 또한 지원
  1. 이름기준 파라미터

    • 파라미터를 이름으로 구분하는 방법
    • 이름 기준 파라미터 앞에 : 를 사용
    TypedQuery<Member> query = 
    em.createQuery("select m from Member m where m.username = :username"
    , Member.class);
    
    String usernameParam = "jong9";
    query.setParameter("username", usernameParam);
  2. 위치 기준 파라미터

    • ?다음에 위치 값을 주면 된다.
    List<Member> members = 
    em.createQuery("select m from Member m where m.username = ?1"
    , Member.class)
    		.setParameter(1, usernameParam)
    		.getResultList();

위치 기준 파라미터보다 이름기준 파라미터 바인딩 방식이 더 명확함

프로젝션

SELECT 절에 조회할 대상을 지정하는 것

프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등)이 있다.

  1. 엔티티 프로젝션

    SELECT m from Member m      // 회원
    SELECT m.team FROM Member m // 팀
    • 둘 다 엔티티 프로젝션 대상으로 사용
    • 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리
  2. 임베디드 타입 프로젝션

    • 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 존재
    • 엔티티를 통해서 임베디드 타입을 조회하여야 한다
    • 임베디드 타입은 엔티티 타입이 아닌 값 타입이기에 영속성 컨텍스트에서 관리되지 않는다 !!
  3. 여러 값 조회

    • 제네릭에 Object[] 를 사용하면 간결한 개발이 가능하다
    • 스칼라 타입 뿐 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다
    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];
    }

    이 때 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

  4. NEW 명령어

    • 객체 변환작업을 줄일 수 있다
    TypedQuery<UserDTO> query = 
    		em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
    		FROM Member m", UserDTO.class);
    
    List<UserDTO> resultList = query.getResultList();
    • SELECT 다음에 NEW 명령어롤 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다
    • 주의사항
      1. 패키지 명을 포함한 전체 클래스 명을 입력해야 함
      2. 순서와 타입이 일치하는 생성자가 필요

페이징 API

데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다

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

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResult(int maxResult) : 조회할 데이터 수
TypedQuery<Member> query = 
		em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();

// FirstResult의 시작은 10이므로 11번째부터 시작해서 총 20건의 데이터를 조회
// 따라서 11~30 데이터를 조회

데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다.

ex)
// HSQLDB
ORDER BY M.NAME DESC OFFSET ? LIMIT ?

// MySQL
ORDER BY M.NAME DESC OFFSET LIMIT ?, ?

// PostgreSQL
ORDER BY M.NAME DESC LIMIT ? OFFSET ?

데이터베이스마다 SQL이 다를 뿐 아니라 오라클과 SQLServer는 페이징 쿼리를 따로 공부해야한다.

페이징 SQL을 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야한다..

집합과 정렬

집합은 집합함수와 함께 통계 정보를 구할 때 사용

집합함수

집합함수 사용 시 참고사항

  • NULL 값은 무시하므로 통계에 미합산
  • 값이 없는데 SUM, AVG, MAX, MIN 함수 사용시 NULL. COUNT는 0
  • DISTINCT 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다 ex) SELECT COUNT(DISTINCT m.age) from Member m
  • DISTINCT를 COUNT해서 사용할 때 임베디드 타입은 지원하지 않는다.

GROUP BY, HAVING

  • GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
  • HAVING은 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.

정렬(ORDER BY)

  • 결과를 정렬할 때 사용한다.
  • ASC : 오름차순 / DESC : 내림차순

JPQL 조인

SQL조인과 기능은 같고 문법만 약간 다르다

내부 조인

  • 내부 조인은 INNER JOIN을 사용
  • INNER는 생략 가능

JPQL 내부 조인과 SQL 조인의 가장 큰 차이점은 아래와 같이 연관 필드를 사용한다는 것이다.

FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID

기존 SQL 처럼 아래와 같이 사용하면 오류가 발생한다.

FROM MEMBER m JOIN TEAM t

서로 다른 타입의 두 엔티티를 조회했으므로 TypeQuery를 사용할 수 없다

외부 조인

JPQL의 외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략가능해서 보통 LEFT JOIN으로 사용한다.

컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.

  • 회원 → 팀 으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team) 를 사용
  • 팀 → 회원 은 반대로 일대다 조인이면서 컬렉션 값 연관 필드(m.member)를 사용

세타 조인

WHERE절을 사용해서 세타 조인 할 수 있다. 세타 조인은 내부 조인만 지원한다.

// JPQL
SELECT COUNT(m) FROM MEMBER m, TEAM t
WHERE m.username = t.name

// SQL
SELECT COUNT(m.id)
FROM
	MEMBER M CROSS JOIN TEAM t
WHERE
	m.username = t.name

JOIN ON 절

JPA 2.1부터 조인할 때 ON절을 지원한다. ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 내부조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON절은 외부 조인에서만 사용한다.

페치 조인

Fetch 조인은 SQL의 조인 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이며 join fetch 명령어로 사용할 수 있다.

  1. 엔티티 페치조인

    SELECT m FROM MEMBER m JOIN FETCH m.team

    이렇게 하면 회원과 팀을 함께 조회한다. 일반적인 JPQL 조인과는 다르게 fetch join은 별칭을 사용할 수 없다. but, 하이버네이트는 fetch join에도 별칭을 허용한다.

    String jpql = "select m from Member m join fetch m.team";
    
    List<Member> members = em.createQuery(jpql, Member.class).getResultList();
    
    for (Member member : members) {
    		System.out.println("username =" + member.getUsername() +", " +
    				"teamname = " + member.getTeam().name());
    }
    
    // 출력결과
    username = 회원 1, teamname =A
    username = 회원 2, teamname =A
    username = 회원 3, teamname =B

    연관된 팀을 사용해도 지연로딩이 일어나지 않는다. 또한 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.

  2. 컬렉션 페치조인

    String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
    List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
    
    for(Team team : teams) {
    		System.out.println("teamname = " + team.getName() + ",
    			team = " + team);
    
    		for (Member member : team.getMembers()) {
    				System.out.println(
    						"->username =" + member.getUsername() + ",
    						member = " + member);
    		}
    }
    
    // 페치조인으로 팀과 회원을 함께 조회해서 지연로딩 발생 안함

    출력결과 팀A가 중복 조회되는 문제가 발생한다.

  3. 페치조인과 DISTINCT

    중복된 결과를 제거하는 명령어이다. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론이고 애플리케이션에서 한번 더 중복을 제거한다.

    select distinct t
    from Team t join fetch t.members
    where t.name = '팀A'

    각 Row의 결과 데이터가 다르므로 SQL의 DISTINCT는 효과가 없다.

    ex) team A, 회원1 / team A, 회원2 → 팀은 중복되나 회원이 다르기에 중복제거가 안됨

    이후 애플리케이션에서 DISTINCT 명령어를 보고 Team의 중복만을 제거한다.

  4. 페치조인과 일반조인의 차이

    JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 즉시로딩으로 설정하면 회원 컬렉션을 즉시로딩하기위해 쿼리를 한번 더 실행한다.

    반면 페치조인을 사용하면 연관된 엔티티도 함께 조회한다.

  5. 페치조인의 특징과 한계

    글로벌 로딩 전략을 즉시로딩으로 설정하면 성능에 악영향을 미칠 수 있으니 되도록이면 지연 로딩을 사용하고 최적화가 필요한 곳에만 페치조인을 적용하는게 효과적이다.

    페치조인을 사용하면 연관된 엔티티를 쿼리시점에 조회하기 때문에 지연로딩 미발생, 준영속 상태에서도 객체 그래프를 탐색할 수 있다.

    하지만 한계 또한 존재한다

    1. 페치조인 대상에는 별칭을 줄 수 없다
      • JPA 표준에서 지원하지 않지만 하이버네이트를 포함한 몇몇 구현체들은 별칭을 지원
      • but 별칭을 잘못사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있다.
      • 2차캐시와 함께 사용할 때 또한 조심해야한다 !
    2. 둘 이상의 컬렉션을 페치할 수 없다
      • 구현체에 따라 되기도 하는데 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의하자
    3. 컬렉션을 페치조인하면 페이징 API를 사용할 수 없다
      • 컬렉션(1:N)이 아닌 단일 값 연관 필드들은 페치조인을 사용해도 페이징 API를 사용할 수 있다.
      • 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고로그를 남기고 메모리에서 페이징 처리가 되며 데이터가 많을경우 성능이슈와 메모리 초과 예외가 발생할 수 있어 아주 위험하다

경로 표현식

쉽게 이야기해서 점을 찍어 객체 그래프를 탐색하는 것

ex) t.name

경로 표현식 용어 정리

  • 상태 필드 : 값을 저장하기 위한 필드 (필드 or 프로퍼티)
  • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함 (필드 or 프로퍼티) ex) t.username, t.age
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
      ex) m.team
    • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
      ex) m.orders

경로 표현식과 특징

  • 상태 필드 경로 : 경로 탐색의 끝. 더는 탐색할 수 없다.
    SELECT m.username, m.age FROM Member m;
  • 단일 값 연관 경로 : 묵시적으로 내부조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.
    SELECT m.* FROM Orders o
    		INNER JOIN Member m ON o.member_id = m.id;
    단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부조인이 일어나는데 이것을 묵시조인 이라 한다. 묵시적 조인은 모두 내부 조인이다.
    • 명시적 조인 : JOIN 을 직접 적어주는 것
    • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어나는 것
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 더는 탐색 할 수 없다. FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
    SELECT t.members FROM Team t; // 성공
    SELECT t.members.username FROM Team t; // 실패
    컬렉션 값에서 경로 탐색을 시도하지 말자 !!
    ex) t.members.username 컬렉션에서 경로탐색을 하고싶으면 조인을 사용해 새로운 별칭을 획득하자.
    ex) join member m > m.username

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 항상 내부조인이다.
  • 컬렉션은 경로탐색의 끝이다
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 줌
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵기 때문에 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자

서브 쿼리

JPQL 도 서브쿼리를 지원하지만 몇가지 제약이 있는데, 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.

SELECT m FROM Member m WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)

서브 쿼리 함수

조건식

타입 표현

연산자 우선 순위

  1. 경로 탐색 연산 : .
  2. 수학 연산 : +, - (단항 연산자), *, /, +, -
  3. 비교 연산 : =, >, ≥, <, ≤, <> (다름)
  4. 논리 연산 : NOT, AND, OR

Between, IN, Like, NULL 비교

  • Between : A이상 B이하 이면 참
  • IN : 같은 값이 예제에 하나라도 있으면 참
  • LIKE : 문자 표현식과 패턴 값 비교
    • % : 아무 값들이 입력되도 상관 x
    • _ : 한글자는 아무 값이 입력되도 되지만 값이 있어야 함
  • NULL : =로 비교하지 않고 IS NULL을 사용

컬렉션 식

  • EMPTY : 컬렉션에 값이 비었으면 참 (NULL과 사용법 동일)
  • MEMBER : 엔티티나 값이 컬렉션에 포함되어있으면 참
    // 예제
    SELECT t FROM Team t WHERE :memberParam Member OF t.members

스칼라 식


수학 식

문자 함수

수학 함수

날짜 함수

CASE 식

특정 조건에 따라 분기할 때 CASE식 사용

  • 기본 CASE
    CASE
    		{WHEN 조건식 THEN 스칼라식} +
    		ELSE 스칼라식
    END
  • 심플CASE
    CASE 조건대상
    		{WHEN 스칼라식1 THEN 스칼라식2} +
    		ELSE 스칼라식
    END
  • COALESCE
    COALESCE( 스칼라식 {, 스칼라식} +)
    SELECT COALESCE( m.username, '이름없는 회원') FROM Member m
  • NULLIF
    NULLIF(스칼라식, 스칼라식)
    SELECT NULLIF(m.username, '관리자') FROM Member m

다형성 쿼리

JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.

// 단일 테이블 전략 사용시 실행되는 SQL
SELECT * FROM ITEM
// 조인 전략 사용시 실행되는 SQL
SELECT
		*
FROM
		Item i
LEFT OUTER JOIN
		Book b ON ~~~
LEFT OUTER JOIN
		Album a ON ~~~
LEFT OUTER JOIN
		Movie m ON ~~~

TYPE

엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용

// JPQL
SELECT i FROM Item i
WHERE TYPE(i) IN (Book, Movie)

// SQL
SELECT i FROM Item i
WHERE i.DTYPE IN ('B', 'M')

TREAT (JPA 2.1)

자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용.

// JPQL
SELECT i FROM Item i WHERE TREAT(i as Book).author = 'kim'

// SQL
SELECT i.* FROM Item i
WHERE
		i.DTYPE = 'B'
		AND i.author = 'kim'

TREAT을 사용하여 부모타입인 item을 자식타입인 Book으로 다룬다. 따라서 author 필드에 접근 가능

사용자 정의 함수 호출

FUNCTION_INVOCATION:: = FUNCTION(FUNCTION_NAME {, FUNCTION_ARG}*)
SELECT FUNCTION('GROUP_CONCAT', i.name) FROM Item i

하이버네이트 구현체를 사용하면 방언 클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야 한다

public class MyH2Dialect extends H2Dialect {
		public MyH2Dialect() {
				registerFunction( "group_concat", new StandardSQLFunction
						("group_concat", StandardBasicTypes.STRING));
		}
}
<property name = "hibernate.dialect" value = "hello.MyH2Dialect" />

하이버네이트 구현체를 사용하면 아래와 같이 축약해서 사용 가능하다

SELECT GROUP_CONCAT(i.name) FROM Item i

기타 정리

  • enum 은 = 비교 연산만 지원
  • 임베디드 타입은 비교 미지원
  • Empty String : 데이터베이스에 따라 ''를 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용
  • NULL 정의
    1. 조건을 만족하는 데이터가 하나도 없을 경우
    2. 알 수 없는 값. NULL과 모든 수학적 계산 결과는 NULL
    3. NULL == NULL 은 알 수 없는 값
    4. NULL is NULL 은 참

엔티티 직접 사용

  • 기본 키 값 : 객체 인스턴스는 참조 값으로 식별, 테이블 로우는 기본 키 값으로 식별
    SELECT COUNT(m.id) FROM Member m
    SELECT COUNT(m) FROM Member m
    // 위 결과 값은 동일한 SQL을 실행한다
  • 외래 키 값
    String qlString = "select m from Member m where m.team.id = :teamId";
    List resultList = em.createQuery(qlString)
    		.setParameter("teamId", 1L)
    		.getResultList();
    예제에서 m.team.id 를 보면 Member와 Team간의 묵시적 조인이 일어날 것 같지만 Member 테이블이 team_id 외래키를 갖고 있어 일어나지 않는다. 따라서 m.team 을 사용하든 m.team.id 를 사용하든 생성되는 SQL은 같다.

Named 쿼리 : 정적 쿼리

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.

  • 동적 쿼리 : em.createQuery("select..") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용하는 것. 이를 Named 쿼리라 하며 한번 정의하면 변경할 수 없다.

Named 쿼리 장점

  • 애플리케이션 로딩시점에 JPQL 문법을 체크하고 미리 파싱해두어 오류확인이 빠르고 파싱된 결과를 재사용하므로 성능상의 이점도 존재
  • 변하지 않는 SQL이 생성되므로 데이터베이스 조회 성능 최적화에 도움이 됨
@Entity
@NamedQuery (
		name = "Member.findByUsername",
		query = "select m from Member m where m.username = :username")
public class Member {
		...
}
List<Member> resultList = em.createNamedQuery("Member.findByUsername",
		Member.class)
				.setParameter("username", "회원")
				.getResultList();
// 위와 같이 Named쿼리 이름을 createNamedQuery 메소드 안에 넣어주면 된다
🔊 findByUsername이 아닌 Member.findByUsername 으로 한 이유는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위함이다. 또한 엔티티명이 앞에있으면 관리하기 쉽다 !

하나의 엔티티에 여러개의 쿼리를 넣고싶다면

@Entity
@NamedQueries({
		@NamedQuery (
				name = "Member.findByUsername",
				query = "select m from Member m where m.username = :username"),
		@NamedQuery (
				name = "Member.count",
				query = "select count(m) from Member m")
})

public class Member {
		...
}

쿼리 어노테이션의 내부구조이다

@Target({TYPE})
public @interface NamedQuery {
		String name(); // 쿼리 이름 (required = true)
		String query(); // JPQL 정의 (required = true)
		LockModeType lockMode() default NONE; // 쿼리 실행시 락모드 설정

		QueryHint[] hints() default{}; // JPA 구현체에 힌트를 줄 수 있음
}
  • LockMode : 쿼리 실행 시 락을 건다
  • hints : SQL의 힌트가 아니라 JPA 구현체에 제공하는 힌트

Named 쿼리를 XML에 정의

네임드 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.

또한 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.

Criteria


  • JPQL을 자바코드로 작성하도록 도와주는 빌더 클래스 API
  • 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일단계에서 잡을 수 있음
  • 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다는 장점
  • 코드가 복잡하고 장황하다는 단점

1. Criteria 기초


// JPQL : select m from Member m

// 쿼리 빌더
CriteriaBuilder cb = em.getCriteriaBuilder(); 

// 생성, 반환타입 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);

// From절
Root<Member> m = cq.from(Member.class);

// 검색 조건
PRedicate usernameEqual = cb.equal(m.get("username"), "회원");

//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.ddesc(m.get("age"));

// 쿼리 생성
cq.select(m)
		.where(usernameEqual)
		.orderBy(ageDesc);

TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();
  • Root는 조회의 시작점
  • 별칭은 엔티티에만 부여 가능
  • m.get("username") = m.username
  • m.get("team").get("name") = m.team.name

2. Criteria 쿼리 생성


public interface CriteriaBuilder {
		CriteriaQuery<Object> createQuery();

		<T> CriteriaQuery<T> createQuery(Class<T> resultClass);
		CriteriaQuery<Tuple> createTupleQuery();
		...
}
  • Criteria 쿼리를 생성할 때 Member.class를 반환타입으로 지정하면 em.createQuery(cq)에서 반환 타입을 지정하지 않아도 된다
  • 반환 타입을 지정할 수 없거나 반환 타입이 둘 이상이면 Object로 반환받으면 된다
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery();
...
List<Object[]> resultList = em.createQuery(cq).getResultList();
  • 반환타입이 둘 이상이면 Object[]를 사용하는 것이 편리
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createQuery();
...
List<Tuple> resultList = em.createQuery(cq);
  • 반환타입을 튜플로 받고 싶으면 위와같이 사용하면 된다

3. 조회


// JPQL : select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));

CriteriaBuilder cb = em.getCriteriaBuilder();
cq.select(cb.array(m.get("username"), m.get("age"));
  • 조회 대상을 여러 건을 지정하려면 multiselect 또는 cb.array를 사용하면 된다

DISTINCT


select, multiselect 다음에 distinct(true)를 사용하면 된다

// JPQL : select distinct m.username, m.age
cq.multiselect(m.get("username"), m.get("age")).distinct(true);

NEW.construct()


JPQL에서 select new 생성자 구문을 Criteria 에서는 cb.construct(클래스타입, ...) 로 사용한다.

cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));

튜플


Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공

TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
		String username = tuple.get("username", String.class);
		...
}
  • 튜플을 사용하려면 cb.create(TupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria를 생성
  • 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 한다. alias() 메소드를 사용하여 지정
  • 선언해둔 튜플 별칭으로 데이터 조회 가능
  • 튜플은 이름 기반이므로 순서 기반의 Object[] 보다 안전
  • tuple.getElements() 같은 메소드를 사용해서 현재 튜플의 별칭과 자바 타입도 조회할 수 있다.

4. 집합


Group By


cq.groupBy(m.get("team").get("name")); // Group By
// = group by m.team.name

Having


having(cb.gt(minAge, 10))
// = having min(m.age) > 10

5. 정렬


cb.desc (...) 또는 cb.asc(...)

6. 조인


join() 메소드와 JoinType 클래스 사용

public enum JoinType {
		
		INNER,
		LEFT,
		RIGHT
		// JPA 구현체나 데이터베이스에 따라 지원하지 않을 수 있다.
}

// Join
m.join("team"); // 내부
m.join("team", JoinType.INNER); // 내부
m.join("team", JoinType.LEFT); // 외부

// Fetch Join
m.fetch("team", JoinType.LEFT); // 페치
  • 페치조인의 주의사항은 JPQL과 동일하다

7. 서브 쿼리


  • 간단한 서브쿼리

    /* JPQL :
    		select m from Member m
    		where m.age >=
    				(select AVG(m2.age) from Member m2)
    */
    
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
    
    // 서브쿼리
    SubQuery<Double> subQuery = mainQuery.subQuery(Double.class);
    Root<Member> m2 = subQuery.from(Member.class);
    subQuery.select(cb.avg(m2.<Integer>get("age")));
    
    // 메인쿼리
    Root<Member> m = mainQuery.from(Member.class);
    mainQuery.select(m)
    		.where(cb.ge(m.<Integer>get("age"), subQuery));
    1. 서브쿼리는 mainQuery.subQuery로 생성
    2. 메인 쿼리 생성 부분을 보면 where(... subQuery)에서 생성한 서브 쿼리를 사용
  • 상호 관련 서브 쿼리

    /* JPQL :
    		select m from Member m
    		where exists
    				(select t from m.team t where t.name = '팀A')
    */
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
    
    Root<Member> m = mainQuery.from(Member.class);
    
    SubQuery<Team> subQuery = mainQuery.subQuery(Team.class);
    Root<Member> subM = subQuery.correlate(m); // 메인 쿼리의 별칭을 가져옴
    Join<Member, Team> t = subM.join("team");
    subQuery.select(t)
    		.where(cb.equal(t.get("name"), "팀A"));
    
    // 메인 쿼리 생성
    mainQuery.select(m)
    		.where(cb.exists(subQuery));
    
    List<Member> resultList = em.createQuery(mainQuery).getResultList();
    • correlate(...) 메소드를 사용하여 메인 쿼리의 별칭을 서브 쿼리에서 사용할 수 있다

8. IN 식


/* JPQL
		select m from Member m
		where m.username in ("회원1", "회원2")
*/

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);

cq.select(m)
		.where(cb.in(m.get("username"))
				.value("회원1")
				.value("회원2"));

9. CASE 식


/* JPQL
		select m.username,
				case when m.age>=60 then 600
						 when m.age<=15 then 500
						 else 1000
				end
		from Member m
*/

cq.multiselect(
		m.get("username"),
		cb.selectCase()
				.when(cb.ge(m.<Integer>get("age"), 60), 600)
				.when(cb.le(m.<Integer>get("age"), 15), 500)
				.otherwise(1000)
);

10. 파라미터 정의


/* JPQL
		select m from Member m
		where m.username = :usernameParam
*/

cq.select(m)
		.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));

List<Member> resultList = em.createQuery(cq)
		.setParameter("usernameParam", "회원1")
		.getResultList();
  • cb.parameter(타입, 파라미터 이름) 메소드를 사용해서 파라미터를 정의
  • setParameter("usernameParam", "회원1")을 사용해서 해당 파라미터에 사용할 값을 바인딩

11. 네이티브 함수 호출


Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);
  • sum 자리에 원하는 네이티브 SQL 함수를 입력하면 됨

12. 동적 쿼리


JPQL 동적 쿼리는 공백을 입력하지 않거나 where와 and의 위치 구성을 신경쓰지 않으면 에러를 접할 수 있어 불편하다.

Integer age =10;

if(age != numm) criteria.add(cb.equal(m.<Integer>get("age"),
		 cb.parameter(Integer.class, "age")));

cq. where(cb.and(criteria.toArray(new Predicate[0])));
if(age != null) query.setParameter("age", age);

위와 같이 Criteria 동적 쿼리는 where, and의 위치나 공백으로 인해 에러는 없지만 장황하고 복잡하다.

13. 함수 정리


Expression 메소드

조건 함수

스칼라와 기타 함수

집합 함수

분기 함수

profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글