본문 바로가기
JPA

[JPA] JPQL(객체 지향 쿼리 언어)

by jinjin98 2022. 11. 15.

̱ JPQL 

 

JPA 는 ORM 기술입니다. ORM 은 Object Relation미 Mapping 의 약자로 객체와 관계형 데이터베이스를 

매핑한다는 의미입니다. ORM 을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하며

검색도 테이블이 아닌 엔티티 객체를 대상으로 합니다. 이 때 사용하는게 JPQL 입니다.

 

JPQL객체지향 쿼리이며 SQL 을 추상화해서 특정 데이터베이스 SQL 에 의존하지 않습니다.

또한 데이터베이스 방언만 변경하면 JPQL 을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있습니다.

예를 들면 같은 결과를 가져오는 SQL 함수라도 데이터베이스마다 사용 문법이 다른 것이 있는데

JPQL 이 제공하는 표준화된 함수를 사용하면 선택한 방언에 따라

해당 데이터베이스에 맞춘 적절한 SQL 함수가 실행됩니다.

SQL 이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면

JPQL 은 엔티티 객체를 대상으로 하는 객체지향 쿼리입니다.

JPQL 을 사용하면 JPA 는 이 JPQL 을 분석하여 적절한 SQL 을 생성해 데이터베이스에서 조회합니다.

그리고 조회한 결과로 엔티티 객체를 생성해 반환합니다. 

 

 

이번 포스팅에서 사용되는 엔티티와 엔티티와 매핑되는 데이터베이스 테이블입니다.

 

 JPQL 도 SQL 과 비슷하게 SELECT, UPDATE, DELETE, FROM, WHERE, GROUP BY, HAVING, JOIN 문을

사용할 수 있습니다. 참조로 엔티티를 저장할 때는 EntityManager 참조 변수.persist() 메서드를 사용하면

되기 때문에 INSERT 문은 없습니다.

 

SELECT 문

 

SELECT m FROM Member m WHERE m.username = "jin"

 

JPQL 로 SELECT 문을 작성한 예제입니다.

 

JPQL 특징

 

1. 대소문자 구분

엔티티와 속성은 대소문자를 구분합니다. 위 코드에서 Member, username 은 대소문자를 구분합니다.

반면에 SELECT, FROM, 같은 JPQL 키워드는대소문자를 구분하지 않습니다.

 

2. 엔티티 이름

JPQL 에서 사용한 Member 는 클래스 명이 아닌 엔티티 명입니다. 엔티티 명은 @Entity 어노테이션에서 

name 속성값으로 지정할 수 있습니다. 엔티티 명을 지정하지 않으면 클래스명을 기본값으로 사용합니다.

 

3. 별칭

Member 에 m 이라는 별칭을 주었습니다. JPQL 에서는 별칭을 필수로 사용해야합니다.

 

TypeQuery, Query

 

작성한 JPQL 을 실행하려면 쿼리 객체를 만들어야 합니다. 쿼리 객체는 TypeQuery 와 Query 가 있는데

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

Query 객체를 사용하면 됩니다.

 

TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

 

 

JPQL 을 작성할 때는 createQuery() 메서드를 사용하는데 첫 번째 인자값으로는 쿼리문을 작성한 문자열을,

두 번째 인자값으로는 반환할 엔티티 타입을 지정합니다. 반환할 엔티티 타입을 지정하면 반환할 타입을 

명확하게 지정한 것이니 TypeQuery 객체를 반환합니다.

 

Query query = em.createQuery("SELECT m.username, m.age from Member m");

 

이번에는 반환할 타입을 지정하지 않고 조회할 열을 username 과 age 로 선택해 프로젝션을 사용했습니다.

이 때는 Query 객체를 반환합니다.

 

결과 조회

쿼리 객체를 생성하고 다음 메서드들을 호출하면 실제 쿼리를 실행해서 데이터베이스에서 값을 조회합니다.

query.getResultList(): 결과가 하나 이상일 때, 리스트 반환하고, 결과가 없으면 빈 리스트 반환합니다.

query.getSingleResult(): 결과가 정확히 하나일 때 사용합니다. 단일 객체를 반환합니다. 결과가 정확히 1개가

아니면 예외가 발생합니다.

 

파라미터 바인딩

 

1. 이름 기준

 

TypedQuery<Member> query 
	= em.createQuery("SELECT m FROM Member m where m.username=:username ", Member.class);

query.setParameter("username", "홍길동");
List<Member> members = query.getResultList();

 

이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법입니다. 이름 기준 파라미터는 앞에 를 사용합니다.

위 코드에서는 :username 이라는 이름 기준 파라미터를 정의하고 query.setParameter() 에서 username 이라는

이름으로 파라미터를 바인딩했습니다. 

 

List<Member> members
        = em.createQuery("SELECT m FROM Member m where m.username=:username", Member.class)
            .setParameter("username", "홍길동")
            .getResultlist();

 

JPQL API 는 대부분 메서드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있습니다.

 

2. 위치 기준 파라미터

 

List<Member> members
        = em.createQuery("SELECT m FROM Member m where m.username=?1", Member.class)
            .setParameter(1, "홍길동")
            .getResultList();

 

