JPQL(Java Persistence Query Language) 이나 Native query 를 직접 작성하여 조회할 수 있는 @Query
에 대해서 알아보겠습니다. Query methods 와 마찮가지로 Repository 에 Method 를 작성하여 활용합니다. 이전 작성 했던 Query Methods 예제를 @Query
를 사용하여 JPQL을 작성해보겠습니다.
domain.member.MemberRepository
@Repository
public interface MemberRepository extends JpaRepository<Member, String> {
//생략 ..
// memberName 으로 조회
@Query("select m from member m where m.memberName = ?1")
List<Member> findMembersByMemberName(String memberName);
// memberId (equal) 와 memberName (like) 을 And 조건으로 조회
@Query("select m from member m where m.memberId = ?1 " +
"and m.memberName like concat('%', ?2, '%')")
List<Member> findMembersByMemberIdAndMemberName(String memberId, String memberName);
// memberId (equal) 와 memberName (like) 을 Or 조건으로 조회
@Query("select m from member m where m.memberId = ?1 " +
"or m.memberName like concat('%', ?2, '%')")
List<Member> findMembersByMemberIdOrMemberName(String memberId, String memberName);
// Enum type 조회
@Query("select m from member m where m.useYn = ?1")
List<Member> findMembersByUseYn(UseYn useYn);
// 연관관계인 Authority 의 authorityName 으로 조회
@Query("select m from member m where m.authority.authorityName = ?1")
List<Member> findMembersByAuthorityAuthorityName(String authorityName);
// BaseEntity 의 createDate 를 between 조건으로 조회
@Query("select m from member m where m.createDate between ?1 and ?2")
List<Member> findMembersByCreateDateBetween(LocalDateTime startDate
, LocalDateTime endDate);
}
parameter 전달 방식은 위와 같이 ?<position>
으로 위치 매개변수를 사용하여 전달 할 수 있습니다. 위치 매개변수는 0이 아닌 1부터 시작하는 것에 주의하세요!
@Param
을 사용해서 parameter 를 전달할 수 있습니다. JPQL 내에서는 :<parameter>
로 사용하며 @Param 속성의 값과 parameter명을 일치시켜야 합니다.
// memberName 으로 조회
@Query("select m from member m where m.memberName = :memberName")
List<Member> findMembersByMemberName(@Param("memberName") String memberName);
// memberId (equal) 와 memberName (like) 을 And 조건으로 조회
@Query("""
select m from member m where m.memberId = :memberId
and m.memberName like %:memberName%
""")
List<Member> findMembersByMemberIdAndMemberName(@Param("memberId") String memberId,
@Param("memberName") String memberName);
위와 같이 Text block(""" """
) 을 사용하여 JPQL의 가독성을 높여줄 수도 있습니다.
JPQL 내에 Java type의 리터럴 을 사용할 수 있습니다.
String
@Query("select m from member m where m.memberName = '이건'")
Enum
@Query("select m from member m where m.useYn = com.geonlee.api.entity.enumeration.UseYn.N")
Date
@Query("select m from member m where m.createDate between {d '2024-08-01'} and {d '2024-08-20'}")
Timestamp
@Query("""
select m from member m where m.createDate between {ts '2024-08-19 21:25:00.000000001'}
and {ts '2024-08-19 21:30:00.000000001'}
""")
JPQL 에서 지원하는 함수들을 알아보겠습니다. Database 에 따라 지원하지 않는 함수가 있을 수 있으니 확인 후 사용하시기 바랍니다.
연산자/함수 | 설명 | 예제 코드 |
---|---|---|
- (subtraction) | 뺄셈 | e.salary - 1000 |
+ (addition) | 덧셈 | e.salary + 1000 |
* (multiplication) | 곱셈 | e.salary * 2 |
/ (division) | 나눗셈 | e.salary / 2 |
ABS | 절대값 계산 | ABS(e.salary - e.manager.salary) |
CASE | 조건문 정의 | CASE e.status WHEN 0 THEN 'active' WHEN 1 THEN 'consultant' ELSE 'unknown' END |
COALESCE | 첫 번째 null이 아닌 값 반환 | COALESCE(e.salary, 0) |
CONCAT | 문자열을 결합 | CONCAT(e.firstName, ' ', e.lastName) |
CURRENT_DATE | 데이터베이스의 현재 날짜 | CURRENT_DATE |
CURRENT_TIME | 데이터베이스의 현재 시간 | CURRENT_TIME |
CURRENT_TIMESTAMP | 데이터베이스의 현재 날짜-시간 | CURRENT_TIMESTAMP |
LENGTH | 문자열 또는 바이너리 값의 길이 | LENGTH(e.lastName) |
LOCATE | 문자열 내에서 다른 문자열의 인덱스 반환, 선택적으로 시작 인덱스 설정 가능 | LOCATE('-', e.lastName) |
LOWER | 문자열을 소문자로 변환 | LOWER(e.lastName) |
MOD | 첫 번째 정수를 두 번째 정수로 나눈 나머지 계산 | MOD(e.hoursWorked / 8) |
NULLIF | 첫 번째 인자가 두 번째 인자와 같으면 null을 반환, 그렇지 않으면 첫 번째 인자를 반환 | NULLIF(e.salary, 0) |
SQRT | 숫자의 제곱근 계산 | SQRT(o.result) |
SUBSTRING | 문자열에서 시작 인덱스부터 부분 문자열을 반환, 선택적으로 부분 문자열의 크기 설정 가능 | SUBSTRING(e.lastName, 0, 2) |
TRIM | 문자열의 앞뒤 또는 양쪽에서 공백 또는 선택적인 트림 문자 제거 | TRIM(TRAILING FROM e.lastName), TRIM(e.lastName), TRIM(LEADING '-' FROM e.lastName) |
UPPER | 문자열을 대문자로 변환 | UPPER(e.lastName) |
CAST | 데이터 타입 변환 | CAST(e.createDate AS string) |
YEAR | 날짜, 시간 데이터에서 년 추출 | YEAR(e.createDate) |
MONTH | 날짜, 시간 데이터에서 월 추출 | MONTH(e.createDate) |
DAY | 날짜, 시간 데이터에서 일 추출 | DAY(e.createDate) |
Database 연산자는 아니지만 JPQL에서 활용할 수 있는 연산자들을 알아보겠습니다.
Function | Description | Example |
---|---|---|
INDEX | Ordered List 요소의 인덱스, @OrderColumn과 함께 사용 가능 | SELECT toDo FROM Employee e JOIN e.toDoList toDo WHERE INDEX(toDo) = 1 |
KEY | Map 요소의 키 | SELECT p FROM Employee e JOIN e.priorities p WHERE KEY(p) = 'high' |
SIZE | 컬렉션 관계의 크기, 서브쿼리로 평가됨 | SELECT e FROM Employee e WHERE SIZE(e.managedEmployees) < 2 |
IS EMPTY, IS NOT EMPTY | 컬렉션 관계가 비어 있으면 true로 평가, 서브쿼리로 평가됨 | SELECT e FROM Employee e WHERE e.managedEmployees IS EMPTY |
MEMBER OF, NOT MEMBER OF | 컬렉션 관계에 값이 포함되어 있으면 true로 평가, 서브쿼리로 평가됨 | SELECT e FROM Employee e WHERE 'write code' MEMBER OF e.responsibilities |
TYPE | 상속 구분자 값 | SELECT p FROM Project p WHERE TYPE(p) = LargeProject |
TREAT | 객체를 서브클래스로 캐스팅 (JPA 2.1 도입) | SELECT e FROM Employee JOIN TREAT(e.projects as LargeProject) p WHERE p.budget > 1000000 |
FUNCTION | 데이터베이스 함수를 호출 (JPA 2.1 도입) | SELECT p FROM Phone p WHERE FUNCTION('TO_NUMBER', p.areaCode) > 613 |
@Query
에 nativeQuery
속성을 ture
로 설정하여 native query 를 작성할 수 있습니다. parameter 전달 방식은 동일합니다.
@Query(value = "select * from member where member_nm = :memberName", nativeQuery = true)
List<Member> findMembersByMemberName(String memberName);
Native Query 사용 시 위와 같이 Entity 를 받을 수 있는 경우와 없는 경우가 있습니다. Entity 와 SQL 의 결과 필드가 1:1 로 매핑되는 경우라면 위와 같이 Entity 로 매핑하여 가져올 수 있습니다. 하지만 하나라도 다른 경우 매핑이 되지 않으며, 이 경우 getter
가 있는 interface
로 매핑해야 합니다. 이처럼 Native Query의 매핑 방식은 SQL 결과와 Entity 간의 구조적 일치여부에 따라 결정됩니다.
@Query(value = "select member_id, member_nm from member where member_nm = :memberName"
, nativeQuery = true)
List<MemberProjection> findMembersByMemberName(String memberName);
MemberProjection.interface
public interface MemberProjection {
String getMemberId();
String getMemberName();
}
SQL 쿼리를 JPA Entity에 이름으로 정의하여 재사용할 수 있게 해주는 Annotation 입니다. JPQL로 표현하기 어려운 복잡한 쿼리나 특정 Database에 특화된 기능을 사용할 때 유용합니다.
entity.Member
@NamedNativeQuery(
name = "Member.findByMemberName",
query = "SELECT * FROM member WHERE member_nm = :memberName",
resultClass = Member.class
)
public class Member extends BaseEntity implements Persistable<String> {
//이하 생략..
@Query 에 name
속성으로 NamedNativeQuery 의 name 속성과 연결해줍니다.
domain.member.MemberRepository
@Query(name = "Member.findByMemberName")
List<Member> findMembersByMemberName(String memberName);
.xml 파일로 query를 분리하여 NamedNativeQuery 를 사용할 수 있습니다. application.yml 에 query가 있는 .xml 파일 경로를 설정합니다.
spring:
jpa:
mapping-resources:
- query/memberNativeQuery.xml
resources 하위 폴더 경로에 위에 설정한 .xml 파일을 생성하고 <named-native-query>
태그 사이에 쿼리를 작성합니다. <sql-result-set-mapping>
태그를 사용하여 컨버팅할 record를 설정하고 각 필드의 속성을 설정해 줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
version="2.2">
<named-native-query name="Member.findByMemberName" result-set-mapping="MemberResultMap">
<query>
SELECT t1.member_id as memberId
, t1.member_nm as memberName
, t1.use_yn as useYn
, t2.authority_nm as authorityName
, TO_CHAR(t1.create_dt, 'YYYY-MM-DD HH24:MI:SS') as createDate
, TO_CHAR(t1.update_dt, 'YYYY-MM-DD HH24:MI:SS') as updateDate
FROM member t1
LEFT JOIN authority t2 ON (t2.authority_cd = t1.authority_cd)
WHERE member_nm = :memberName
</query>
</named-native-query>
<sql-result-set-mapping name="MemberResultMap">
<constructor-result
target-class="com.geonlee.api.domin.member.record.MemberSearchResponse">
<column name="memberId" class="java.lang.String"/>
<column name="memberName" class="java.lang.String"/>
<column name="useYn" class="java.lang.String"/>
<column name="authorityName" class="java.lang.String"/>
<column name="createDate" class="java.lang.String"/>
<column name="updateDate" class="java.lang.String"/>
</constructor-result>
</sql-result-set-mapping>
</entity-mappings>
domain.member.MemberRepository
@Query(name = "Member.findByMemberName", nativeQuery = true)
List<MemberSearchResponse> findMembersByMemberName(String memberName);
기본적으로 .xml 의 <named-native-query>
name 속성으로 Repository의 method를 찾아서 매핑됩니다. (Entity명.Method명) 이름이 다른경우 위와 같이 @Query 의 name 속성으로 native query 를 매핑해줍니다.
EntityManager 의 createQuery 를 활용하여 @Query 와 동일한 구현을 할 수 있습니다.
List<Member> memberList = entityManager.createQuery(
"select m from member m where m.memberId = :memberId")
.setParameter("memberId", "member01")
// .unwrap(NativeQuery.class) nativeQuery 사용 시
.getResultList();
간결하고 자주사용되는 쿼리는 @Query 를 사용하여 작성하고, 동적으로 변경되어야 하는 쿼리는 createQuery를 활용합니다.
@Query 를 사용하여 JPQL을 작성할 때 select 키워드 다음에 new record 생성자를 추가하여 Entity 가 아닌 record or DTO 를 반환할 수 있습니다. Entity 속성을 그대로 사용하는 경우가 아닌, JPQL Function 등을 사용하여 속성 이외의 값을 가져올 경우 활용합니다.
@Query(value = """
select new com.geonlee.api.domin.member.record.MemberSearchResponse(
m.memberId
, m.memberName
, CAST(m.useYn AS string)
, m.authority.authorityName
, CAST(FUNCTION('TO_CHAR', m.createDate, 'YYYY-MM-DD HH24:MI:SS') AS string)
, CAST(FUNCTION('TO_CHAR', m.createDate, 'YYYY-MM-DD HH24:MI:SS') AS string)
from member m
where m.memberName = :memberName
""")
List<MemberSearchResponse> getMemberToRecord(String memberName);
주의해야 할 점은 Function을 사용했을 경우나 Entity 와 변경할 record or DTO 속성 type 을 명확하게 맞춰 주어야 한다는 것 입니다.
@Query 를 사용하여 JPQL 및 SQL을 작성하여 활용하는 여러 방법을 알아보았습니다. JPA를 사용하면 Native Query 의 사용을 최대한 지양해야 하지만, 프로젝트를 진행하다보면 Database 설계의 문제나, 여러 복합적인 이유로 어쩔 수 없이 Native Query를 사용해야만 할 경우가 생깁니다. Native Query의 사용은 최대한 지양하되, 적절한 방법으로 활용하도록 합시다.
JPQL 에서 left join 과 fetch join 은 모두 조인 연산을 수행하지만 그 목적과 동작 방식에 차이가 있습니다. left join 은 연관된 Entity 가 있을 때만 조인하며, 지연 로딩 (Lazy Loading) 전략을 따릅니다. 주로 연관관계 Entity의 존재와 상관없이 데이터를 조회할 때 사용됩니다.
fetch join은 연관관계의 Entity 를 즉시 로딩(Eager Loading)하며 지연 로딩을 무시하고 한 번에 모든 데이터를 로드합니다. N+1 문제를 방지하는데 유용하지만, 페이징 쿼리와 같이 사용하면 문제가 발생할 수 있습니다.