JPA(Java Persistence API)
를 사용하여 서비스를 구현하다 보면 JPA의 Query Methods
만으로는 조회가 불가능한 경우가 존재한다.
이러한 경우 JPQL(Java Persistence Query Language)
를 이용하여 SQL과 비슷한 형태의 쿼리를 작성하여 조회를 할 수 있다.
JPQL를 작성하기 위한 방법에는 여러가지 방법이 존재하나 이번 글에서는 @Query Annotation
과 EntityManager.createQuery
등을 사용하여 JPQL를 작성하는 방법에 대해서 작성하려고 한다.
@Query Annotation
는 Entity
의 JpaRepository
를 상속받는 인터페이스에 정의하게 된다.
기본적인 작성방법은 from
구문에 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;
}
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);
JPQL를 사용게되는 이유는 여러가지가 존재하지만 가장 큰 이유는 function
과 join
의 경우일 것이다. 이러한 경우 정의한 Entity
의 속성외에 속성이 추가 될것이며 이를 위해 DTO
반환이 필요하다.
@Query Annotation
를 사용하여 DTO
반환을 하기위해서는 select
구분에서 생성자
를 통해서 객체를 반환하여야 한다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String id;
private String name;
private String phone;
private String deptId;
private String deptName;
}
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();
}
위에서 이야기한것과 같이 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
에 등록된 각 DataBase
의 Dialect
에 정의된 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);
}
@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();
}
}
NatvieQuery
는 JPQL
이 아닌 SQL
를 직접 정의하여 사용하는 방식이다. 위에서 이야기 한것과 같이 function
과 join
를 하는 경우 JPQL
를 사용할 수도 있지만 SQL
를 직접 정의할수있는 NativeQuery
를 사용할 수 있다.
@Query
를 이용하여 NativeQuery
를 작성하는 방법은 @Query
속성중 nativeQuery
의 값을 true
로 설정하며 value
에는 SQL
문을 그대로 작성하면 된다.
DTO
맵핑의 경우 JPQL
를 사용할때와는 다르게 DTO Class
가 아닌 Getter
만 존재하는 interface
를 정의하며 네이밍은 SQL
의 리턴되는 칼럼명과 일치해야 한다. 다음은 예제이기 때문에 Repository
에 interface
를 정의하였으며 구조에 맞는 위치에 정의해도 상관없다.
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
를 이용하여 NativeQuery
를 작성하는 방법은 createNativeQuery()
를 통해서 SQL
문장을 작성하며 EntityManager
를 사용하는 경우 Hibernate
의 NativeQuery.class
를 이용하여 setResultTransformer
를 통해 DTO class
를 매핑하여 결과를 리턴받는 방법이 있다. 하지만 setResultTransformer
메소드가 @Deprecated
로 예정되어 있지만 현재로서는 해당 방법만이 존재한다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String id;
private String name;
private String phone;
private String deptId;
private String deptName;
}
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
에 대한 글을 작성할 예정이다.
@Deprecated 인 setResultTransformer 대신
createNativeQuery(sql, UserDto.class) 형태로 사용 가능할 것 같네요