위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 됩니다. 위치 값은 1부터 시작합니다.

위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확합니다.

 

프로젝션

 

관계형 데이터베이스 SQL 에서 프로젝션이 있듯이 JPQL 에도 프로젝션이 있는데요. 프로젝션은 SELECT 절에

조회할 대상을 지정하는 것을 의미합니다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있습니다.

 

1. 엔티티 프로젝션

ex) SELECT m.team FROM Member m 

회원과 연관된 팀을 조회합니다. 엔티티를 프로젝션 대상으로 사용하여 원하는 객체를 바로 조회했는데요.

컬럼을 일일이 나열해서 조회하는 SQL 과는 차이가 있습니다. 참고로 이렇게 조회한 엔티티는 

영속성 컨텍스트에서 관리됩니다.

 

2. 임베디드 타입 프로젝션

ex) SELECT m.address FROM Member m

임베디드 타입은 엔티티와 거의 비슷하게 사용됩니다. 임베디드 타입은 조회의 시작점이 될 수 없는 제약이 있습니다.

임베디드 타입은 엔티티 타입이 아닌 값 타입이기 때문에 직접 조회한 임베디드 타입은 영속성 컨텍스트에 

관리되지 않습니다.,

 

3. 스칼라 타입 프로젝션

ex) SELECT m.username, m.age FROM Member m 

스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 의미합니다.

중복 데이터를 제거할 떄는 SELECT 키워드 뒤에 DISTINCT 를 사용합니다.

ex) SELECT DISTINCT  m.username, m.age FROM Member m 

 

프로젝션으로 조회할 떄 사용하는 타입

 

1. Query

 

Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
    Objects[] row (Objects[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}

 

앞서 설명했듯이 명확하게 반환 타입을 지정하지 않으면 Query 를 사용합니다.

 

2. Object 배열

 

List<Object []> resultList = em.createQuery("SELECT m.username, m.age from Member m").getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
    Objects[] row (Objects[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}

 

조회한 값들의 타입이 다를 수 있으니 Object 타입의 배열로 받아 해당 타입에 맞는 타입으로 형변환을 

할 수 있습니다.

 

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

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
    Objects[] row (Objects[]) iterator.next();
    Member username = (Member) row[0];
    Product age = (Product) row[1];
    int age = (Integer) row[2];
}

 

엔티티 타입도 여러 값을 함께 조회할 수 있습니다.

 

3. new 명령어, DTO(Data Transaction Object) 클래스로 받기

 

public class UserDTO{

    private String username;
    private int age;

    public UserDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

TypeQuery<UserDto> query =
                em.createQuery("SELECT new jpabook.jpql.USERDTO(m.username, m.age) from Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();

 

데이터를 반환받을 클래스를 생성해서 해당 클래스 타입으로 데이터를 받습니다. 반환 타입을 지정해줄 수 있기

때문에 객체 변환 작업을 줄일 수 있습니다. DTO 를 사용할 떄는 SELECT 뒤에는 패키지 명을 포함한

전체 클래스 명을 적어주고 조회할 값들을 생성자 인자값으로 넣어줍니다.

 

집합과 정렬

 

집합은 집합함수와 함꼐 통계정보를 구할 때 사용합니다.

 

select
    COUNT(m),
    SUM(m.age),
    AVG(m.age),
    MAX(m.age),
    MIN(m.age)
from Member m;

 

에제 코드입니다. 회원 수, 나이 합, 평균 나이, 최대 나이, 최소 나이를 조회합니다.

 

집함 합수

 

함수 설명
COUNT 결과 수를 구합니다. 반환타입은 long 입니다
MAX, MIN 최대, 최소 값을 구합니다. 문자, 숫자, 날짜 등에 사용합니다
AVG 평균값을 구합니다. 숫자타입만 사용할 수 있습니다.
반환 타입은 Double 입니다
SUM 합을 구합니다. 숫자 타입만 사용할 수 있습니다.
반환 타입: 정수합 Long, 소수합: Double,
BigInteger : BigInteger, BigDecimal : BigDecimal

 

집합 함수 사용시 참고사항

NULL 값은 무시하므로 통계에 잡히지 않습니다. (Distinct 가 정의되어 있어도 무시됩니다.)

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

Distinct 를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있습니다.

ex) select COUNT (DISTINCT m.age ) from Member m

Distinct  를 COUNT 에서 사용할 떄 임베디드 타입은 지원하지 않습니다.

 

GROUP BY, HAVING

 

select t.name COUNT(m.age)
from Member m.LEFT JOIN m.team team
GROUP BY t.name;

 

관계형 데이터베이스에서 사용하는 것과 같이 GROUP BY 는 통계 데이터를 구할 떄 특정 그룹끼리 묶어줍니다.

 

select t.name COUNT(m.age)
from Member m.LEFT JOIN m.team team
GROUP BY t.name
HAVING AVG(m.age) >= 10;

 

HAVING 은 GROUP BY 와 함꼐 사용하며, GROUP BY 로 그룹핑한 통계 데이터에 조건을 주어 필터링 합니다.

 

ORDER BY

 

select m from Member m order by m.age DESC, m.username ASC

 

ORDER BY 는 정렬할 때 사용합니다. 위 코드는 회원의 나이를 내림차순 으로 정렬하고 

회원의 나이가 같다면 회원의 이름을 오름차순, 즉 사전순으로 정렬합니다.

ASC: 오름차순(기본값)

DESC: 내림차순

 

JPQL 조인

JPQL 도 조인을 지원합니다. SQL 조인과 기능은 같지만 문법이 살짝 다릅니다.

 

내부 조인

 

String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
        + "WHERE t.name = : teamName";

List<Member> members = em.createQuery(query, Member.class)
        .setParameter("teamName", teamName)
        .getResultList();

 

회원과 팀을 내부 조인해서 '팀A' 에 소속된 회원을 조회하는 JPQL 을 생성했습니다. 

내부 조인을 할 때는 INNER JOIN 에서 INNER 를 생략할 수 있습니다.

 

SELECT 
    M.ID AS ID
    M.AGE AS AGE,
    M.TEAM.ID AS TEAM.ID,
    M.NAME AS NAME
FROM 
    MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE 
    T.NAME = ?

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. JPQL 조인문과 다른 것을 확인할 수 있는데요.

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것입니다. m.team 이 연관 필드인데 연관 필드는

다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말합니다. Member 테이블이 Team 테이블의 기본키를

외래키로 들고 있고, 외래키가 Member 엔티티의 Team 객체와 매핑되어 있기 때문에 이렇게 사용하는 것이죠.

FROM Member m: 회원을 선택하고 m 이라는 별칭을 주었습니다

Member m INNER JOIN m.team t: 회원이 가지고 있는 연관 필드로 팀과 조인합니다. 조인한 팀에는 t 라는 

별칭을 주었습니다.

 

SELECT m, t FROM Member m INNER JOIN m.team t

 

만약 조인한 두 개의 엔티티를 조회하려면 위 코드와 같이 작성하면 됩니다. 이 때는 서로 다른 타입의 

두 엔티티를 조회했으므로 TypeQuery 를 사용할 수 없습니다.

 

외부 조인

 

SELECT m FROM Member m LEFT OUTER JOIN m.team t

 

외부 조인도 연관 필드를 사용하며, OUTER 는 생략이 가능합니다.

 

SELECT
        M.ID AS ID
        M.AGE AS AGE,
        M.TEAM.ID AS TEAM.ID,
        M.NAME AS NAME
FROM
        MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE
        T.NAME = ?

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다.

 

컬렉션 조인

 

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

회원 - > 팀 으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team) 를 사용합니다

팀 -> 회원 으로의 조인은 일대다 조인이면서 컬렉션 값 연관 필드(m.members) 를 사용합니다.

 

ex) SELECT t, m FROM Team t LEFT JOIN t..members m

