Dynamic Spring Data JPA Repository Query With Arbitrary AND Clauses

1. 개요

애플리케이션을 Spring Data로 개발하면서, 데이터베이스에서 데이터를 가져오기 위해 선택 기준에 따라 동적 쿼리를 구성해야 할 경우가 자주 있습니다.

이 튜토리얼에서는 Spring Data JPA 리포지토리에서 동적 쿼리를 작성하는 세 가지 방법인 예제 쿼리, 사양 쿼리, 그리고 Querydsl에 대해 탐구합니다.

2. 시나리오 설정

demonstration을 위해 SchoolStudent라는 두 개의 엔티티를 생성하겠습니다. 이 두 클래스 간의 관계는 하나의 School에 여러 Student가 존재하는 일대다 관계입니다.

@Entity
@Table
public class School {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column
    private String name;

    @Column
    private String borough;

    @OneToMany(mappedBy = "school")
    private List<Student> studentList;

    // constructor, getters and setters
}
@Entity
@Table
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column
    private String name;

    @Column
    private Integer age;

    @ManyToOne
    private School school;

    // constructor, getters and setters
}

엔티티 클래스 외에도 Student 엔티티를 위한 Spring Data 리포지토리를 정의하겠습니다.

public interface StudentRepository extends JpaRepository<Student, Long> {
}

마지막으로, School 테이블에 샘플 데이터를 추가하겠습니다.

Student 테이블에도 동일한 작업을 수행하겠습니다.

다음 섹션에서는 다음 기준으로 레코드를 찾을 것입니다:

  • Studentname 끝이 Smith로,
  • Studentage20이면서,
  • Student의 학교가 Ealing 자치구에 위치하는 경우

3. 예제 쿼리

Spring Data는 예제를 사용하여 엔티티를 쿼리하는 간단한 방법을 제공합니다. 아이디어는 간단합니다: 우리는 예제 엔티티를 만들고 그 안에 우리가 찾고 있는 검색 기준을 넣습니다. 그런 다음, 이 예제를 사용하여 일치하는 엔티티를 찾습니다.

이 기능을 사용하기 위해 리포지토리가 QueryByExampleExecutor 인터페이스를 구현해야 합니다. 우리의 경우, 이 인터페이스는 일반적으로 리포지토리에서 확장하는 JpaRepository에 이미 확장되어 있으므로 명시적으로 구현할 필요는 없습니다.

이제 우리가 필터링하고자 하는 세 가지 선택 기준을 포함하는 Student 예제를 생성해 보겠습니다.

School schoolExample = new School();
schoolExample.setBorough("Ealing");

Student studentExample = new Student();
studentExample.setAge(20);
studentExample.setName("Smith");
studentExample.setSchool(schoolExample);

Example<Student> example = Example.of(studentExample);

일단 예제를 설정한 후, 리포지토리의 findAll(…) 메소드를 호출하여 결과를 얻습니다:

List<Student> studentList = studentRepository.findAll(example);

그러나 위의 예제는 정확한 매칭만 지원합니다. 만약 “Smith“로 끝나는 이름을 가진 학생들을 찾고 싶다면, 매칭 전략을 사용자 정의해야 합니다. 예제를 통해 쿼리하기는 ExampleMatcher 클래스를 제공하므로, 이를 사용할 수 있습니다. 이름 필드에 대해 ExampleMatcher를 생성하고 이를 Example 인스턴스에 적용하기만 하면 됩니다.

ExampleMatcher customExampleMatcher = ExampleMatcher.matching()
  .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase());
Example<Student> example = Example.of(studentExample, customExampleMatcher);

여기서 사용하는 ExampleMatcher는 자명합니다. 이름 필드를 대소문자 구분 없이 매칭하며 지정한 예제의 이름으로 끝나도록 값을 제한합니다.

쿼리 by 예제는 이해하기 쉽고 구현하기 간단합니다. 그러나 필드에 대한 더 복잡한 쿼리 조건, 예를 들어, 보다 크거나 작음을 지원하지 않습니다.

4. 사양 쿼리

Spring Data JPA의 사양 쿼리는 조건 집합에 기반하여 동적 쿼리를 생성할 수 있게 해주는 Specification 인터페이스를 제공합니다.

전통적인 방법인 파생 쿼리 메소드나 @Query로 사용자 정의 쿼리와 비교했을 때 이 접근 방식은 더 유연합니다. 복잡한 쿼리 요구 사항이나 런타임에 쿼리를 동적으로 조정해야 할 때 유용합니다.

예제 쿼리와 유사하게, 리포지토리 메소드는 이 기능을 활성화하기 위해 인터페이스를 확장해야 합니다. 이번에는 JpaSpecificationExecutor를 확장해야 합니다.

public interface StudentRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> {
}

다음으로, 각 필터 조건에 대한 세 가지 메소드를 정의해야 합니다. Specification은 하나의 필터 조건에 대해 하나의 메소드만 사용하도록 제한하지 않습니다. 이는 주로 가독성을 위한 것입니다.

