[Spring Data JPA] JPQL 사용 방법(@Query & nativeQuery & DTO Mapping & function)

YouMakeMeSmile·2022년 5월 10일
8

JPA(Java Persistence API)를 사용하여 서비스를 구현하다 보면 JPA의 Query Methods만으로는 조회가 불가능한 경우가 존재한다.

이러한 경우 JPQL(Java Persistence Query Language)를 이용하여 SQL과 비슷한 형태의 쿼리를 작성하여 조회를 할 수 있다.

JPQL를 작성하기 위한 방법에는 여러가지 방법이 존재하나 이번 글에서는 @Query AnnotationEntityManager.createQuery등을 사용하여 JPQL를 작성하는 방법에 대해서 작성하려고 한다.

@Query

@Query AnnotationEntityJpaRepository를 상속받는 인터페이스에 정의하게 된다.
기본적인 작성방법은 from 구문에 Entity의 객체를 선언하여 해당 객체의 속성명을 통해서 조건과 파라미터를 작성하게된다.
주의할점은 각 문장에 끝에 띄어쓰기를 추가하도록 한다. 각 문장이 문자열로 이어져있기 때문에 문자의 처음 또는 끝에 띄어쓰기가 없는 경우 한 문장으로 인식되어 정상적인 문장이 아니게된다.

Entity

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "user")
@AllArgsConstructor
public class User {
    @Id
    private String id;
    private String name;
    private String phone;
    private String registerInfo;
    private String deptId;
}

JpaRepository

public interface UserRepository extends JpaRepository<User, String> {

    @Query(value = "select user " +
            "from User user " +
            "where user.name = :name")
    List<User> findByName(@Param("name") String name);

DTO Mapping

JPQL를 사용게되는 이유는 여러가지가 존재하지만 가장 큰 이유는 functionjoin의 경우일 것이다. 이러한 경우 정의한 Entity의 속성외에 속성이 추가 될것이며 이를 위해 DTO 반환이 필요하다.
@Query Annotation를 사용하여 DTO 반환을 하기위해서는 select 구분에서 생성자를 통해서 객체를 반환하여야 한다.

DTO

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private String id;
    private String name;
    private String phone;
    private String deptId;
    private String deptName;
}

return DTO & join

public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select " +
            "new io.velog.youmakemesmile.jpql.UserDto(user.id, user.name, user.phone, user.deptId, dept.name) " +
            "from User user " +
            "left outer join Dept dept on user.deptId = dept.id ")
    List<UserDto> findUserDept();
}

SQL Function

위에서 이야기한것과 같이 JPQL를 사용하게되는 이유중 하나는 SQL Function이다. JPQL에서는 기본적으로 select 구문max, min, count, sum, avg를 제공하며 기본 function으로는 COALESCE, LOWER, UPPER등을 지원하며 자세한 Function은 다음 문서를 참고하면 된다.

public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select max(user.id) " +
            "from User user " +
            "where user.deptId is not null")
    String findMaxUserId();
}

이러한 JPQL에서 기본적으로 지원하는 ANSI Query Function만으로는 비지니스의 조회를 해결하기는 한계가 존재한다. MSA에서는 데이터 저장 방법이 각 서비스에 맞게 변화될수 있게 설계되어야 한다고 하지만 현실적으로는 성능과 비용을 생각할때 DataBase에서 제공하는 function을 사용하지 않을 수 없다.

DataBase Function를 사용하는 방식은 JPQL에서 function()을 활용하여 hibernate에 등록된 각 DataBaseDialect에 정의된 function을 사용하는 방식이다.

public interface UserRepository extends JpaRepository<User, String> {

    @Query(value = "select function('date_format', :date, '%Y/%m/%d') " +
            "from User user ")
    String findNow(@Param("date") LocalDateTime date);
}

하지만 hinbernate에서 기본적으로 등록되는 function에서도 누락되는 function이 존재한다. 이러한 경우 이전에는 Dialect를 상속받아 구현하는 방식을 사용하였으나 현재에는 MetadataBuilderContributor의 구현체를 구현하는 방식을 제공하고있다.

public class MyMetadataBuilderContributor implements MetadataBuilderContributor {
    @Override
    public void contribute(MetadataBuilder metadataBuilder) {
        metadataBuilder.applySqlFunction("JSON_EXTRACT", new StandardSQLFunction("JSON_EXTRACT", StringType.INSTANCE))
                .applySqlFunction("JSON_UNQUOTE", new StandardSQLFunction("JSON_UNQUOTE", StringType.INSTANCE))
                .applySqlFunction("STR_TO_DATE", new StandardSQLFunction("STR_TO_DATE", LocalDateType.INSTANCE))
                .applySqlFunction("MATCH_AGAINST", new SQLFunctionTemplate(DoubleType.INSTANCE, "MATCH (?1) AGAINST (?2 IN BOOLEAN MODE)"));
    }
}

applySqlFunction의 첫번째 파라미터는 JPQL에서 function("함수명") 함수명에 해당하는 등록명이다.

StandardSQLFunction은 기본적인 함수를 등록하기위한 Class로 생성자의 첫번째 파마리터는 실제 DataBase Function명이며 두번째 파라미터는 function의 리턴 타입이다. StandardSQLFunction의 경우 파라미터는 함수에 순서에 맞게 JPQL function('등록 함수명', 파라미터1, 파리마터2 ...) 정의하여 사용하면 된다.

SQLFunctionTemplate은 문법이 존재하는 function를 등록할때 사용가능하며 첫번째 파라미터가 function의 리턴타입이며 두번째 파라미터가 function이다. ?1, ?2와 같이 명시하여 JPQL function()에서 전달되는 파라미터의 순서대로 파싱되어 SQL이 생성된다.