일대다 조인을 해서 t LEFT JOIN t..members m 는 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 

외부조인을 했습니다.

 

세타 조인

 

select count(m) 
from Member m, Team t
where m.username = t.name;

 

JPQL 에서는 WHERE 절을 사용해서 세타 조인도 할 수 있습니다. 세타 조인은 내부 조인만을 지원하며

전혀 관계없는 엔티티도 조인할 수 있습니다.

 

SELECT COUNT(M.ID)
FROM
    MEMBER M CROSS JOIN TEAM T
WHERE
    M.USERNAME = T.NAME;

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. 조인 조건을 ON 이 아닌 WHERE 에  작성된 것을 볼 수 있습니다.

 

JOIN ON 절

ON 절을 사용하면 조인 대상을 필터링하고 조인을 할 수 있습니다. 내부 조인의 ON 절은 WHERE 절을

사용했을 떄와 결과가 같기 때문에 보통 ON 절은 외부 조인에서만 사용합니다.

 

//JPQL
SELECT m, t FROM Member m LFFT JOIN m.team t ON t.name = 'A'
      
//SQL
SELECT m.*, t.*
FROM Member m
LEFT JOIN Team t 
ON m.TEAM_ID = t.id and t.name='A'

 

페치 조인

 

페치 조인은 SQL 문에는 존재하지 않고 JPQL 에서 성능 최적화를 위해 제공하는 기능입니다. 페치 조인은

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능입니다. join fetch 라는 명령어를 입력해서 사용할 수 

있습니다. 

 

페치 조인문법

[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

 

select m
from Member m join fetch m.team

 

페치 조인을 해서 회원과 팀을 함께 조회하는 JPQL 입니다.

 

SELECT M.*, T.*
FROM Member m
INNER JOIN Team t ON M.TEAM_ID = T.ID

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. 페지 조인 JPQL 을 실행하면 SQL 문에서는 페치 조인이

내부 조인으로 변환되고, select m 으로 회원 엔티티만 선택했는데 실행된 SQL 을 보면 SELECT M.*, T.* 로

회원과 연관된 팀도 함께 조회하는 것을 확인할 수 있습니다.

 

 

String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class);

for (Member member : members) {
    System.out.println("username = " + member.getUsername() +
            "teamname = " + member.getTeam().getName());
}

결과
username = 회원1, teamname = 팀A 
username = 회원2, teamname = 팀A 
username = 회원3, teamname = 팀B

 

