#10 객체지향 쿼리 언어 - 발표

김영한 저 "자바 ORM 표준 JPA 프로그래밍, 2015"을 읽고 정리한 내용입니다.

10.1 객체지향 쿼리

미리보기

------------- 유저이름이 KIM인 멤버만 뽑아내보자. ------------
----------------------------------------------------------
1. JPQL 
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
----------------------------------------------------------
2. Creteria 쿼리 
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();
----------------------------------------------------------
3. QueryDSL 
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
List<Member> members =
	query.from(member).where(member.username.eq("kim")).list(member);
----------------------------------------------------------
4. Native SQL
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList =
	em.createNativeQuery(sql, Member.class).getResultList();

10.2 JPQL

UML와 ERD

김영한, 「자바 ORM 표준 JPA 프로그래밍」, 에이콘, 2015, 354p

위 ERD 주문 테이블에 MEMBER_ID가 들어가야되는 거 같은데...

시작하기 전에 JPQL의 특징 정리

  • 객체지향 쿼리이기 때문에, 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

10.2.1 기본 문법과쿼리 API - JPQL

  • INSERT문만 은 없다. persist() 사용.

  • JPQL에서 UPDATE, DELETE = 벌크 연산, 10.6절에서 설명

  • 대소문자 구분, 다만, SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분 X

  • 클래스 명이 아니라 엔티티명

  • JPQL은 별칭을 필수(Alias=Identification variable)

TypeQuery, Query

  • 작성한 JPQL을 실행하려면 쿼리 객체 필요

  • 반환할 타입을 명확하게 지정할 수 있으면, TypeQuery 객체

  • 지정할 수 없으면, Query 객체를 사용

TypeQuery 예제

Query 예제

  • 2개 반환(username, age)으로 조회대상 타입이 명확하지 않음.

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

    • query.getResultList(): 결과를 예제로 반환. 결과가 없으면 빈 컬렉션을 반환

    • query.getSingleResult(): 결과가 정확히 하나일 때 사용

  • 결과가 없으면 NoResultException 예외가 발생한다.

  • 결과과 1 개보다 많으면 NonUniqueResultException 예외가 발생한다.

10.2.2 파라미터 바인딩 - JPQL

  • 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확

  • SQL 인젝션 공격 대응책

    • 위험코드 "select m from Member m where m.username = '" + usernameParam + "'"

10.2.3 프로젝션 - JPQL

  • 조회할 대상을 지정하는 것

  • 프로젝션 대상인 3가지 엔티티, 임베비디드 타입, 스칼라 타입 그리고 3가지의 복합

  • 위와 함께 NEW 명령어도 대해 알아보자.

엔티티

  • 엔티티를 프로젝션 대상으로 사용

  • 조회한 엔티티는 영속성 컨텍스트에서 관리

엠비디드

  • 임베디드 타입은 엔티티 타입이 아닌 값 타입

  • 영속성 컨텍스트에서 관리 X

  • 조회의 시작점이 될 수 없다는 제약

스칼라

  • 숫자, 문자, 날짜와 같은 기본 데이터 타입

여러값 조회

  • 이때도 조회한 엔티티는 영속성 컨텍스트에서 관리

NEW 명령어

  • 실제 개발시 Object[] 직접 사용 X, UserDTO 처럼 의미 있는 객체로 변환해서 사용

  • TypeQuery 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.

주의사항

  1. 패키지 명을 포함한 전체 클래스 명을 입력해야 한다. new jpabook.jpql.UserDTO(m.username, m.age) -> o new UserDTO(m.username, m.age) -> x

  2. 순서와 타입이 일치하는 생성자가 필요하다.

10.2.4 페이징 API - JPQL

  • 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다. (오라클, SQLServer 너무 어렵다. 365페이지 참조)

  • ?에 바인딩하는 값도 데이터베이스마다 다르다.