public class StudentSpecification {
    public static Specification<Student> nameEndsWithIgnoreCase(String name) {
        return (root, query, criteriaBuilder) ->
          criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase());
    }

    public static Specification<Student> isAge(int age) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("age"), age);
    }

    public static Specification<Student> isSchoolBorough(String borough) {
        return (root, query, criteriaBuilder) -> {
            Join<Student, School> schoolJoin = root.join("school");
            return criteriaBuilder.equal(schoolJoin.get("borough"), borough);
        };
    }
}

위의 메소드를 통해, CriteriaBuilder를 사용하여 필터 조건을 구성하는 것을 알 수 있습니다. CriteriaBuilder는 JPA에서 프로그래밍 방식으로 동적 쿼리를 작성하는 데 도움을 주며 SQL 쿼리 작성을 통해 얻는 유연성을 제공합니다. equal(…)like(…)와 같은 메소드로 조건을 정의하여 프레디케이트를 만들 수 있게 합니다.

기본 테이블과 추가 테이블을 조인해야 하는 더 복잡한 작업이 있을 경우, Root.join(…)을 사용합니다. 루트는 FROM 절의 앵커 역할을 하며, 엔티티의 속성과 관계에 대한 접근을 제공합니다.

이제 Specification을 사용하여 필터링된 결과를 얻기 위해 리포지토리 메소드를 호출해 보겠습니다.

5. QueryDSL 쿼리

[사양**은 *예제*에 비해 더 복잡한 쿼리를 다룰 수 있는 강력한 기능을 가지고 있습니다. 그러나 *사양* 인터페이스는 복잡한 쿼리를 다룰 때 장황해지고 가독성이 떨어질 수 있습니다.**

이곳에서 QueryDSL사양의 한계를 해결하려고 시도합니다. 직관적이고 읽기 쉬우며 강력한 형식으로 동적 쿼리를 생성할 수 있는 형식 안전 프레임워크입니다.

QueryDSL을 사용하기 위해 몇 가지 종속성을 추가해야 합니다. 다음 Querydsl JPA 및 APT 지원 종속성을 pom.xml에 추가하겠습니다.

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>5.1.0</version>
    <classifier>jakarta</classifier>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.1.0</version>
    <classifier>jakarta</classifier>
    <scope>provided</scope>
</dependency>

특히, JPA의 패키지 이름이 JPA 3.0부터 javax.persistence에서 jakarta.persistence로 변경되므로, 버전 3.0 이상의 경우 종속성에 jakarta 분류자를 추가해야 합니다.

그 외에도, pom.xml의 플러그인 섹션에도 다음과 같은 주석 프로세서를 포함해야 합니다.

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

이 프로세서는 컴파일 시간 중에 엔티티 클래스에 대한 메타 모델 클래스를 생성합니다. 이러한 설정을 애플리케이션에 통합하고 컴파일하면 Querydsl이 빌드 폴더에 두 개의 쿼리 유형인 QStudentQSchool을 생성합니다.

이와 유사하게, 이번에는 리포지토리가 이러한 쿼리 유형을 사용하여 결과를 가져올 수 있도록 QuerydslPredicateExecutor를 포함해야 합니다.

public interface StudentRepository extends JpaRepository<Student, Long>, QuerydslPredicateExecutor<Student> {
}

다음으로, 이러한 쿼리 유형을 기반으로 동적 쿼리를 생성하고 이를 쿼리를 위한 StudentRepository에서 사용하겠습니다. 이 쿼리 유형은 해당 엔티티 클래스의 모든 속성을 포함하므로 프레디케이트를 빌드할 때 필요한 필드를 직접 참조할 수 있습니다.

QStudent qStudent = QStudent.student;
BooleanExpression predicate = qStudent.name.endsWithIgnoreCase("smith")
  .and(qStudent.age.eq(20))
  .and(qStudent.school.borough.eq("Ealing"));
List<Student> studentList = (List) studentRepository.findAll(predicate);

위의 코드에서 쿼리 유형으로 쿼리 조건을 정의하는 것은 간단하고 직관적입니다.

필요한 종속성 설정이 복잡하지만 이는 일회성 작업이며, 사양과 같은 유창한 방법을 제공하여 직관적이고 읽기 쉽습니다.

또한, 사양 클래스에서 요구하는 대로 필터링 조건을 수동으로 명시적으로 정의할 필요가 없습니다.

6. 결론

이 기사에서는 Spring Data JPA에서 동적 쿼리를 생성하는 다양한 접근 방식을 탐구했습니다.

  • 예제 쿼리는 간단한 정확한 일치 쿼리에 가장 적합합니다.
  • 사양 쿼리는 SQL과 유사한 표현과 비교가 필요할 경우 적절한 복잡한 쿼리에 잘 작동합니다.
  • QueryDSL의 경우, 쿼리 유형을 사용하여 조건을 정의하기 쉬워지기 때문에 매우 복잡한 쿼리에서 최적입니다.

항상 그렇듯이, 우리의 예제의 전체 소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다