회원 엔티티에서 팀 필드를 지연로딩으로 설정하면 팀 엔티티를 조회할 때 프록시로 조회하고 실제 팀 엔티티를

사용할 때 팀 데이터를 데이터베이스에서 조회하게 됩니다. 하지만 페치 조인을 사용하면 회원을 조회활 때 팀도

같이 조회하므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티 입니다. 그렇기 때문에 회원 엔티티가 영속성

컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있습니다.

 

컬렉션 페치 조인

 

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

 

이번에는 일대다 관계인 컬렉션을 페치 조인해봤습니다. 팀을 조회하면서 페치 조인을 사용해서 연관된 회원 컬렉션도 

함께 조회합니다.

 

SELECT M.*, T.*
FROM Team t
INNER JOIN Member m ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'

 

 JPQL 이 실행되었을 때 생성되는 SQL 문입니다. 페치 조인한  JPQL 에서 select t 로 팀만 선택했는데 실행된

SQL 문을 보니 T.*, M.* 로 팀과 연관된 회원도 함꼐 조회한 것을 확인할 수 있습니다.

 

 

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"); 
    }
}

결과
teamname = 팀A, team = Team@0x100 
username = 회원1, member = Member@0x200 
username = 회원2, member = Member@0x300 

teamname = 팀A, team = Team@0x100 
username = 회원1, member = Member@0x200 
username = 회원2, member = Member@0x300

 

일대다 관계인 컬렉션을 페치 조인을 하니 Team 테이블에서 '팀A' 는 하나지만 Member 테이블과 조인하면서

결과가 증가해서 '팀A' 가 2건 조회되었습니다. 이처럼 일대다 조인은 결과가 증가할 수 있고 일대일, 다대일

조인은 결과가 증가하지 않습니다.

 

페치 조인과 DISTINCT

 

SQL 의 DISTINCT 는 중복된 결과를 제거하는 명령어입니다. JPQL 의 DISTINCT 명령어는 SQL 에

DISTINCT 를 추가하는 것은 물론이고 어플리케이션에서 한 번 더 중복을 제거합니다.

방금 설명한 컬렉션 페치 조인은 팀A 가 중복으로 조회됩니다. 여기서 DISTINCT 를 추가해보겠습니다.

 

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

 

 

먼저 DISTINCT 를 사용하면 SQL 에 SELECT DISTINCT 가 추가됩니다. 하지만 지금은 각행의 데이터가 

다르므로 SQL 의 DISTINCT 는 효과가 없습니다. 

 

 

결과
teamname = 팀A, team = Team@0x100
username = 회원1, member = Member@0x200
username = 회원2, member = Member@0x300

 

다음으로는 어플리케이션에서 DISTINCT 명령어를 보고 중복된 데이터를 걸러냅니다. select DISTINCT t 의

의미는 팀 엔티티의 중복을 제거하라는 것입니다.

 

페치 조인과 내부 조인의 차이

 

//JPQL
select t
from Team t join t.members m
where t.name = '팀A'

//SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

 

페치 조인이 아닌 내부 조인을 하고 변환된 SQL 문을 보면 팀만 조회하고 조인했던 회원은 조회하지 않습니다.

JPQL 은 결과를 반환할 때 연관관계까지 고려하지 않습니다. 단지 SELECT 절에 지정한 엔티티만 조회할 

뿐입니다. 만약 연관된 엔티티인 회원 엔티티를 지연 로딩으로 설정했다면 프록시나 초기화되지 않은 컬렉션

래퍼를 반환합니다. 즉시 로딩으로 설정했다면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행합니다.

 

페치 조인의 특징과 한계

 

페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함꼐 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을

최적화할 수 있습니다. @ManyToOne(fetch = FetchType.LAZY) 와 같이 엔티티에 직접 적용하는 로딩

전략은 어플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부릅니다. 페치 조인은 글로벌 로딩

전략보다 우선 순위가 높습니다. 예를 들어 연관된 엔티티의 글로벌 로딩 전략을 지연 로딩으로 설정해도

JPQL 에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회합니다. 최적화를 위해 글로벌 로딩 전략을

즉시 로딩으로 설정하면 어플리케이션 전체에서 항상 즉시 로딩이 일어납니다. 물론 일부는 빠를 수 있겠지만

전체적인 측면에서 본다면 사용하지 않는 엔티티를 자주 로딩하므로 성능에 악영향을 미치는 부작용을 낳을 수 

있습니다. 따라서 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을

적용하는 것이 효과적입니다. 또한 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이

발생하지 않습니다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있습니다.

 

페치 조인은 다음과 같은 한계가 있습니다.

 

1. 페치 조인 대상에는 별칭을 줄 수 없습니다.

지금까지 페치 조인을 적용한 JPQL 문을 보면 별칭이 없었는데요 SELECT, WHERE 절, 서브 쿼리에 페치 조인

대상을 사용할 수 없습니다.

 

2. 둘 이상의 컬렉션을 페치할 수 없습니다.

 

3. 컬렉션을 페치 조인하면 페이징 API 를 사용할 수 없습니다.

컬렉션(일대다)이 아닌 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인하면 페이징 API 를 사용할

수 있습니다. 하이버네이트에서 컬렉션을 페치 조인하고 페이징 API 를 사용하면 경고 로그를 남기면서

