728x90
반응형
SMALL
반응형
SMALL

2024.10.31 업데이트


JPQL(Java Persistence Query Language) 데이터베이스의 SQL과 유사하나 객체지향 쿼리 언어이며 그렇기에 테이블을 대상으로 쿼리하는 게 아니라 엔티티 객체를 대상으로 쿼리한다는 차이점이 있다. JPQLSQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. 이 말은 데이터베이스마다 다른 방언(MySQL은 LIMIT, Oracle은 ROWNUM 같은)에 상관없이 동작한다는 의미이다.

 

그러나 결국, DB는 SQL만을 받기 때문에 JPQL은 결국에는 SQL로 변환된다. 

JPQL을 잘 이해하고 사용할 줄 알아야 기본적인 쿼리문을 사용하는데 문제가 없고 복잡한 쿼리를 처리해주는 QueryDsl도 편하게 사용할 수 있다. 그래서 JPQL을 잘 사용할 줄 알아야 한다.

 

JPA의 다양한 쿼리 방법

JPQL을 공부하기 앞서, JPA는 다양한 쿼리 방법을 지원한다.

  • JPQL
  • JPA Criteria
  • QueryDsl
  • native SQL
  • JDBC API 직접 사용

지금까지는 이런식으로 간단하게 작성했다. 

AddressEntity ae = entityManager.find(AddressEntity.class, 1L);

근데, 개발을 하다보면 이런 간단한 쿼리도 있지만 복잡한 쿼리도 필요할 때가 있기 마련이다. 예를 들어, "유저의 이름이 중간에 'hello'가 들어가는 유저들" 이런 쿼리를 원한다면? 사실 이것도 복잡한 쿼리도 아니다. 근데 여기서 말하고자 하는건 이렇게 조건이 들어가는 경우가 비일비재 하다는 것이다. 

 

그러면 이제, 다음과 같이 쿼리를 작성할 수 있다.

이게 바로 JPQL이다. 그리고 이게 지금 IDE가 잘 도와주고 있기 때문에 이렇게 보이는거지 사실 저건 다 문자열이다. 그래서 IDE의 도움을 받지 못하는 경우에는 문자열에 잘못된 부분이 있어도 컴파일러는 체크할 수 없다.

 

그리고 이 문자열로 쿼리를 만들어 낼 때 가장 까다로운 것은 동적 쿼리를 만들기가 정말 어렵다는 것이다. 그래서 JPA Criteria라는게 있는데 결론부터 말하면 이건 사용하지 마라. 근데 일단 스펙에 있기 때문에 뭔지는 알아야 하니까 말하자면 아래와 같이 사용하면 된다.

 

JPA Criteria 사용 예시

// Criteria 사용 준비
CriteriaBuilder cb = entityManager.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 = entityManager.createQuery(cq).getResultList();
  • 문자가 아닌 자바 코드로 JPQL을 작성할 수 있다. 
  • JPQL의 빌더 역할을 하는게 JPA Criteria이다.
  • JPA에서 공식적으로 지원하는 기능이다. 
  • 그러나, 너무 복잡하고 실용성이 없다. 실용성이 없다는 말은 저 코드를 보고 이게 어떤 쿼리인지 인지하기가 너무 어렵다. 지금이야 아주 간단한 쿼리니까 읽다보면 이해할 수 있겠지만, 조금만 복잡해져도 알아보기가 너무 어렵다.

그래서 결론을 말하면 그냥 딱 이렇게 생각하면 된다. 

JPQL + QueryDsl 두 개를 같이 사용하면 거의 95% 경우의 쿼리를 다 수행할 수 있다. 만약, 정말 정말 복잡한 쿼리가 있어서 저 둘로 해결하지 못한다면, 그럴때 nativeQuery를 사용하면 된다. 실제로 SQL을 작성해서 그 SQL을 수행할 수 있게 해주는 녀석이다. 아래가 그 예시 코드이다. 

String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> result = entityManager.createNativeQuery(sql, Member.class).getResultList();

 

JPQL 문법

JPQL 문법이라고 SQL과 다를 것 없다. 다만 지켜야 할 규칙이 있다.

select m from Member as m where m.username = "member"
  • 엔티티와 속성은 대소문자 구분을 한다 (Member, username)
  • JPQL 키워드는 대소문자 구분을 하지 않는다 (SELECT, FROM, where)
  • 엔티티 이름을 사용한다 (테이블 이름이 아니다)
  • 별칭은 필수(m), as는 생략 가능

 

