동적 쿼리란 ?
동적 쿼리란 상황에 따라 다른 문법의 SQL을 적용하는 것을 말한다.
예를 들면 DB에서 값을 조회할 때 조회 조건이 동적으로 바뀌어야 하는 경우가 많다.(검색 조건이 하나가 아닌 다중 조건일 때) 이런 상황을 Querydsl을 사용하면 손쉽게 해결할 수 있다.왜냐하면 JPA를 통해 작성하면 모든 조건마다 하나씩 메소드를 생성하고 그것에 대한 If문 및 Case문 처리를 통해 코드가 복잡해진다.
name 값이 들어오면 WHERE name = {name} age 값이 들어오면 WHERE age = ${age} name과 age가 모두 들어오면 WHERE name = ${name} AND age={age}
name과 age 모두 들어오지 않으면 WHERE 절을 사용하지 않는다.
Query Dsl을 위해서는 우리는 Dependency설정을 추가적으로 활용해야한다.
필자의 Spring Boot 버전은 3.0.x이면 Java = 17버전을 쓰고 있다 참조하기를 바란다.
plugins { id 'java' id 'org.springframework.boot' version '3.0.4' id 'io.spring.dependency-management' version '1.1.0' id "org.asciidoctor.jvm.convert" version "3.3.2" //원래 3.0 버전이전에는 // id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" // Querydsl 플러그인 사용 한다. } group = 'com.developers.solve' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' configurations { compileOnly { extendsFrom annotationProcessor } asciidoctorExt } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.testng:testng:7.1.0' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' // spring rest docs asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // Query Dsl 의존성 설정 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { useJUnitPlatform() } //Query DSL은 기존엔티티를 쓰지않고 Qclass라고 엔티티를 복사한 새로운 클래스를 쓴다. 그리고 아래의 설정은 Qclass를 저장하는 위치를 설정한다. def querydslDir = "src/main/generated" sourceSets { main { java { srcDirs = ['src/main/java', querydslDir] } } } tasks.withType(JavaCompile) { options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir) } clean.doLast { file(querydslDir).deleteDir() } ext { // (5) snippetsDir = file('build/generated-snippets') // outputDir = file('build/docs/asciidoc') } test { // (6) outputs.dir snippetsDir } asciidoctor { // (7) inputs.dir snippetsDir // (8) configurations 'asciidoctorExt' // (9) dependsOn test // (10) baseDirFollowsSourceDir() // (11) } task copyDocument(type: Copy) { // (12) dependsOn asciidoctor from file("build/docs/asciidoc/") into file("src/main/resources/static/docs") } build { // (13) dependsOn copyDocument } bootJar { // (14) dependsOn copyDocument from ("${asciidoctor.outputDir}") { into 'src/main/resources/static/docs' } }
Query Dsl은 기존의 JPA와 다르게 Query DSL을 위한 레포지토리 클래스를 보통 만들어서 사용합니다.만드는 방법은 아래와 같이 3가지가 있다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {} // 기존 방법2 public class MemberRepositoryCustom extends QuerydslRepositorySupport {} // 추천하는 방법 @Repository @RequiredArgsConstructor public class MemberRepositoryCustom { private final JpaQueryFactory queryFactory; // 물론 이를 위해서는 빈으로 등록을 해줘야 한다. }
필자는 사용법을 그냥 JPA의 Interface를 활용하면 되느줄 알고 계속 빌드했지만, 지속적으로 QueryDSL 레포지토리가 Interface의 메소드를 다 실행하지 않았다는 오류가 떴다.(Ex:QueryDslRepository is not implement method in ProbelmRepositoryinterface)
조심하길 바란다. 필자는 추천하는 방법을 활용했다.
@Repository @RequiredArgsConstructor public class MemberRepositoryCustom { private final JpaQueryFactory queryFactory; // 물론 이를 위해서는 빈으로 등록을 해줘야 한다. }
여기서 빈을 만들어줘야한다는 소리를 분석해보자.
QueryDSL은 JPAQeurtFactory를 통해 쿼리문을 작성해 나갑니다. 그렇기에 우리는 늘 생성자로 주입해줘야합니다. 하지만 이부분을 귀찮기에 Config Class를 만들고 @Bean으로 등록하여 사용하고자 합니다.@Configuration public class QuerydslConfiguration { @Autowired EntityManager em; @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(em); } }
이제 기본 셋팅이 거의 다 끝났습니다. 이제 Repository에서 진짜 Query Dsl를 작성해봅시다.
@Repository @RequiredArgsConstructor public class ProblemRepositoryImp { private final JPAQueryFactory queryFactory; QProblem problem = QProblem.problem; private OrderSpecifier<Long> getProblemSortedByViews(String condition){ if (condition.contains("views")) { return problem.views.desc(); } else return OrderByNull.getDefault(); } private OrderSpecifier<LocalDateTime> getProblemSortedByLocalTime(String condition){ if (condition.contains("localTime")){ return problem.createdAt.desc();} else return OrderByNull.getDefault(); } private OrderSpecifier<Long> getProblemSortedByLikes(String condition){ if (condition.contains("likes")) {return problem.likes.desc();} else return OrderByNull.getDefault(); } public List<Problem> getProblemSortedByLevel(String condition) { return queryFactory.selectFrom(problem).where(containLevel(condition),(containType(condition))).orderBy(getProblemSortedByLikes(condition),getProblemSortedByViews(condition),getProblemSortedByLocalTime(condition)).limit(20).fetch(); } private BooleanExpression containLevel(String condition){ if (condition.contains("gold")||condition.contains("silver")||condition.contains("bronze")){ return problem.level.contains(condition); } else return null; } private BooleanExpression containType(String condition){ if (condition.contains("choice")||condition.contains("answer")){ return problem.type.contains(condition); } else return null; }
위의 코드를 보면 크게 세가지 부분으로 볼 수 있다.
1.public List<Problem> getProblemSortedByLevel(String condition) { return queryFactory.selectFrom(problem).where(containLevel(condition),(containType(condition))).orderBy(getProblemSortedByLikes(condition),getProblemSortedByViews(condition),getProblemSortedByLocalTime(condition)).limit(20).fetch(); } 이처럼 Query문을 직접 실행하고 데이터를 리턴하는 부분.
private BooleanExpression containLevel(String condition){ if (condition.contains("gold")||condition.contains("silver")||condition.contains("bronze")){ return problem.level.contains(condition); } else return null; } private BooleanExpression이라는 메서드 부분
private OrderSpecifier<Long> getProblemSortedByViews(String condition){ if (condition.contains("views")) { return problem.views.desc(); } else return OrderByNull.getDefault(); } private OrderSpecifier이라는 부분
위의 코드 중 1번을 보면 return 부분에서 QuertFactory뒤를 이어 테이블을 selectfrom하고 where과 orderby limit등 흔히아는 쿼리문을 메소드 형식을 작성해 나가는 것을 볼 수 있습니다. 추가적으로 where과 orderby에 여러가지 조건이 ","와 함께 그리고라는 조건으로 동시에 들어가는 것을 볼 수 있습니다. ","뿐만 아니라 or도 있으면 그 외에도 많은 조건들을 지원합니다.
추가적으로 Fetch메소드로 끝맺음을 맺어야 Entity에서 데이터를 불러온다!
그러면 위의 QueryFactory를 제외한 두개의 private 메서드는 무엇인가??? 둘다 where 및 order by에 조건을 넣어주기 위한 메서드라고 생각하면 편하다. 그중 BooleanExpression은 Where조건을 추가해주는 메소드입니다. 위의 예제코드는 condition이라는 조건이 "golds,bronze,silver"라는 문자열을 포함하고 있으면 해당 문자열 들고 Level에서 일치하는 조건의 데이터들을 조회하라는 것이다.
기억해야할 것은 BooleanExpression 데이터를 넘겨주는 것이 아니다. 조건 그자체를 넘겨주는 것이다!!!!
위의 예시 contains뿐만 아니라 eq, eqall,containsingnoreCase등 다양한 메소드를 지원한다.
위 부분 특히 order by의 조건을 할당할 때 사용된다.Ordespecifier에서 주의해야할 점은 두가지가 있다.
1.정렬하고자 하는 컬럼의 자료형을 정확히 기재해라(Ex:Long,Interger)
2.BooleanExpression과 같이 단순 Null이 들어오면 무시하지 못한다....(매우 답답하다.) 그래서 우리는 추가적인 처리가 필요하다. 바로 단순 Null이 아니라 Orderspecifier 상위 클래스 안에 있는 OrderBynull.DEFAULT를 활용해야 NUll로 인식한다. 그를 위해 나는 Util directory에 따로 클래스를 선언해주었다.
package util; import com.querydsl.core.types.NullExpression; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; public class OrderByNull extends OrderSpecifier { private static final OrderByNull DEFAULT = new OrderByNull(); private OrderByNull() { super(Order.ASC, NullExpression.DEFAULT, NullHandling.Default); } public static OrderByNull getDefault() { return OrderByNull.DEFAULT; } }