메모리에서 페이징 처리를 합니다. 이 때 데이터가 많아지게 되면 성능 이슈와 메모리 초과 예외가

발생할 수도 있습니다.

 

페치 조인은 연관된 엔티티들을 SQL 한 번으로 조회할 수 있어 성능 최적화를 할 수 있습니다. 하지만

모든 것을 페치 조인을 통해 해결할 수는 없는데요. 페치 조인은 객체 그래프를 유지할 때 사용하면

효과적입니다. 반면에 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 도출해야

한다면 억지로 페치 조인을 사용하기보다는 여러 테이블에서 필요한 필드들만 조회해서 DTO 로

반환하는것이 더 효좌적일 수 있습니다.

 

경로 표현식

 

경로 표현식은 지금까지 보여드린 JPQL 코드에서 계속 사용했습니다. 경로 표현식은 .(점) 을 찍어서

객체 그래프를 탐색하는 것입니다.

 

select m.username // 상태 필드
from Member m
join m.team t //단일 값 연관 필드
join m.orders o // 컬렉션 값 연관 필드
where t.name = '팀A'

 

상태 필드(state field): 단순히 값을 저장하기 위한 필드입니다. (ex: m.username) 

연관 필드(association field): 연관관계를 위한 필드입니다. 임베디드 타입을 포함합니다.

- 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티입니다. (ex: m.team)

- 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션입니다. (ex: m.orders)

 

상태 필드는 단순히 값을 저장하는 필드이고

연관 필드는 객체 사이의 연관관계를 맺기 위해 사용하는 필드입니다.

 

경로 표현식과 특징

 

상태 필드: 경로 탐색의 끝입니다. 더는 탐색이 불가능합니다.

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

컬렉션 값 연관 경로: 묵시적으로 내부 조인이 일어납니다. 더는 탐색이 불가능합니다. 단 FROM 절에서

조인을 통해 별칭을 얻으면 별칭으로 탐색이 가능합니다.

 

상태 필드 경로 탐색

JPQL: select m.username, m.age from Member m 

SQL: select m.username, m.age from Member m

 

단일 값 연관 경로 탐색

JPQL: select o.member from Order o 

SQL: select m.* from Orders o inner join Member m on o.member_id = m.id

 

o.member 를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색을 했습니다. 단일 값 연관 필드로 경로 탐색을

하면 SQL 문에서 내부 조인이 일어나는데 이것을 묵시적 조인이라고 합니다. 직접적으로 조인문을 작성하지

않았는데 조인이 실행된 것이죠. 여기서 중요한 점은 묵시적 조인은 모두 내부 조인이라는 것입니다. 외부 조인은

명시적으로 JOIN 키워드를 사용해야 합니다.

 

명시적 조인: JOIN 을 직접 적어주는 것입니다

묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것입니다.

 

예제를 살펴보겠습니다.

 

select o.member.team
from Order o
where o.product.name ='productA' and o.address.city = 'gimpo';

 

주문 중에서 주문 상품명이 'productA' 고 배송지가 'gimpo' 인 회원이 소속된 팀을 조회하는 JPQL 입니다.

 

select t.*
from Orders o
inner join Member m on o.member_id = m.id
inner join Team t on m.team_id = t.id
inner join Product p on o.product_id = p.id
where p.name = 'productA' and o.city = 'gimpo'

 

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. 총 3번의 조인이 발생했습니다. o.address 처럼 임베디드 타입에

접근하는것도 단일 값 연관 경로 탐색이지만 주문 테이블에 이미 포함되어 있으므로 조인이 발생하지 않습니다.

 

컬렉션 값 연관 경로 탐색

 

select m.username from Team t join t.members m

 

컬렉션 값에서 경로 탐색은 더 이상 불가능하다고 앞에서 설명했는데요. 만약 컬렉션에서 경로 탐색을 하고 싶다면

조인을 사용해서 새로운 별칭을 얻어야 합니다.

 

select t.members.size from Team t

 

참고로 컬렉션은 컬렉션의 크기를 구할 수 있는 size 라는 특별한 기능을 사용할 수 있습니다.

size 를 사용하면 COUNT 함수를 사용하는 SQL 로 적절하게 변환됩니다.

 

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

 

1. 항상 내부 조인을 합니다 

2.컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 합니다.

3. 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해

SQL의 FROM (JOIN) 절에 영향을 줍니다.

 

조인은 성능에 차지하는 부분이 매우 큽니다. 묵시적 조인은 보다시피 조인이 일어는 상황을 한 눈에 파악하기가

어렵다는 단점이 있습니다. 그렇기 때문에 가급적이면 묵시적 조인 대신에 명시적 조인 사용을 해야 합니다.

 

서브 쿼리

 

JPQL 에서도 SQL 처럼 서브 쿼리를 지원합니다. 대신 몇 가지 제약이 있습니다. 서브쿼리를

WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용이 불가능합니다.

하이버네이트에서는 SELECT 절 서브 쿼리도 가능해서 JPA 도 SELECT 절 서브 쿼리까지는사용할

수 있습니다. (JPA 구현체가 하이버네이트입니다.) 하지만 아직까지 FROM 절의 서브 쿼리는 지원하지