TypedQueryQuery 타입이 있는데 TypedQuery는 반환되는 데이터의 타입이 명확할 때를 말하고 Query는 명확하지 않을 때이다.

크게 중요한 내용은 아닌데 알아둬야 하니까.

TypedQuery<Member> members = em.createQuery("select m from Member m", Member.class);

이렇게 createQuery()에 두번째 인자로 타입을 지정할 수 있는데 아무 타입이나 지정할 수 있는건 아니고 거의 대부분은 엔티티를 지정한다. 이렇게 타입을 명확히 명시를 한 상태에서의 반환타입이 TypedQuery가 된다.

 

그러나 아래와 같이 idLong, usernameString인 데이터를 받아올 때는 타입을 명시할 수 없으므로 반환되는 데이터 타입이 Query가 된다. 이런 차이가 있다.

Query members = em.createQuery("select m.id, m.username from Member m");

 

 

그러나, 저런식으로 사용하지 않고 체인으로 사용하는게 일반적이고 그 예시는 다음과 같다.

List<Member> members = em.createQuery("select m from Member m", Member.class)
                                    .getResultList();
Member singleMember = em.createQuery("select m from Member m where m.id=1", Member.class)
                                    .getSingleResult();

이렇게 한번에 getResultList() 또는 getSingleResult()로 데이터를 받아온다. 메소드 명만 봐도 알겠지만 getResultList()는 복수의 데이터를 가져오는 방식이고 getSingleResult()는 단일값을 가져오는 방식이다.

 

그러나 주의할 점이 있다.

getResultList()는 데이터가 없는 경우 빈 리스트를 반환하기 때문에 큰 문제가 되지 않는다. 그러나 getSingleResult()는 무조건 딱 더도 덜도 말고 딱! 하나만 있어야 한다. 만약, getSingleResult()를 사용하는데, 데이터가 없는 경우 NoResultException이 발생하고 데이터가 둘 이상인 경우 NonUniqueResultException이 발생한다. 이것을 주의해야한다. (Spring Data JPA는 결과가 없으면 이 에러를 처리해서 null로 반환해주긴 함)

 

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

파라미터를 던져줄 상황이 굉장히 많이 발생할텐데 사용방법은 또 간단하다.

이름 기준

List<Member> members = em
                    .createQuery("select m from Member m where m.username = :username", Member.class)
                    .setParameter("username", "Member1")
                    .getResultList();

username이라는 파라미터를 던져줄 때 setParameter()를 호출한다. 그리고 받는 쪽은 where m.username = :username 여기가 파라미터를 받는 부분이다.

 

위치 기준

위치기준은 사용하지 말자. 그러나 뭐가 위치 기준인지는 알아야 하니 일단 예시는 다음과 같다.

List<Member> members = em
                    .createQuery("select m from Member m where m.username=?1", Member.class)
                    .setParameter("1", "Member1")
                    .getResultList();

"?1" 이 부분이 파라미터를 받는 부분, 주는 부분은 setParameter("1", "value") 이 부분이다. 보면 알겠지만 코드 길어지거나 안보다가 보면 "1"이런 값들이 가시성이 확 떨어진다. 또한 중간에 추가하게 되면 순서가 달라지기 때문에 다 밀리게 되고 아주 불편하다.

 

 

프로젝션

프로젝션이란, SELECT절에 조회할 대상을 지정하는 것을 말한다.

 

프로젝션 대상

  • 엔티티
  • 임베디드 타입
  • 스칼라 타입 (숫자, 문자 등 기본 데이터 타입)

엔티티 프로젝션

// 엔티티 프로젝션
SELECT m FROM Member m
  • 멤버 전체를 가져오는 쿼리이므로 엔티티 프로젝션을 말한다.
  • 그리고 이렇게 엔티티 프로젝션을 통해 엔티티를 가져오면, 엔티티 매니저에 의해서 영속성 컨텍스트가 관리하는 대상이 된다. 즉, 1차 캐시에도 들어가고 변경감지도 수행된다는 의미다.
