JPA Search By Condition(2) - @Query

GEONNY·2024년 8월 23일
0
post-thumbnail

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

📌@Query parameter 전달 방법

?<position>

parameter 전달 방식은 위와 같이 ?<position> 으로 위치 매개변수를 사용하여 전달 할 수 있습니다. 위치 매개변수는 0이 아닌 1부터 시작하는 것에 주의하세요!

@Param

@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의 가독성을 높여줄 수도 있습니다.

📌Java type 의 Literal 사용

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 Supported functions

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)

📌JPQL Special Operators

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

📌NativeQuery

@QuerynativeQuery 속성을 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();
}

📌NamedNativeQuery

SQL 쿼리를 JPA Entity에 이름으로 정의하여 재사용할 수 있게 해주는 Annotation 입니다. JPQL로 표현하기 어려운 복잡한 쿼리나 특정 Database에 특화된 기능을 사용할 때 유용합니다.

  • 복잡한 SQL 사용 시
  • JPQL로 표현할 수 없는 복잡한 쿼리 사용 시
  • SQL 최적화 및 성능 튜닝 필요 시

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 분리

.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

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를 활용합니다.

📌Record or DTO mapping

@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 을 명확하게 맞춰 주어야 한다는 것 입니다.

📌Concolusion

@Query 를 사용하여 JPQL 및 SQL을 작성하여 활용하는 여러 방법을 알아보았습니다. JPA를 사용하면 Native Query 의 사용을 최대한 지양해야 하지만, 프로젝트를 진행하다보면 Database 설계의 문제나, 여러 복합적인 이유로 어쩔 수 없이 Native Query를 사용해야만 할 경우가 생깁니다. Native Query의 사용은 최대한 지양하되, 적절한 방법으로 활용하도록 합시다.

📚참고

📕JPQL left join, fetch join 차이

JPQL 에서 left join 과 fetch join 은 모두 조인 연산을 수행하지만 그 목적과 동작 방식에 차이가 있습니다. left join 은 연관된 Entity 가 있을 때만 조인하며, 지연 로딩 (Lazy Loading) 전략을 따릅니다. 주로 연관관계 Entity의 존재와 상관없이 데이터를 조회할 때 사용됩니다.
fetch join은 연관관계의 Entity 를 즉시 로딩(Eager Loading)하며 지연 로딩을 무시하고 한 번에 모든 데이터를 로드합니다. N+1 문제를 방지하는데 유용하지만, 페이징 쿼리와 같이 사용하면 문제가 발생할 수 있습니다.

profile
Back-end developer

0개의 댓글