않습니다. 그렇기 때문에 FROM 절에 서브 쿼리가 필요하다면 조인으로 해결해야 합니다,

 

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

 

이번에는 한 건이라도 주문을 한 회원을 찾는 쿼리입니다.

 

select m from Member m
where m.orders.size > 0

 

다음과 같이 컬렉션 값 연관 필드의 size 기능을 사용해도 같은 결과를 얻을 수 있으며

실행되는 SQL 도 같습니다.

 

서브 쿼리 지원 함수

 

[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참 입니다

ex) select m from Member m where exists (select t from m.team t where t.name = ‘팀A')

 

{ALL | ANY | SOME} (subquery): 비교 연산자와 같이 사용합니다. ALL 모두 만족하면 참 ,

•ANY, SOME: 은 조건을 하나라도 만족하면 참입니다.

ex)

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)

 

[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참입니다. IN 은 서브쿼리가 아닌 곳에도

사용됩니다.

ex)

select t from Team
where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)

 

조건식

 

타입 표현

종류 설명 예제
문자 작은 따옴표 사이에 표현
작은 따옴표를 표현하고 싶으면 작은 
따옴표 연속 두 개를 사용
'Hello'
'She''s'
숫자 L(Long 타입 지정)
D(Double 타입 지정)
F(Floadt 타입 지정)
10L
10D
10F
날짜 DATE {d 'yyyy-mm-dd'}
TIME {t 'hh-mm-ss'}
DATETIME {ts 'yyyy-mm-dd hh:mmss.f'}
{d '2022-11-14'}
{t '10-39-33'}
{ts '2012=03-24 10-11-11.123'}
m.crerateDate = {d '2012-03-24'}
Boolean TRUE, FALSE  
Enum 패키지명을 포함한 전체 이름 사용 jpabook.MemberType.Admin
엔티티 타입 엔티티 타입 표현. 주로 상속과 관련해 사용 TYPE(m) = Member

 

대소문자는 구분하지 않습니다.

 

연산자 우선 순위

1. 경로 탐색 연산 (.)

2. 수학 연산: +, -(단항 연산자), *, /, +, -

3. 비교 연산: =, ., >=, <, <=, <>(다름), [NOT] BETWEEN,  [NOT] LIKE,  [NOT] IN, IS [NOT[ NULL, 

IS  [NOT]  EMPTY  [NOT]  MEMBER [OF].  [NOT]  EXISTS

 

BETWEEN 식 문법

[NOT] BETWEEN A  AND B

비교하는 값이 A 와 B 사이의 값중 하나라면 참입니다. A 값과 B값도 포함합니다.

ex) 

select m from Member m

where m.age between 10 and 20

 

IN 식 문법

[NOT] IN (값 .. )

IN 에서 지정해준 값중 하나라도 해당되면 참입니다. IN 에서 지정해준 값은 서브 쿼리로도 지정할 수 있습니다.

ex)

select m from Member m

where m.username in ("회원1" "회원2"):

 

Like 식 문법

문자표현식 [NOT] LIKE 패턴값 [ESCAPE 이스케이프문자]

문자표현식과 패턴값을 비교합니다.

%(퍼센트) : 아무 값들이 입력되어도 됩니다. 값이 없어도 됩니다.

_(언더라인): 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 합니다.

 

//이름 중간에 원이라는 단어가 들어간 회원
select m from member
where m.username like '%원%'

//이름 시작이 회원이라는 단어가 포함된 회원
where m.username like '회원%'

//이름 끝이 회원이라는 단어가 포함된 회원
where m.username like '%회원'

//이름 시작이 회원이고 그 뒤에 한 글자로만 된 값이 와야됨
where m.username like '회원_'

//이름 끝이 2이고 2 앞에 두 글자로 된 값이 와야 됨
where m.username like '__2'

//이름이 회원%인 회원
where m.username like '회원%'

 

Like 식을 사용한 예제입니다.

 

NULL 비교식 문법

IS [NOT[] NULL

NULL 인지 비교합니다. NULL 은 = 으로 비교하는게 아니고 꼭  IS NULL 을 사용합니다.

 

컬렉션 식

컬렉션 식은 컬렉션에만 사용하는 특별한 기능입니다. 컬렉션 식 이외에 다른 식은 사용할 수 없습니다.

 

빈 컬렉션 식 문법

{컬렉션 값 연관 경로} IS [NOT] EMPTY

컬렉션에 값이 비었으면 참입니다. 

 

//JPQL: 주문이 하나라도 있는 회원 조회
select m from Member m
where m.orders is not empty

//실행된 SQL
select m.* from Member m
where exists {
    select o.id
    from Orders o
    where m.id = o.member_id;
}

 

컬렉션의 멤버 식 문법

{엔티티나 값} [NOT] MEMBER  [OF] {컬렉션 값 연관 경로}

엔티티나 값이 컬렉션에 포함되어 있음 참입니다.

ex)

select t from Team t

where :memberParam member of t.members

파라미터로 넣은 회원 엔티티가 회원 컬렉션에 포함되어 있는 팀을 가져오는 JPQL 입니다.

 

스칼라 식