// 엔티티 프로젝션
SELECT m.team FROM Member m
  • 멤버의 팀을 가져오는 쿼리로 팀이 엔티티이기 때문에 엔티티 프로젝션이라 한다.
  • 근데 이 코드를 수행하면 어떤 쿼리가 나갈까? 멤버가 속한 팀을 가져오고 있는데 그럼? 맞다. 조인을 하게 된다.
List<Team> result = entityManager
                    .createQuery("SELECT m.team FROM Member m", Team.class)
                    .getResultList();

실행된 쿼리

Hibernate: 
    /* SELECT
        m.team 
    FROM
        Member m */ select
            team1_.id as id1_3_,
            team1_.name as name2_3_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

이렇듯, 조인을 한다. 물론 당연한 소리다. 당연히 조인을 해야지. 근데 JPQL만 보면 조인을 한다고 명시적으로 적어주지 않았기 때문에 JPQL과 JPA에 대해 잘 이해하고 있는 사람이야 알겠지만 그렇지 않은 사람은 모를수도 있다. 즉, 명시적으로 JPQL을 작성해 주는게 훨씬 더 좋은 방향이라고 본다. 아래처럼 말이다.

List<Team> result = entityManager
                    .createQuery("SELECT t FROM Member m JOIN m.team t", Team.class)
                    .getResultList();

 

임베디드 타입 프로젝션

// 임베디드 타입 프로젝션
SELECT m.address FROM Member m
  • 멤버의 값 타입으로 임베디드한 Address를 가져오는 쿼리이므로 임베디드 타입 프로젝션이라 한다.

스칼라 타입 프로젝션

// 스칼라 타입 프로젝션
SELECT m.username, m.age FROM Member m
  • 멤버가 가지고 있는 문자, 숫자 등 기본 데이터 타입을 가져오므로 스칼라 타입 프로젝션이라 한다.
// DISTINCT로 중복을 제거
SELECT DISTINCT m.username, m.age FROM Member m
  • 참고로 중복제거도 역시 가능하다. 그래서 풀 코드로 작성하면 이렇게 쓰면 된다.
Query members = em.createQuery("select distinct m.id, m.username from Member m");
근데 여기서 한가지 의문이 발생한다. "이 경우에는 타입을 지정못하는데 가져올 땐 어떻게 가져와야 하지?"

 

방법은 여러가지가 있다.

첫번째는, 위에서 작성한 코드처럼 Query 타입으로 가져오는 방법이다.

Query query = em.createQuery("select m.username, m.id From Member m");
List<Object[]> result = query.getResultList();

Object[] data = result.get(0);
System.out.println("username: " + data[0]);
System.out.println("id: " + data[1]);

//Output:
username: Member1
id: 1
  • 모든 데이터 타입의 상위 타입은 Object 이므로, Object[]로 받는다. []로 받는 이유는 m.username, m.id[]에 0번, 1번으로 들어가기 때문이다. 그래서 0과 1을 찍어보면 각각 usernameid를 출력한다.
  • 그러나 좀 불편한 부분이 있어보인다. 그래서 좀 더 좋은 방법은 DTO 객체로 받아오는 것이다.

두번째는, DTO 객체로 받아오는 방법이다.

SelectMemberDTO

package org.example.entity.jpql;

public class SelectMemberDTO {
    
    private Long id;
    private String username;

    public SelectMemberDTO(Long id, String username) {
        this.id = id;
        this.username = username;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}
  • 이렇게 받아줄 DTO 클래스를 만들고 이 객체로 데이터를 받아오는 방법이다. 아래 코드를 보자.
List<SelectMemberDTO> result = em
            .createQuery(
                    "select new org.example.entity.jpql.SelectMemberDTO(m.id, m.username) From Member m",
                    SelectMemberDTO.class)
            .getResultList();

