spring / / 2023. 11. 9. 14:38

Querydsl 소개

이 내용은 baeldung의 querydsl 내용을 정리한 글이다. (https://www.baeldung.com/intro-to-querydsl)

Querydsl의 목적

ORM 프레임워크는 엔터프라이즈 자바의 핵심이다. 이것은 객체지향과 관계형 데이터베이스 모델과의 간극을 좁혀주는 역할을 한다. 또한 개발자로 하여금 더 깨끗하고 더 정교하게 퍼시스턴스 코드와 도메인 로직을 작성하게 도와준다.

하지만 ORM 프레임워크에서 가장 어려운 디자인 요소 중 하나는 올바르고 타입 안정성(type-safe)있는 쿼리를 작성하는데 있다.

JAVA ORM 프레임워크 중 가장 널리 사용되는 것 중 하나인 Hibernate는 문자열 기반 쿼리 언어인 HQL(JPQL)을 사용한다. 가장 확실한 단점은 타입 안정성이 부족하고 정적 쿼리를 체크하기 어렵다는 것이다. 게다가 복잡한 HQL쿼리로 작성한다는 것은 불안정하고 에러가 발생하기 쉬운 문자열 조합을 한다는 것이다.

JPA 2.0에서는 Criteria Query API로 개선할 수 있다. 어노테이션 프로세싱을 할 때 생성되는 메타모델 클래스를 사용하여 타입 안정성이 있는 쿼리를 작성할 수 있는 장점이 있다. Criteria Query API는 장황하고 가독성이 떨어진다. 여기에 하나의 예제가 있다.

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Pet> cq = cb.createQuery(Pet.class);
Root<Pet> pet = cq.from(Pet.class);
cq.select(pet);
TypedQuery<Pet> q = em.createQuery(cq);
List<Pet> allPets = q.getResultList();

Querydsl 클래스 생성

Querydsl API를 사용하여 마법과 같은 메타클래스를 생성해보자.

Maven에 Querydsl 추가

프로젝트에 Querydsl을 추가하는 것은 빌드 파일과 JPA 어노테이션 처리를 위한 플러그인을 구성할 때 몇 가지 의존성을 추가하는 것 만큼 간단하다. Querydsl 라이브러리 버전은 <properties> 영역에 구분되어 있다.

<properties>
    <querydsl.version>5.0.0</querydsl.version>
</properties>

다음으로 <dependencies> 영역에 다음 의존성을 추가하자.

<dependencies>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
        <classifier>jakarta</classifier>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <classifier>jakarta</classifier>
        <version>${querydsl.version}</version>
    </dependency>
</dependencies>

querydsl-apt 의존성은 어노테이션 프로세싱 도구(APT)이다 - 컴파일 단계 전 소스 파일의 어노테이션을 처리할 수 있도록 하는 Java API와 관련된 구현이다. 이 도구는 소위 말하는 Q타입을 만들어낸다 - 애플리케이션에서 엔터티 클래스와 직접 관련된 클래스이지만 Q 문자로 시작한다. 만일 애플리케이션에서 @Entity 어노테이션이 User 클래스에 있다면 QUser.java 소스파일의 Q타입이 생성될 것이다.

querydsl-apt 의존성의 provided scope의 의미는 해당 jar는 빌드 때에만 사용되고 아티팩트에는 포함되지 않는다는 것을 나타낸다.

querydsl-jpa 라이브러리는 JPA 애플리케이션과 동작하도록 만들어진 Querydsl이다.

querydsl-apt를 사용하는 어노테이션 프로세싱 플러그인을 구성하기 위해서는 pom 파일에 다음과 같은 플러그인 구성을 추가하면 된다.

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

이 플러그인은 Q타입이 maven 빌드 동안 생성된다는 것을 보장한다. outputDirectory는 Q타입 파일이 어디에 생성되는지를 나타낸다. 이 속성 값은 나중에 Q파일을 찾아볼 때 유용하다.

IDE에서 자동으로 생성하지 않는다면 프로젝트 소스 파일에 이 디렉토리를 추가해야 한다.

여기에서는 blog 서비스에 대한 간단한 JPA 모델을 사용할 것이다. Users와 BlogPosts는 1대다 관계에 있다.

@Entity
@Getter
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String login;

    private Boolean disabled;

    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user")
    private Set<BlogPost> blogPosts = new HashSet<>(0);
}
@Entity
@Getter
public class BlogPost {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String body;