페이징 API 코드 (멤버 조회해서 11~30번째 가져오기)

  • 페이징 SQL 더 최적화하려면 Native SQL 사용

10.2.5 집합과 정렬 - JPQL

집합함수 사용

함수

설명

COUNT

결과 수를 구한다. 반환 타입: Long // int로 받으니 형변환 에러

MAX, MIN

최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다.

AVG

평균값을 구한다. 숫자타입만 사용할 수 있다. 반환 타입: Double

SUM

합을 구한다. 숫자타입만 사용할 수 있다. 반환 타입: 정수합 Long, 소수합: Double, Biginteger합: Biginteger, BigDecimal합: BigDecimal

참고사항

  • NULL 값은 무시하므로 통계에 잡히지 않는다(DISTINCT가 정의되어 있어도 무시된다).

  • 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단, COUNT는 0이 된다.

  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.

GROUP BY, HAVING

팀 테이블

id

team_name

1

팀1

2

팀2

3

팀3

멤버 테이블

id

name

age

team_id

1

멤버1

11

1

2

멤버2

12

2

3

멤버3

13

3

4

멤버4

14

1

5

멤버5

15

2

6

멤버6

16

3

  • 팀 이름을 기준으로 그룹별로 묶어서 통계

  • 그룹별 통계 데이터 중에서 평균나이가 10살 이상인 그룹을 조회

  • 보통 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담

  • 사용자가 적은 새벽에 통계 쿼리를 실행

ORDER BY

  • 결과를 정렬

10.2.6 JPQL 조인 - JPQL

팀테이블

id

team_name

1

팀1

2

팀2

3

팀3

4

팀4

멤버 테이블

id

name

age

team_id

1

멤버1

11

1

2

멤버2

12

1

3

멤버3

13

2

4

멤버4

14

2

5

멤버5

15

3

6

멤버6

16

3

7

멤버7

17

NULL

내부조인 결과

id

name

age

team_id

1

멤버1

11

1

2

멤버2

12

1

3

멤버3

13

2

4

멤버4

14

2

5

멤버5

15

3

6

멤버6

16

3

외부조인 결과

id

name

age

team_id

1

멤버1

11

1

2

멤버2

12

1

3

멤버3

13

2

4

멤버4

14

2

5

멤버5

15

3

6

멤버6

16

3

7

멤버7

17

NULL

  • JPA 2.1 부터 조인시 ON 절 지원.

10.2.7 페치 조인 - JPQL

  • SQL 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능

  • 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능

엔티티 페치 조인

아래 설명할 정도로 책이 깔끔하지안은거 가타

컬렉션페치조인

페치 조인과 DISTINCT

Q. distinct는 컬렉션 패치조인하고 꼭 같이 써야 하는가?

페치 조인의 특징과 한계

  • 특징

    • 페치 조인은 글로벌 로딩 전략보다 우선한다(@0neToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략)

    • 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회

    • 따라서 지연 로딩을 사용하고, 경우에 따라 EAGER로딩이 필요할 경우, 최적화를 위해 페치 조인을 적용하는 것이 효과적

  • 한계

    • 페치 조인 대상에는 별칭을 줄 수 없다. 따라서 SELECT, WHERE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다. (하이버네이트는 지원) 예를들어, 위 페치조인대상인 팀의 컬럼으로 where절에서 사용불가.

    • 둘 이상의 컬렉션을 페치할 수 없다. 카테시안 곱이 만들어지므로 주의.

    • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다. 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일,다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.

페치조인 정리

  • 페치 조인은 실무에서 자주 사용

  • 객체 그래프를 유지할 때 사용하면 효과적. 멤버->팀.

  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하다면 억지로 페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO로 반환하는 것이 더 효과적일 수 있다.