    for (SelectMemberDTO selectMemberDTO : result) {
        System.out.println("selectMemberDTO id= " + selectMemberDTO.getId());
        System.out.println("selectMemberDTO username= " + selectMemberDTO.getUsername());
    }
  • 보면 new org.example.entity.jpql.SelectMemberDTO(m.id, m.username) 이라고 작성되어 있다. 여기가 문자열이기 때문에 패키지명까지 쭉 작성해줘야하는 번거로움이 있긴하지만 위에 Object로 받는 방법보다는 훨씬 가시적이다.
  • 그리고 이후에 QueryDsl을 배우고 사용하면 저렇게 패키지도 쭉 안 적어도 된다. 이후에 같이 배워보자!

 

페이징

JPA에서 페이징하는 방법이 너무 간단하게 잘 되어있다. setFirstResult(), setMaxResults()를 사용하면 끝이다.

List<Member> resultList = em
                .createQuery("select m from Member m order by m.id desc", Member.class)
                .setFirstResult(0)
                .setMaxResults(10)
                .getResultList();
  • setFirstResult(0), setMaxResults(10)을 주면 0부터 10개까지를 가져온다. 이게 끝이다. 결과는 다음과 같다.
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_0_,
        member0_.TEAM_ID as team_id3_0_,
        member0_.username as username2_0_ 
    from
        Member member0_ 
    order by
        member0_.MEMBER_ID desc limit ?
member = Member99
member = Member98
member = Member97
member = Member96
member = Member95
member = Member94
member = Member93
member = Member92
member = Member91
member = Member90

 

조인

조인의 경우 크게 세 가지가 있다.