    @ManyToOne
    private User user;
}

Q타입을 생성하기 위해서 다음을 실행하자.

mvn compile

생성된 클래스 확인

apt-maven-plugin의 outputDirectory에 있는 디렉토리(target/generated-sources/java)를 보자. 여기에 도메인 모델이 직접 반영되어 있는 패키지와 클래스 구조를 확인할 수 있을 것이다.

QUser.java 파일 열어보자. 이 파일은 최상위 엔터티로 User 를 가지고 있는 모든 쿼리를 작성할 수 있는 시작점이다. 여기서 알아야 할 내용은 이 파일이 자동으로 생성이 되었고 손으로 직접 수정하면 안된다는 것을 나타내는 @Generated 어노테이션이다. 만일 도메인 모델 클래스를 변경한다면 Q타입에 관련된 것을 재생성하기 위해 mvn compile을 실행해야 할 것이다.

여러 가지 QUser 생성자와는 별도로 QUser 클래스의 public static final 인스턴스를 확인할 수 있다.

public static final QUser user = new QUser("user");

이것은 엔터티에서 단일 쿼리로 여러 테이블 인스턴스를 조인하는 것과 같이 복잡한 쿼리를 사용할 때를 제외하고 Querydsl 쿼리를 작성시 대부분의 경우에 사용할 수 있는 인스턴스이다.

마지막으로 QUser 클래스에 NumberPath id, StringPath login, SetPath blogPosts와 같이 Q타입이 Path 필드와 관련이 있다. 이 필드들은 나중에 알아볼 fluent 쿼리 API의 부분으로 사용된다.

Querydsl로 쿼리하기

간단한 쿼리(Query)와 필터링(Filtering)

우선 쿼리를 만들기 위해서 좀 더 선호되는 방식인 JPAQueryFactory 인스턴스를 만들 필요가 있다. JPAQueryFactory 가 필요한 것은 EntityManager이다. 이것은 EntityManagerFactory.createEntityManager() 호출이나 @PersistenceContext 주입을 통해 JPA 애플리케이션에서 이미 사용해본 것이다.

EntityManagerFactory emf = 
  Persistence.createEntityManagerFactory("com.baeldung.querydsl.intro");
EntityManager em = entityManagerFactory.createEntityManager();
JPAQueryFactory queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em);

우선 첫 번째 쿼리를 만들어보자.

QUser user = QUser.user;

User c = queryFactory.selectFrom(user)
  .where(user.login.eq("David"))
  .fetchOne();

변수로 QUser 라는 user를 정의하고 QUser.user 로 초기화했다. 간단하게 나타내기 위한 것 뿐이다. QUser.user는 static import를 해야 한다.

처음에 JPAQueryFactoryselectFrom 메소드에서 쿼리 작성이 시작된다. 여기에 QUser 인스턴스를 넘기고 where() 메소드의 쿼리절을 계속 만든다. user.loginUser 클래스의 StringPath 필드에 대한 참조값이다. StringPath 객체는 또한 필드 동등 비교조건을 나타냄으로써 쿼리 작성을 좀 더 편리하게 사용할 수 있도록 하는 .eq() 메소드를 가지고 있다.

마지막으로 데이터베이스에서 영속성 컨텍스트로 값을 가져오기 위해서 체인 마지막에 fetchOne() 메소드를 호출하였다. 이 메소드는 객체가 조회되지 않으면 null 을 리턴하고 .where 조건을 만족하는 여러 결과가 있다면 NonUniqueResultException 예외를 던진다.