public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select user " +
            "from User user " +
            "where function('JSON_UNQUOTE', function('JSON_EXTRACT',user.registerInfo,'$.id')) = 'admin' ")
    List<User> findAllByRegisterAdmin();
}
public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select user " +
            "from User user " +
            "where function('MATCH_AGAINST', user.name, :name) >0 ")
    List<User> findAllByName(@Param("name") String name);
}

EntityManager 사용

@Query를 이용한 정의 방식이외에 java에서 EntityManager를 활용하여 JPQL를 작성할수도 있다.
EntityManager.createQuery() 메소드에 JPQL 문자열과 리턴 타입을 전달하면된다. @Query에서와 동일하게 DTO를 리턴하는 경우 해당 DTO를 생성하고 생성자형태로 JPQL문장을 작성하면 된다.

@Repository
public class sample{
    @PersistenceContext
    private EntityManager entityManager;
    
    public void test() {

        List<User> resultList = entityManager
                .createQuery("select user from User user where function('MATCH_AGAINST', user.name, :name) > 0 ", User.class)
                .setParameter("name", "le")
                .getResultList();

        List<UserDto> resultList2 = entityManager
                .createQuery("select " +
                        "new io.velog.youmakemesmile.jpql.temp.UserDto(user.id, user.name, user.phone, user.deptId, dept.name) " +
                        "from User user " +
                        "left outer join Dept dept on user.deptId = dept.id ", UserDto.class)
                .getResultList();
    }
}

NativeQuery

NatvieQueryJPQL이 아닌 SQL를 직접 정의하여 사용하는 방식이다. 위에서 이야기 한것과 같이 functionjoin를 하는 경우 JPQL를 사용할 수도 있지만 SQL를 직접 정의할수있는 NativeQuery를 사용할 수 있다.

@Query

@Query를 이용하여 NativeQuery를 작성하는 방법은 @Query 속성중 nativeQuery의 값을 true로 설정하며 value에는 SQL문을 그대로 작성하면 된다.
DTO 맵핑의 경우 JPQL를 사용할때와는 다르게 DTO Class가 아닌 Getter 만 존재하는 interface를 정의하며 네이밍은 SQL의 리턴되는 칼럼명과 일치해야 한다. 다음은 예제이기 때문에 Repositoryinterface를 정의하였으며 구조에 맞는 위치에 정의해도 상관없다.

public interface UserRepository extends JpaRepository<User, String> {
    @Query(value = "select user.id as id, user.name as name, user.phone as phone, json_unquote(json_extract(user.register_info,'$.id')) as registerInfo , str_to_date(json_unquote(json_extract(user.register_info, '$.date')), '%Y-%m-%d') as registerDate, user.dept_id as deptId, dept.name as deptName " +
            "from user user " +
            "left outer join dept dept on user.dept_id = dept.id " +
            "where match (user.name) against (:name in boolean mode) > 0", nativeQuery = true)
    List<UserNativeVo> findTest4(@Param("name") String name);


    interface UserNativeVo {
        String getId();
        String getName();
        String getPhone();
        String getRegisterInfo();
        String getRegisterDate();
        String getDeptId();
        String getDeptName();
    }

}

EntityManager

EntityManager를 이용하여 NativeQuery를 작성하는 방법은 createNativeQuery()를 통해서 SQL 문장을 작성하며 EntityManager를 사용하는 경우 HibernateNativeQuery.class를 이용하여 setResultTransformer를 통해 DTO class를 매핑하여 결과를 리턴받는 방법이 있다. 하지만 setResultTransformer 메소드가 @Deprecated로 예정되어 있지만 현재로서는 해당 방법만이 존재한다.

DTO Class

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private String id;
    private String name;
    private String phone;
    private String deptId;
    private String deptName;
}

createNativeQuery

    public List<UserDto> test7(){
        return entityManager.createNativeQuery("select user.id as id, user.name as name, user.phone as phone, user.dept_id as deptId, dept.name as deptName " +
                "from user user " +
                "left outer join dept dept on user.dept_id = dept.id " +
                "where match (user.name) against (:name in boolean mode) > 0").setParameter("name","le")
                .unwrap(NativeQuery.class)
                .setResultTransformer(Transformers.aliasToBean(UserDto.class))
                .getResultList();
    }

위의 글을 통해 간단하게 JPQL 사용법을 정리하였다. 지금까지 JPA로 개발을 진행하면서 Query Method만으로는 로직처리에 어려움이 반드시 발생하여 JPQL를 사용하게 되었다.
JPA를 사용한다면 최대한 NativeQuery의 사용은 피해야한다고 생각하며 김영한님의 답변과 같이 차라리 다른 방법을 통해 SQL를 정의하는것이 바람직해 보인다.

다음 글에서는 JPQL를 작성하는 다른 방식으로 QueryDsl에 대한 글을 작성할 예정이다.

profile
알고싶고 하고싶은게 많은 주니어 개발자 입니다.

4개의 댓글

comment-user-thumbnail
2022년 12월 19일

@Deprecated 인 setResultTransformer 대신
createNativeQuery(sql, UserDto.class) 형태로 사용 가능할 것 같네요

1개의 답글
comment-user-thumbnail
2023년 3월 5일

덕분에 좋은 내용 잘 보고 갑니다.
정말 감사합니다.

답글 달기
comment-user-thumbnail
2023년 3월 27일

덕분에 api구현 중 큰 도움을 얻었습니다~감사합니다!

답글 달기