스칼라는 숫자, 문자, 날짜, case, 엔티티 타입(엔티티의 타입 정보) 같은 가장 기본적인 타입들을 말합니다.

 

수학 식

+, - : 단항 연산자

*, /, +, -: 사칙 연산

 

함수  설명  예제
CONCAT(문자1, 문자2) 문자를 합칩니다. CONCAT('A', 'B') =AB
SUBSTRING(문자, 위치, [길이]) 위치부터 시작해 길이만큼 문자를 구합니다. 길이 값이 없으면 나머지 전체 길이를 의미합니다. SUBSTRING('ABCDEF', 2, 0) BCD
TRIM([[LEADING | TRALING | BOTH]
[트림 문자] FROM 문자)
LEADING: 왼쪽만
TRALING: 오른쪽만 
BOTH: 양쪽 다 트림 문자를 제거합니다.
기본값은 BOTH, 트림 문자의 기본값은
공백(SPACE) 입니다.
TRIM(' ABC ') = 'ABC'
LOWER (문자) 소문자로 변경 LOWER(' ABC ') = 'abc'
UPPER (문자) 대문자로 변경 (' abc ') = 'ABC'
LENGTH (문자) 문자 길이 (' ABC ') = 3
LOCATE (찾을 문자, 원본 문자,
검색 시작 위치]))
검색 위치부터 문자를 검색합니다. 1부터시작, 못 참으면 0 반환 LOCATE('DE', 'ABCDEFG') = 4

 

수학함수

 

함수 설명  예제
ABS(수학식) 절대값을 구합니다. ABS(-10) = 10
SQRT(수학식) 제곱근을 구합니다. SQRT(4) = 2.0
MOD(수학식, 나눌 수) 나머지를 구합니다. MOD(4,3) = 1
SIZE(컬렉션 값 연관 경로식) 컬렉션의 크기를 구합니다 SIZE(t.members)
INDEX(별칭) LIST 타입 컬렉션의 위치값을 구합니다.
단 컬렉션이 @oRDERcOLUMN 을 사용하는 LIST 타입일 때만 사용할 수 있습니다.
t.members m where INDEX(m) > 3

 

날짜함수

날짜함수는 데이터베이스의 현재 시간을 조회합니다.

CURRENT_DATE: 현재 날짜:

CURRENT_TIME: 현재 시간

CURRENT_TIMESTAMP: 현재 날짜 시간

ex)

select CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP from Team t

2022-11-14, 16:03:10, 2022-11-14,16:03:10

 

하이버네이트는 날짜 타입에서 년, 월, 시간, 분, 초 값을 구하는 기능을 지원합니다.

YEAR, MONTH, DAY, HOUR, MINUTE, SECOND

ex) select year(CURRENT_DATE) from Team t

 

CASE 식

특정 조건에 따라 분기할 때 CASE 식을 사용합니다. CASE 식은 4가지 종류가 있습니다.

 

기본CASE

문법 

CASE {WHEN <조건식> THEN <스칼라식>} +_ELSE<스칼라식> END

ex)

select

case when m.age <= 10 then '학생요금'

case when m.age <= 10 then '경로요금'

else '일반요금' end

from Member m

 

심플 CASE

심플 CASE 는 조건식을 사용할 수 없지만, 문법이 단순합니다. 자바의 switch case 문과 비슷합니다.

문법 

CASE {WHEN <스칼라식1> THEN> <스칼라식>} + ELSE<스칼라식> END

ex)

select

case t.name when '팀A' then '인센티브110%' when '팀A' then '인센티브110%' ekse '인센티브105%' end

from Team t

 

COALESCE

스칼라식을 차례대로 조회해서 null 이 아니면 반환합니다.

문법

COALESCE(<스칼라식> {, <스칼라식>}+} )

ex)

select COALESCE(m.username, ''이름 없는 회원') from Member m

회원의 이름이 NULL 이면 '이름 없는 회원' 을 반환합니다.

 

NULLIF

두 값이 같으면 NULL 을 반환하고 다르면 첫 번째 값을 반환합니다. 집합 함수는 NULL 을 포함하지 않으므로

보통 집합 함수와 함께 사용합니다.

문법

NULLIF (<스칼라식>, <스칼라식>)

ex)

select NULLIF( m.username, '관리자' ) from Member m

회원의 이름이 관리자면 NULL 을 반환하고 나머지는 회원의 이름을 반환합니다.

 

다형성 쿼리

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

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Item {

 

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

 

현재 사용하는 예제에서는 Item 의 자식으로 Book, Movie, Album 이 있는데요.

 

List resultList = em.createQuery("select i from Item i").getResultList();

 

위와 같은 쿼리를 실행하면 Item의 자식도 함께 조회됩니다.

 

SELECT * FROM ITEM

 

단일 테이블 전략(@Inheritance(strategy = InheritanceType.SINGLE_TABLE)) 을 사용할 때 실행되는 SQL 입니다.

 

SELECT i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity,
       b.author, b.isbn
       a.artist, a.etc
       m.actor, m.director
From item item
LEFT JOIN Book b on i.ITEM_ID = B.ITEM_ID
LEFT JOIN Album a on i.ITEM_ID = B.ITEM_ID
LEFT JOIN Movie on i.ITEM_ID = B.ITEM_ID

 

 조인 전략(@Inheritance(strategy = InheritanceType.JOINED) 을 사용할 때 실행되는 SQL 입니다.

 

TYPE

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

 

ex)

JPQL

select i from Item i where type(i) IN (Book, Movie)

SQL

SELECT i FROM  Item i WHERE i.DTYPE in ('B', 'M')

Item 중 Book, Movie 를 조회합니다.

 

TREAT

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

JPA 표준은 FROM, WHERE 절에서 사용할 수 있지만, 하이버네이트는 SELECT 절에서도 TREAT 를 사용할 수

있습니다.

 

ex) 