Ordering and Grouping

목록의 모든 사용자를 login 오름차순으로 정렬된 조건으로 조회해보자.

List<User> c = queryFactory.selectFrom(user)
  .orderBy(user.login.asc())
  .fetch();

Path 클래스나 .asc().desc() 메소드가 있어 이런 식으로 사용할 수 있다. orderBy() 메소드를 다양한 필드를 정렬하는데 여러 개의 인수로 지정할 수도 있다.

이제 좀 더 복잡한 것을 해보자. 모든 posts를 title로 그룹핑하고 중복 title을 카운팅해야 한다고 가정하자. 이것은 .groupBy() 절로 할 수가 있다. 또한 title을 count로 정렬하고 싶을 수도 있다.

NumberPath<Long> count = Expressions.numberPath(Long.class, "c");

List<Tuple> userTitleCounts = queryFactory.select(
  blogPost.title, blogPost.id.count().as(count))
  .from(blogPost)
  .groupBy(blogPost.title)
  .orderBy(count.desc())
  .fetch();

title을 그룹핑하고 count로 정렬하면서 blog post의 title과 중복 건수를 선택했다. .orderBy() 절에서 참조가 필요하기 때문에 우선 .select() 절에서 count() 필드에 별칭을 생성했다.

조인(Join)과 서브쿼리(Subquery)로 복잡한 쿼리

"Hello World!"라는 제목으로 post를 작성한 모든 사용자를 찾아보자. 그런 경우에 inner join을 사용할 수가 있다. .on 절에 참조를 가진 조인 테이블에 blogPost 별칭을 만들었다.

QBlogPost blogPost = QBlogPost.blogPost;

List<User> users = queryFactory.selectFrom(user)
  .innerJoin(user.blogPosts, blogPost)
  .on(blogPost.title.eq("Hello World!"))
  .fetch();

서브쿼리에서도 동일하게 해보자.

List<User> users = queryFactory.selectFrom(user)
  .where(user.id.in(
    JPAExpressions.select(blogPost.user.id)
      .from(blogPost)
      .where(blogPost.title.eq("Hello World!"))))
  .fetch();

보는 바와 같이 서브쿼리는 쿼리와 매우 비슷하여 읽기 쉽지만 JPAExpressions 팩토리 메소드로 작성을 해야 한다. 서브쿼리를 메인 쿼리와 연결하기 위해서는 이전에 사용된 별칭을 참조해야만 한다.

데이터 수정

JPAQueryFactory는 쿼리 조회 뿐만 아니라 수정, 삭제도 할 수 있다. 사용자 로그인과 계정 잠금을 해보자.

queryFactory.update(user)
  .where(user.login.eq("Ash"))
  .set(user.login, "Ash2")
  .set(user.disabled, true)
  .execute();

각각 다른 필드에 여러가지 set() 절을 사용할 수 있다. .where() 절은 반드시 필요하고 한번에 모든 레코드를 업데이트 할 수 있다.

특정 조건을 만족하는 레코드를 삭제하기 위해서 유사한 구문을 사용할 수 있다.

queryFactory.delete(user)
  .where(user.login.eq("David"))
  .execute();

.where() 절은 필요한 것은 아니지만 주의해야 한다. .where() 절은 특정 유형의 모든 엔티터를 삭제할 수가 있다.

JPAQueryFactory 에 .insert() 메소드는 왜 없는지 이상하게 생각할 수도 있다. 이것은 JPA 쿼리 인터페이스의 제약이다. jakarta.persistence.Query.executeUpdate() 메소드는 update와 delete를 실행할 수는 있지만 insert 구문을 실행할 수 없다. 데이터를 insert하기 위해서 EntityManager로 엔터티를 단지 persist 하기만 하면 된다.

Querydsl 구문으로 데이터를 insert하고 싶다면 querydsl-sql 라이브러리에 있는 SQLQueryFactory 클래스를 사용해야 한다.

참고

https://www.baeldung.com/intro-to-querydsl

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유