10.2.8 경로표현식 - JPQL

  • JPQL에서 사용하는 경로 표현식을 알아보고 경로 표현식을 통한 묵시적 조인도 알아보자.

  • 경로 표현식이라는 것은 .(점)을 찍어 객체 그래프를 탐색하는 것

  • m.username, m.team, m.orders, t.name이 경로 표현식

    경로 표현식을 이해하려면 우선 다음 용어들을 알아야 한다.

  • 상태필드: 단순히 값을 저장하는 필드

  • 연관필드: 객체 사이의 연관관계를 맺기 위해 사용하는 필드, 임베디드 타입 포함

    • 단일 값 연관 필드: @ManyToOne, @OneToOne -> 대상이 엔티티

    • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany -> 대상이 컬렉션

경로표현식 특징

  • 상태 필드 경로: 경로 탐색의 끝이다. 더는 탐색할 수 없다.

  • 단일 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.

  • 컬렉션 값 연관 경로: 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없다. 단, FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.

상태 필드 경로탐색

단일 값 연관 경로

  • 주문 중에서 상품명이 'productA'고 배송지가 'JINJU'인 회원이 소속된 팀을 조회해보자.

  • 문제 - 위에는 명시적일까? 묵시적걸까?

    • o.member.team와 같이 경로탐색을 사용했기에 묵시적이다.

  • 문제 - 위 쿼리는 몇번의 조인이 일어날까?

    • 3번 조인 - 아래 참고

컬렉션 값 연관 경로 탐색

  • 컬렉션 값에서 경로 탐색을 시도하는 실수를 하지말자.

  • t.members 처럼 컬렉션까지는 경로 탐색이 가능하나 username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다. username을 얻기 위해선 아래 같이 각팀 멤버들을 별칭으로 받는다.

size라는 특별한 기능을 사용 가능

묵시적 조인 시 주의사항

경로 탐색을 사용하면 묵시적 조인이 발생해서 SQL에서 내부 조인이 일어난다.

  • 항상 내부 조인이다. SELECT t.members FROM Team t 에서 '팀1'에 멤버가 없으면 결과에서 제외됨.

  • 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.

  • 경로 탐색은 주로 SELECT, WHERE 절 (다른 곳에서도 사용됨)에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.

결론, 단순하고 성능에 이슈가 없으면 크게 문제가 안 되지만 성능이 중요하면 분석하기 쉽도록 묵시적 조인보다는 명시적 조인을 사용하자.

10.2.9 서브 쿼리 - JPQL

  • JPQL도 SQL처럼 서브 쿼리를 지원

  • WHERE, HAVING 절에서만 사용 O,

  • SELECT, FROM 절에서는 사용 X

문제 - 나이가 평균보다 많은 회원 찾는 쿼리

문제 - 한 건이라도 주문한 고객을 찾는 쿼리

서브 쿼리 함수

  • [NOT] EXISTS (subquery)

  • {ALL | ANY | SOME} (subquery)

  • [NOT] IN (subquery)

서브쿼리들을 실습해보자.

10.2.10 조건식 - JPQL

컬렉션식

컬렉션 식은 컬렉션에만 사용하는 특별한 기능이다. 문제 - 주문이 하나라도 있는 회원 조회

아래는 가능할까?

  • IS NULL은 컬렉션 식이 아니다.

컬렉션 멤버식

문법 : { 엔티티나 값} [NOT] MEMBER [OF] { 컬렉션값 연관 경로 } 매개변수로 받은 멤버가 속한 팀을 컬렉션의 멤버식을 이용해 찾아보자.

스칼라식

  • 문자함수: CONCAT, LOWER, UPPER, LENGTH, SUBSTRING, TRIM, LOCATE 아래의 값은 어떻게 나올까?

  • 수학함수: ABS, SQRT, MOD, SIZE, INDEX

  • 날짜함수: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP

하이버네이트에서는 아래 기능 지원

CASE 식

10.2.11 다형성쿼리 - JPQL

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

위 실행시 두가지 전략(단일테이블, 조인)을 사용할때 각각 SQL이 다음과 같이 나온다.