JPQL

select i from Item i where treate (i as Book).author = 'kim'

SQL

select i.* from Item i where i.DTYPE='B' and i.author='kim'

부모 타입인 Item 을 자식 타입인 Book 으로 다루어서 Book 의 author 필드에 접근할 수 있습니다.

 

엔티티 직접 사용

 

기본키 값

 

객체 인스턴스는 참조 값으로 식별하고 테이블 행은 기본키 값으로 식벼랗ㅂ니다. 따라서 JPQ 에서 

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

 

JPQL

select count(m.id) from Member m

select count(m) from Member m

첫 번째 JPQL 은 엔티티의 아이디를 사용했고 두 번쨰 행은 엔티티의 별칭을 넘겨주었습니다.

이렇게 엔티티를 직접 사용하면 JPQL 이 SQL 로 변환될 때 해당 엔티티의 기본키를 사용합니다.

 

SQL

select count(m.id) as cnt from Member m

따라서 위에 두 개의 JPQL 이 변환되는 SQL 은 서로 같습니다.

 

다른 예제를 살펴보겠습ㄴ다.

 

String query = "select m from Member m where m = :member";
List resultList = em.createQuery(query)
                    .setParameter("member", member)
                    .getResultList();

 

WHERE 절에 넣은 파라미터와 같은 엔티티를 찾는 쿼리입니다.

 

select m.* from Member m where m.id = ?

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. JPQL 에서는  FROM 에 지정한 엔티티가 파라미터로 들어온

엔티티와 같아야하는 조건을 주었는데 생성된 SQL 문을 확인하면 엔티티가 아닌 엔티티의 기본키값을 

사용하는 것을 확인할 수 있습니다

 

String query = "select m from Member m where m.id = :memberId";
List resultList = em.createQuery(query)
                    .setParameter("memberId", 1L)
                    .getResultList();

 

위와 같이 엔티티가 아닌 기본키값을 사용해도 생성되는 SQL 문은 같습니다.

 

외래키 값

 

Team team = em.find(Team.class, 1L);

String query = "select m from Member m where m.team = :team";
List resultList = em.createQuery(query)
                    .setParameter("team", team)
                    .getResultList();

 

기본키가 1L 인 팀 엔티티를 가져오고 해당 엔티티를 JPQL 쿼리 파라미터로 사용했습니다. m.team 은 현재

team_id 라는 외래키와 매핑되어 있습니다.

 

select m.*
from Member m
where m.team_id=?(팀 파라미터의 ID 값)

 

JPQL 이 실행되었을 때 생성되는 SQL 문입니다. 

 

String query = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(query)
                    .setParameter("teamId", team)
                    .getResultList();

 

위에 기본키값 예제처럼 엔티티 대신 식별자 값을 직접 사용해도 생성되는 SQL 문은 같습니다.

여기서 m.team.id 를 보면 Member 와 Team 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 

team_id 외래키를 가지고 있으므로 묵시적 조인은 일어나지 않습니다. 만약 m.team.name  을 호출하면

묵시적 조인이 일어납니다. 따라서 m.team 을 사용하든 m.team.id  를 사용하든 생성되는 SQL 은 같습니다.

 

Named 쿼리: 정적 쿼리

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

 

동적 쿼리: em.createQuery("select ..") 처럼 JPQL 을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 합니다.

런타임에 특정조건에 따라 JPQL 을 동적으로 구성할 수 있습니다.

 

정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라고 합니다.

Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리입니다.

 

Named 쿼리는 어플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둡니다. 따라서 오류를 빨리

확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용해서 성능상 이점도 있습니다. Named 쿼리는

변하지 않는 정적 SQL 이 생성되므로 데이터베이스 조회 성능 최적화에도 도움이 됩니다.

Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할

수 있습니다.

 

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

 

Named 쿼리은 위 코드와 같이 작성합니다. 말 그대로 이름을 부여해서 사용하는 방법입니다.

name 속성에는 Named 쿼리의 이름을, query 에는 쿼리문을 작성합니다.

 

@Entity
@Getter @Setter
@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 {

 

하나의 엔티티에 2개 이상의 Named 쿼리를 정의할 때는 @NamedQueries 어노테이션을 사용하면 됩니다.

 

List<Member> resultList =
        em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", "회원1")
                .getResultList();

 

Named 쿼리를 사용할 때는  em.createNamedQuery() 메서드에 Named 쿼리 이름을 작성하면 됩니다.

 

이 포스팅은 자바 ORM 표준 JPA 프로그래밍의 내용을 참고하여 작성하였습니다.

댓글