  • 내부 조인: [INNER] JOIN
  • 외부 조인: LEFT [OUTER] JOIN
  • 세타 조인

 

내부 조인 ([INNER] JOIN)

내부조인은 아래 JPQL을 보면 멤버를 기준(`FROM Member m`)으로 조인하고 있는데 이때, 팀의 값이 있는 데이터들만 뽑아낸다. 그러니까 팀의 값이 없는 멤버 레코드는 아예 결과로 나오지가 않는게 내부 조인이다.

SELECT m FROM Member m [INNER] JOIN m.team t

 

외부 조인 (LEFT [OUTER] JOIN)

외부조인은 아래 JPQL을 보면 멤버를 기준(`FROM Member m`)으로 조인하고 있는데 이때, 팀의 값이 없어도 결과로 레코드가 나온다. 대신 팀의 값이 null로 표현된다. 참고로, LEFT [OUTER] JOIN이다. OUTER는 생략가능하고 보통은 LEFT JOIN이라고 많이 말한다. OUTER JOIN 이런건 없다!

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

 

세타 조인

아무런 연관성이 없는 데이터들도 뽑아낸다. 보면 `m.team t` 가 아니라, `FROM Member m, Team t`다. 즉, 어떠한 연관도 없는 두 엔티티끼리도 조인이 가능한데 이것을 세타 조인이라고 한다.

SELECT count(m) FROM Member m, Team t WHERE m.username = t.name

 

 

그래서 조인을 사용하는 코드를 한번 작성해보면 다음과 같다.

// 내부 조인
String query = "select m from Member m join m.team t where t.name =:teamName";
List<Member> resultList = em
        .createQuery(query, Member.class)
        .setParameter("teamName", "Team1")
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();
        
// 외부 조인 
String query = "select m from Member m left join m.team t";
List<Member> resultList = em
        .createQuery(query, Member.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();
        
        
// 세타 조인
String query = "SELECT m FROM Member m, Team t WHERE m.username = t.name";
List<Member> resultList = em
        .createQuery(query, Member.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();

 

조인 대상 필터링 - ON

조인 하기 전 필터링을 해서 데이터 양을 좀 줄일 수 있고 JPQL도 역시 유사한 키워드로 지원한다.

List<Member> result = entityManager
                    .createQuery("SELECT m FROM Member m JOIN m.team t ON t.name = :teamName", Member.class)
                    .setParameter("teamName", "teamA")
                    .getResultList();
  • 저렇게 ON절을 사용해서 팀의 이름을 특정하여 필터링을 할 수가 있다.
  • 실제 나가는 쿼리는 어떻게 나갈까? 바로 아래와 같이 나간다.
SELECT m.*, t.* 
FROM Member m
INNER JOIN Team t ON m.TEAM_ID = t.id AND (t.name = 'teamA')

 

 

연관 관계가 없는 엔티티의 외부 조인도 필터링을 걸 수 있다.

List<Member> result = entityManager
                    .createQuery("SELECT m FROM Member m JOIN Team t ON t.name = m.name", Member.class)
                    .getResultList();
  • 보면 m.team을 사용한게 아니라 JOIN Team t 이렇게 작성했다. 이렇게 연관관계가 없는 상태에서도 조인을 하는게 가능하고 이걸 세타 조인이라고 하며, ON절을 사용해서 필터링할 수 있다.
  • 실제 나가는 쿼리는 어떻게 나갈까? 바로 아래와 같이 나간다.
SELECT m.*, t.* 
FROM Member m
INNER JOIN Team t ON m.name = t.name

 

서브 쿼리

SQL문에서 사용하던 서브 쿼리랑 같은 것을 말하고 JPQL도 역시 이를 지원한다. 

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

이렇게 서브 쿼리를 사용할 때 위에서처럼 mm2로 서브 쿼리랑 본 쿼리의 멤버를 분리해야 일반적으로 SQL 성능이 좋다. 

 

 

서브 쿼리 지원 함수로 다음과 같은 것들이 있다.

  • [NOT] EXISTS (subquery): 서브 쿼리에 결과가 존재하면 참
# 팀A 소속인 회원
SELECT m FROM Member m WHERE EXISTS (SELECT t FROM m.team t WHERE t.name = '팀A')

 

  • {ALL | ANY | SOME} (subquery)
    • ALL: 모두 만족하면 참
    • ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
# 전체 상품 각각의 재고보다 주문량이 많은 주문들
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): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
# 어떤 팀이든 팀에 속한 멤버들
SELECT m FROM Member m WHERE m.team IN (SELECT t FROM Team t)

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT절도 가능 (하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능 (조인으로 풀 수 있으면 풀어서 해결)
FROM절의 서브 쿼리도 이제 가능하다! Hibernate 6.1.0.Final 버전부터 가능하다! (아래 참조링크 확인)
 

Hot features of Hibernate ORM 6.1 - In Relation To

Hibernate ORM version 6.1.0.Final was just announced a few days ago, but the announcement didn’t go into a too much detail. Read on if you want to know more about some of the hot new features this shiny new release comes with.

in.relation.to

 

 

JPQL 타입 표현

  • 문자  'HELLO', 'She"s'
  • 숫자  10L(Long), 10D(Double), 10F(Float)
  • Boolean → TRUE(true), FALSE(false)
String query = "SELECT m.username, 'HELLO', TRUE, 10L FROM Member m";

List<Object[]> resultList = em
        .createQuery(query)
        .setFirstResult(0)
        .setMaxResults(1)
        .getResultList();

for (Object[] objects : resultList) {
    System.out.println("objects = " + objects[0]);
    System.out.println("objects = " + objects[1]);
    System.out.println("objects = " + objects[2]);
    System.out.println("objects = " + objects[3]);
}

//Output:
objects = Member0
objects = HELLO
objects = true
objects = 10

 

  • ENUM jpa.MemberType.ADMIN (패키지명을 다 포함)
String query = "SELECT m.username, 'HELLO', TRUE, 10L " +
                    "FROM Member m " +
                    "WHERE m.memberType = org.example.entity.jpql.MemberType.ADMIN";

근데 보통은 파라미터를 바인딩해서 사용할테니까 저렇게까지 불편할 정도는 아니다. 파라미터 바인딩을 하면 다음과 같이 조금 더 편리해지고 보기도 좋아진다.

String query = "SELECT m.username, 'HELLO', TRUE, 10L, m.memberType " +
                    "FROM Member m " +
                    "WHERE m.memberType = :memberType";

List<Object[]> resultList = em
        .createQuery(query)
        .setParameter("memberType", MemberType.ADMIN)
        .setFirstResult(0)
        .setMaxResults(2)
        .getResultList();

 

 

  • 엔티티 타입  TYPE(m) = Member (상속 관계에서 사용)

이렇게 ITEM, BOOK이 상속관계일 때 ITEM으로부터 가져온 레코드 중 DTYPEBook인 녀석들만 가져오는 쿼리도 할 수 있다는 것을 보여준다.

String query = "SELECT i " +
                    "FROM Item i " +
                    "WHERE TYPE(i) = Book";

List<Item> resultList = em
        .createQuery(query, Item.class);
        .getResultList();

 

  • JPQL 기타 → EXISTS, IN, AND, OR, NOT, =, >, >=, <, <=, BETWEEN, LIKE, IS NULL 모두 다 가능하다.
String query = "SELECT m " +
                    "FROM Member m " +
                    "WHERE m.age BETWEEN 0 AND 10";

List<Member> resultList = em
        .createQuery(query, Member.class);
        .getResultList();

 

조건식

CASE 식

//기본 CASE식
SELECT
	case when m.age <= 10 then '학생요금'
    	 when m.age >= 60 then '경로요금'
         else '일반요금'
     end
FROM Member m


//단순 CASE식
SELECT
	case t.name
        when '팀A' then '인센티브110%'
     	when '팀B' then '인센티브120%'
     	else '인센티브105%'
     end
FROM Team t

이렇게 생긴 문장 많이 봤을거다. SQL하면서. JPQL에서도 동일하게 지원해준다.

 

String query = "SELECT " +
                    "       case when m.age <= 10 then '학생요금'" +
                    "       when m.age >= 60 then '경로요금'" +
                    "       else '일반요금'" +
                    "       end " +
                    "FROM Member m";
            
List<String> resultList = em.createQuery(query, String.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();

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

COALESCE, NULLIF

COALESCE는 조회 결과가 NULL이 아니면 반환하는 조건식이다. 

SELECT COALESCE(m.username, '이름 없는 회원') FROM Member m

그러니까 찾은 유저의 m.usernameNULL인 경우 '이름 없는 회원'으로 대체한다는 의미이다.

 

NULLIF는 두 값이 같으면 NULL반환, 다르면 첫번째 값 반환

SELECT NULLIF(m.username, '관리자') FROM Member m

찾은 유저 각각의 유저네임이 관리자인 유저는 NULL로 유저네임을 반환하고 그게 아닌경우, 유저네임 그대로를 반환한다는 의미이다. 

 

 

JPQL 함수 

JPQL이 제공하는 표준 함수

JPQL이 표준으로 제공하는 함수들 (데이터베이스에 종속적이지 않아서 어떤 데이터베이스이든 상관없이 사용 가능하다)

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX(JPA 용도)
// CONCAT
String query = "SELECT CONCAT('A', 'B') FROM Member m";

// SUBSTRING
String query = "SELECT SUBSTRING(m.username, 2, 3) FROM Member m";

....

//SIZE (컬렉션의 크기를 돌려줌)
String query = "SELECT SIZE(t.members) FROM Team t";
  • 기본으로 제공하는 함수가 있는 반면 DB에 종속적인 방언적 함수가 각 DB마다 또 있을 수 있는데 그런 함수가 JPQL이 지원을 즉각적으로 안하는 경우 사용자가 직접 함수를 등록해서 사용할 수 있다.

 

사용자 정의 함수

사용자 정의 함수란, JPQL이 공식적으로는 지원하지 않지만 해당 데이터베이스에서 지원하는 함수를 사용하고 싶을 때 그 함수를 등록해서 사용하는 방법을 말한다. 예를 들어, H2 데이터베이스는 `group_concat`이라는 함수가 있다. 이 함수는 조회한 레코드들로 사용자로부터 받은 특정 컬럼을 한 줄로 쭉 이어 붙이는 함수인데 이건 H2 데이터베이스에서 지원하는 함수다. 그리고 내가 만약 이 함수를 사용하고 싶다면?

 

아래와 같이 Dialect를 커스텀할 클래스하나를 만들고 H2 데이터베이스의 H2Dialect를 상속받는다. 그리고 내가 원하는 함수를 생성자 안에서 등록한다. 

package cwchoiit.jpql;

import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;

public class MyH2Dialect extends H2Dialect {

    public MyH2Dialect() {
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

 

그리고 JPA 설정 파일에서 Dialect를 이 파일로 변경만 해주면 끝. 아래는 XML로 설정했기 때문에 XML 파일 형식인거고 요즘은 거의 YAML, properties 파일에서 설정할 것이다.

...
<property name="hibernate.dialect" value="cwchoiit.jpql.MyH2Dialect"/>
...

 

위와 같이 설정을 한 다음 사용할 수 있다. 사용하는 코드를 아래에서 확인해보자.

Main

package cwchoiit;

import cwchoiit.jpql.Member;
import cwchoiit.jpql.Team;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hellojpa");
        EntityManager entityManager = entityManagerFactory.createEntityManager();

        entityManager.getTransaction().begin();
        try {

            Member member = new Member();
            member.setName("member");
            entityManager.persist(member);

            Member member2 = new Member();
            member2.setName("member2");
            entityManager.persist(member2);

            entityManager.flush();
            entityManager.clear();

            List<String> resultList = entityManager
                    .createQuery("SELECT FUNCTION('group_concat', m.name) FROM Member m", String.class)
                    .getResultList();

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

            entityManager.getTransaction().commit();
        } catch (Exception e) {
            entityManager.getTransaction().rollback();
        } finally {
            entityManager.close();
        }
        entityManagerFactory.close();
    }
}

실행 결과

Hibernate: 
    /* SELECT
        FUNCTION('group_concat',
        m.name) 
    FROM
        Member m */ select
            group_concat(member0_.name) as col_0_0_ 
        from
            Member member0_
s = member,member2

 

 

경로 표현식

경로 표현식은 크게 세가지로 분류할 수 있다.

  • 상태 필드: 단순히 값을 저장하기 위한 필드 (예: m.username
  • 단일 값 연관 필드: @ManyToOne, @OneToOne 대상이 엔티티 (예: m.team)
  • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany. 대상이 컬렉션 (예: m.orders)

 

  • 상태 필드: 경로 탐색의 끝, 이후 탐색 불가능
// m.username이 상태 필드 경로 표현식
select m.username from Member m;

 

  • 단일 값 연관 경로: 묵시적 내부 조인 발생, 이후 탐색 가능 
// m.team이 단일 값 연관 경로 표현식
select m.team from Member m;
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 이후 탐색 불가능
// t.members가 컬렉션 값 연관 경로 표현식
select t.members from Team t;

 

여기서 솔직히 어려운 내용은 하나도 없다. 객체로 생각해보면 당연히 상태 필드는 그 녀석이 더 담고 있는 데이터가 없기 때문에 이후 탐색이 불가능한거고 특정 엔티티가 타입인 경우 그 녀석이 가지고 있는 다른 데이터가 있기 때문에 그 이후에 탐색이 가능한것이다. 컬렉션은 컬렉션의 하나하나의 데이터를 뽑아오는 게 아니라 컬렉션 자체를 반환하기 때문에 그 이후에 탐색이 불가능한 것이다.

 

이게 중요해서가 아니라 묵시적 내부 조인이 발생한다는 것이 중요하다.

// m.team이 단일 값 연관 경로 표현식
select m.team from Member m;

위 코드에서 멤버의 팀을 가져오기 위해서는 DB입장에서는 조인을 할 수 밖에 없다. 그래서 내부조인이 일어난다. 하지만 우리가 직접 명시하지 않았기 때문에 묵시적으로 조인이 발생한다. 코드 수행 결과를 한번 보면 다음과 같다.

String query = "SELECT m.team FROM Member m";

List<Team> resultList = em.createQuery(query, Team.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();

for (Team team1 : resultList) {
    System.out.println("team1 = " + team1);
}

실행 결과

select
        team1_.TEAM_ID as team_id1_1_,
        team1_.name as name2_1_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID limit ?
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e
team1 = org.example.entity.jpql.Team@468dda3e

보다시피 내부 조인이 일어난다. 당연하다. 근데 작성한 코드에서는 명시적으로 조인을 사용하지 않았기에 조인을 사용할 것이라고 예측하기는 쉽지는 않다. JPA를 잘 이해하고 잘 하는 사람이야 바로 보고 알수도 있겠다만. 

 

또 컬렉션에서도 경로 탐색이 더이상 불가능한데 다음 코드를 보자.

// t.members가 컬렉션 값 연관 경로 표현식
select t.members from Team t;

이렇게 생긴 쿼리를 실제로 수행하면 당연히 묵시적 내부 조인이 발생한다. 팀에 해당하는 멤버를 가져와야 하기 때문에.

그리고 여기서는 더 이상 탐색이 불가하다. 근데 탐색하고 싶을 수 있다. 멤버 하나하나의 이름이나 뭐 다른 데이터를 알고싶을 수 있잖아.

그럴 때는 FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.

String query = "SELECT m.username FROM Team t join t.members m";

이렇게 컬렉션은 위 방법으로 이후 탐색을 진행할 수 있다. 그러나, 가장 중요한 내용은 다음과 같다.

묵시적 조인이 발생하는 경우를 모두 차단해라.

 

묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵고 조인은 SQL 튜닝에 중요 포인트다. 그렇기 때문에 묵시적 조인은 가급적(아예) 사용하지 말자. 

 

728x90
반응형
LIST

+ Recent posts