TYPE - 조회 대상을 특정 자식 타입으로 한정할 때 사용 예, Item 중에 Book, Movie를 조회하라.

TREAT - 자바의 타입 캐스팅과 비슷하며 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. 예, 부모 타입인 Item을 자식 타입인 Book으로 다룬다. 이로 author 필드에 접근할 수 있다.

10.2.12 사용자 정의 함수 호출(JPA 2.1) - JPQL

하이버네이트의 경우 - 아래같이 방언 클래스를 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야 한다.

10.2.13 기타정리 - JPQL

  • enum은 = 비교 연산만 지원한다.

  • 임베디드 타입은 비교를 지원하지 않는다. 예, Order o WHERE o.address = ... X

  • JPA 표준은 ''을 길이 0인 EmptyString으로 정했지만 데이터베이스에 따라 ''를 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용하자.

  • NULL 정의

    • NULL과의 모든 수학적 계산 결과는 NULL

    • Null == Null은 알 수 없는 값

    • Null is Null은 참

JPA 표준 명세는 NULL(U) 값과 TRUE(T), FALSE(F)의 논리 계산을 다음과 같이 정의하였다. 나중에 자세히 알아보기

AND

AND

T

F

U

T

T

F

U

F

F

F

F

U

U

F

U

OR

OR

T

F

U

T

T

T

T

F

T

F

U

U

T

U

U

NOT

NOT

T

F

F

T

U

U

10.2.14 엔티티 직접 사용 - JPQL

기본키값

JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키값을 사용한다

엔티티를 파라미터로 직접 받는 코드(엔티티로 파라미터 줘도 ID로 조회)

외래키값

10.2.15 Named Query: 정적쿼리 - JPQL

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나뉜다.

  • 동적쿼리

    • em.createQuery("select..") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.

  • 정적쿼리 = Named 쿼리

    • 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용한다. 한 번 정의하면 변경할 수 없는 정적인 쿼리다.

    • 애플리케이션 로딩시점에 JPQL 문법을 체크하고 미리 파싱해 둔다. 따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다. 쿼리캐시자료.arrow-up-right

@NamedQuery @NamedQueries

Member.findByUsername처럼 앞에 엔티티 이름을 주었는데, Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위해 엔티티 이름을 앞에 주었다. 즉, 엔티티 이름이 앞에 있으면 관리하기가 쉽다.

  • lockMode: 쿼리 실행 시 락을 건다. 16.1절에서 자세히.

  • hints: SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트. 2차 캐시를 다룰 때 사용한다.

Named 쿼리를 XML에 정의

  • JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성

  • Named 쿼리를 작성할 때, XML을 사용하는 것이 더 편리. 멀티라인을 자바에서 다루면은 아래처럼 상당히 귀찮기 때문이다.

  • 참고

    • XML에서 &, <,>는 XML 예약문자라 &, <, >를 사용해야 하지만 <![CDATA[ ]]>안에서는 예약문자 자유롭게 사용.

    • XML과 어노테이션에 같은 설정 이 있으면 XML이 우선권

10.3 Criteria 쿼리

JPA 표준으로 지원하지만 복잡하여 생략. QueryDSL 사용하기.

10.3.1 기초 - Criteria

10.3.2 쿼리 생성 - Criteria

10.3.3 조회 - Criteria

10.3.4 집합 - Criteria

10.3.5 정렬 - Criteria

10.3.6 조인 - Criteria

10.3.7 서브쿼리 - Criteria

10.3.8 IN 식 - Criteria

10.3.9 CASE 식 - Criteria

10.3.10 파라미터 정의 - Criteria

10.3.11 네이티브 함수 호출 - Criteria

10.3.12 동적 쿼리 - Criteria

10.3.13 함수정리 - Criteria

10.3.14 메타모델 API - Criteria

10.4 QueryDSL

특징

  • QueryDSL은 JPQL 빌더 역할을 하며 Criteria 대체 가능

  • 쉽고 간결하며 모양도 쿼리와 비슷하게 개발 가능

