이전 글

ORM - Hibernate - JPA - QueryDSL을 알아보자

QueryDSL에 대해서 어떤 것인지, 어떠한 구조를 가지고 ORM이 진행되는지를 알아보았다.

작은 서비스를 가정하고, 구현한 상태에서 QueryDSL을 적용하여 간단히 사용해보자.


셋업

plugins {
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' // Q 클래스 생성 플러그인
    // ...
}

def querydslDir = "$buildDir/generated/querydsl" // 생성된 Q 클래스가 저장될 위치를 정의한다.

// queryDSL 자체에 대한 config
querydsl { 
    jpa = true // jpa = true로 설정하여 JPA 애너테이션을 인식
    querydslSourcesDir = querydslDir // querydslSourcesDir를 querydslDir로 설정하여 Q 클래스에 대한 output 디렉토리를 지정
}

sourceSets {
    main.java.srcDir querydslDir // Q 클래스 등이 저장되는 querydslDir를 소스 코드가 저장되는 디렉토리로 등록
		// 이 덕에 Gradle이 자동으로 QueryDSL 코드를 컴파일할 수 있다.
}

configurations {
    querydsl.extendsFrom compileClasspath // querydsl 클래스 경로를 컴파일 클래스 경로에 상속
}

// 내부적으로 Querydsl은 Java의 Annotation Processor를 이용한다.
// 컴파일러는 이 경로에 위치한 모든 JAR 파일들을 검색하여 annotation processor를 찾는다. 
// 그리고 이를 실행하여 소스 코드에서 정의한 어노테이션들을 처리한다.
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

dependencies {
		//...
		implementation "com.querydsl:querydsl-codegen:5.0.0" // QueryDSL 코드 생성
		implementation "com.querydsl:querydsl-jpa:5.0.0" // JPA 지원
		implementation "com.querydsl:querydsl-apt:5.0.0" // Annotation Process Tool
		//...
}

// -proc:only: 이 옵션은 컴파일러의 어노테이션 처리만 수행하고, 클래스를 실제로 컴파일하지 않는 것을 의미한다.
// -processor: 이 옵션은 어노테이션 프로세서를 지정하는데, QueryDSL의 어노테이션 프로세서와 Lombok의 어노테이션 프로세서를 지정하여 명시적으로 설정한다.
project.afterEvaluate {
    project.tasks.compileQuerydsl.options.compilerArgs = [
            "-proc:only",
            "-processor", project.querydsl.processors() +
                    ',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
    ]
}

// Java 컴파일 작업이 compileQuerydsl 작업에 의존하도록 설정한다.
// compileQuerydsl 작업이 먼저 수행된 후에 compileJava 작업이 수행되게끔 순서를 정해주는 것인데,
// QueryDSL Q-Type 소스 생성 작업이 Java 컴파일 작업 전에 먼저 이뤄지도록 한다.
// 위 afterEvaluate와 같이 사용하면, Lombok 어노테이션 프로세스와 충돌하지 않는다.
tasks.named('compileJava') {
    dependsOn tasks.named('compileQuerydsl')
}

자세한 설명은 gradle에 주석으로 달아놓았다.


Repository


도메인

@Entity
@Table(name = "RENTAL")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Rental {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "RENTAL_ID")
	private Long id;

	@Column(name = "MEMBER_ID", nullable = false)
	private Long memberId;

	@Column(name = "BOOK_ID", nullable = false)
	private Long bookId;

	@Column(name = "RENTAL_STATUS", nullable = false)
	@Enumerated(EnumType.STRING)
	private RentalStatus rentalStatus;

	@Column(name = "RENTED_AT", nullable = false)
	private LocalDateTime rentedAt;

	@Column(name = "RETURNED_AT")
	private LocalDateTime returnedAt;

	@JoinColumn(name = "MEMBER_ID", nullable = false, insertable = false, updatable = false)
	@ManyToOne(fetch = LAZY, optional = false)
	private Member member;

	@JoinColumn(name = "BOOK_ID", nullable = false, insertable = false, updatable = false)
	@ManyToOne(fetch = LAZY, optional = false)
	private Book book;

  // .. 생략
}
/*---------------------------------------------------------*/
@Entity
@Table(name = "BOOK")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {

	@Id
	@GeneratedValue(strategy = javax.persistence.GenerationType.AUTO)
	@Column(name = "BOOK_ID")
	private Long id;

	@Column(name = "TITLE", nullable = false)
	private String title;

	@Column(name = "AUTHOR", nullable = false)
	private String author;

	@Column(name = "PUBLISHER", nullable = false)
	private String publisher;

	@Column(name = "STORED_AT", nullable = false)
	private LocalDateTime storedAt;

	@Column(name = "RELEASED_AT")
	private LocalDateTime releasedAt;

  // .. 생략
}
/*---------------------------------------------------------*/
@Entity
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "MEMBER_ID")
	private Long id;

	@Column(name = "NAME", nullable = false)
	private String name;

	@Column(name = "AGE")
	private int age;

	@Column(name = "REGISTERED_AT", nullable = false)
	private LocalDateTime registeredAt;

	@Column(name = "UPDATED_AT", nullable = false)
	private LocalDateTime updatedAt;

	@Column(name = "DELETED_AT")
	private LocalDateTime deletedAt;

  // .. 생략
}
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class AuthorRentalInfoDto {
	private final String author;
	private final List<RentalInfoDto> rentalInfoDtos;
}
/*---------------------------------------------------------*/
@Getter
@ToString
public class RentalInfoDto {
	private final String title;
	private final String publisher;
	private final LocalDateTime storedAt; // title, publisher, storedAt은 Book의 정보.
	private final String name; // name은 Member의 정보.
	private final LocalDateTime rentedAt; // rentedAt은 Rental의 정보다.
}

대여 중인 책과 그 대여자의 이름에 대한 정보를 담은 DTO, 그리고 그것을 저자(author)를 기준으로 가져오는 Dto가 필요하다고 해보자

그렇다면, author를 매개변수로 받아서 해당 저자가 쓴 책, 그리고 그 책을 따라 대여자 && 대여 정보(대여 시각)을 찾아서 매핑하는 것이 좋아보인다.