10.4.1 설정 - QueryDSL

필요 라이브러리(pom.xml)

  • querydsl-jpa QueryDSL JPA 라이브러리

  • querydsl-apt 쿼리 타입(Q)을 생성할 때 필요한 라이브러리

환경설정

  • QueryDSL을 사용하려면 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 함(예, QMember 엔티)

  • 쿼리 타입 생성용 플러그인을 pom.xml에 추가

    • 콘솔에서 mvn compile을 입력하면 outputDirectory에 지정한 target/generated-sources 위치에 QMember.java처럼 Q로 시작하는 쿼리 타입들이 생성

10.4.2 시작 - QueryDSL

10.4.3 검색조건 쿼리 - QueryDSL

10.4.4 결과 조회 - QueryDSL

10.4.5 페이징과 정렬 - QueryDSL

10.4.6 그룹 - QueryDSL

10.4.7 조인 - QueryDSL

10.4.8 서브 쿼리 - QueryDSL

10.4.9 프로젝션과 결과 반환 - QueryDSL

10.4.10 수정, 삭제 배치 쿼리 - QueryDSL

10.4.11 동적 쿼리 - QueryDSL

10.4.12 메소드 위임 - QueryDSL

10.4.13 정리 - QueryDSL

10.5 Native SQL

10.5.1 사용 - Native SQL

10.5.2 Named Native SQL

10.5.3 XML에 정의

10.5.4 정리

10.5.5 Stored Procedure(JPA 2.1)

10.6 객체지향 쿼리 심화

10.6.1 벌크연산

한 번에 여러 데이터 를 수정하는것. 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키려면?

.executeUpdate();

가격이 100원 미만인 모든 상품을 삭제하려면?

하이버네이트는 아래같이 대량 삽입(INSERT)도 가능

벌크 연산의 주의사항

영속성 컨텍스트를 무시하고 데이터베이스 에 직접 쿼리한다

해결법

  1. em.refresh() -> 데이터베이스에서 다시 조회

  2. 벌크 연산 먼저 실행 -> 가장 실용적인 해결책

  3. 벌크 연산 수행 후 영속성 컨텍스트 초기화 -> 영속성 컨텍스트에 남아 있는 엔티티를 제거하여 DB 조회하게

10.6.2 영속성컨택스트와 JPQL

  • JPQL의 조회 대상 - 엔티티, 임베디드 타입, 값 타입

    • 3개중 엔티티만 영속성 컨택스트에서 관리되고, 임베디드랑 값타입은 변경 감지에 의한 수정이 발생하지 않는다.

10.6.3 find() vs JPQL

10.6.4 JPQL과 플러시 모드

변경 내역을 데이터베이스에 동기화하는 것

FlushModeType.AUTO FlushModeType.COMMIT

그럼 무조건 디폴트 AUTO로 쓰면 되는거 아니야? - 쿼리시 발생하는 플러시 횟수를 줄여서 성능을 최적화

JDBC는 오토 기능이 없으므로 쿼리전 항상 플러시 호출

10.7 정리

  • JPQL은 SQL을 추상화해서 특정 DB에 종속되지 않는다.

  • Creteria나 QueryDQL은 JPQL을 만들어주는 비럳 역할을 할 뿐이므로 JPQL을 잘 알아야 한다.

  • Creteria나 QueryDQL 를 사용하면 동적으로 변하는 쿼리 작성 가능

  • Creteria -> JPA지원 O, 직관적 X

  • QueryDSL -> JPA지원 X 직관적 O

  • JPA도 Native SQL을 지원하나 DBMS 변경시 SQL 작성 규칙이 다르면, 쿼리 모두 수정해야 되는 상황. 최대한 JPQL 사용.

  • JPQL -> 대량 데이터 수정 삭제하는 벌크연산 지원

최초 작성일 2020.05.18

Last updated