Querydsl์ด๋ ์ ์ ํ์ ์ ์ด์ฉํ์ฌ SQL๊ณผ ๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋๋ก ์ง์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์๋ฏธํ๋ค.
์ฆ SQL ํ์์ ์ฟผ๋ฆฌ๋ฅผ Type-Safeํ๊ฒ ์์ฑํ ์ ์๋๋ก DSL์ ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ ๊ฒ์ด๋ค.
@Query
๋ฅผ ์ฌ์ฉํด์ ์ง์ JPQL์ ์ฟผ๋ฆฌ๋ฅผ ๋ณดํต ์์ฑํ ๊ฒ์ด๋ค.implementaion 'com.querydsl:querydsl-jpa:5.0.0:jakarata'
ํด๋น ์ฝ๋๋ฅผ ์ถ๊ฐํ๋๋ฐ, querydsl-jpa ๋ฒ์ ๋ช ์ ๋ค์ :jakarta ์ถ๊ฐ ๊ผญ ํ๊ธฐ!!
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
// querydsl ์ธํ
์์
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
๐build.gradle ์ ์ฒด ์ฝ๋ ๐
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
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()
}
// querydsl ์ธํ
์์
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
spring.application.name=QueryDsl-tutorial
spring.datasource.url=jdbc:mysql://localhost:3306/querydsl
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username={๋์ ์ฌ์ฉ์ ์ด๋ฆ}
spring.datasource.password={๋์ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ๋น๋ฐ๋ฒํธ}
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
logging.level.org.springframework.data.jpa.repository.query=DEBUG
logging.level.com.querydsl.jpa.impl.JPAQueryFactory=DEBUG
package com.springboot.querydsltutorial.entity;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@OneToMany(mappedBy = "user")
@JsonManagedReference
private List<Post> posts;
}
package com.springboot.querydsltutorial.entity;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JsonBackReference
@JoinColumn(name = "user_id")
private User user;
}
์ฌ๊ธฐ๊น์ง ์ ๋ฐ๋ผ์์ผ๋ฉด build๋ฅผ ํ๋ฒ ์คํํด๋ณด์!
๊ทธ๋ ๊ฒ ๋๋ฉด Q๋๋ฉ์ธ ํด๋์ค๊ฐ ์๊ธฐ๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
๋๋ User ์ํฐํฐ์ Post ์ํฐํฐ๋ฅผ ๋ง๋ค์๊ธฐ ๋๋ฌธ์ QUser ํด๋์ค, QPost ํด๋์ค๊ฐ ๋ง๋ค์ด ์ก๋ค.
Q๋๋ฉ์ธ ํด๋์ค๋ JPA ์ํฐํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋์ด ํด๋น ์ํฐํฐ์ ํ๋์ ๊ด๊ณ๋ฅผ ์ ์ ์ผ๋ก ๋ํ๋ด๋ java ํด๋์ค๋ค.
package com.springboot.querydsltutorial.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
@PersistenceContext
JPA์์ ์ฌ์ฉํ๋ฉฐ Entity Manager๋ฅผ ํ๋๋ ๋ฉ์๋ ๋งค๊ฐ๋ณ์๋ก ์ฃผ์ ํ ๋ ์ฌ์ฉํ๋ค.
Entity Manager๋ JPA์์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ๊ด๋ฆฌํ๋ฏ๋ก ํธ๋์ญ์ ๊ด๋ฆฌ, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉ(CRUD) ๋ฑ์ ์ง์ํ๋ค.
// UserRepository๋ก์ JpaRepository๋ UserRepositoryCustom์ ์์๋ฐ๋๋ค.
// (๋ ๋ค ์ธํฐํ์ด์ค!!)
@Repository
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
}
package com.springboot.querydsltutorial.repository;
import com.springboot.querydsltutorial.entity.User;
import java.util.List;
// ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ ๋ฉ์๋๋ฅผ ์ ์ธํด์ค๋ค.
public interface UserRepositoryCustom {
List<User> findUsersByName(String name);
List<User> findUsersByNameAndEmail(String name, String email);
}
// ์ฐ๋ฆฌ๋ ํด๋น CustomRepository๋ฅผ ์ด์ฉํด์ QueryDsl์ ์ฌ์ฉํ ์ ์๊ฒ ๋๊ณ , ๋์ ์ฟผ๋ฆฌ ๋ํ ๋ง๋ค ์ ์๋ค.
package com.springboot.querydsltutorial.repository;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.springboot.querydsltutorial.entity.QPost;
import com.springboot.querydsltutorial.entity.QUser;
import com.springboot.querydsltutorial.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
import static com.springboot.querydsltutorial.entity.QPost.post;
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<User> findUsersByName(String name) {
// RequestParam์ผ๋ก ๋ฐ์์จ ์ด๋ฆ์ ์กฐ๊ฑด์์ ์ฌ์ฉํด์ ํด๋น ์กฐ๊ฑด์ ๋ง๋ ์ํฐํฐ ๊ฐ์ ธ์ค๊ธฐ
QUser user = QUser.user;
QPost post = QPost.post;
return queryFactory.select(user).from(user)
.leftJoin(user.posts, post).fetchJoin()
.where(user.name.eq(name))
.fetch();
}
@Override
public List<User> findUsersByNameAndEmail(String name, String email) {
QUser user = QUser.user;
QPost post = QPost.post;
BooleanBuilder builder = new BooleanBuilder();
// BooleanBuilder๋ ๋ค์ค ์กฐ๊ฑด ์ฒ๋ฆฌ๋ฅผ ํ ๋, '()'๊ฐ ๋ค์ด๊ฐ๋ ์ํฉ์์ ์ข๊ณ
// BooleanExpression์ ๋จ์ํ ์ฟผ๋ฆฌ๋ ๋จ์ผ ์กฐ๊ฑด์ผ ๋
if (name != null && !name.isEmpty()) {
builder.and(user.name.eq(name));
}
if( email != null && !email.isEmpty()) {
builder.and(user.email.eq(email));
}
return queryFactory.selectFrom(user)
.leftJoin(user.posts, post).fetchJoin()
.where(builder)
.fetch();
}
}
BooleanBuilder
๋ฅผ ์ฌ์ฉํ๋ค. ์ฆselectFrom()
๋ถํฐ ์์ํ๋ค. ๋ง์ฝ ์ ์ฒด๊ฐ ์๋๋ผ ์ผ๋ถ๋ง ์กฐํํ๊ณ ์ถ๋ค๋ฉด .select().from()
๊ณผ ๊ฐ์ด ๋ถ๋ฆฌํด์ ์์ฑํ๋ฉด ๋๋ค. ๋ํ, select()์ select(a,b,c)
์ ๊ฐ์ด ์กฐํํ๊ณ ์ถ์ ์ผ๋ถ๋ง ๋ฝ์๋ผ ์ ์๋ค. fetch()
๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.fetchOne()
, ์ฌ๋ฌ ๊ฑด์ ์กฐํ ๊ฒฐ๊ณผ ์ค 1๊ฑด ๋ฐํ์ fetchFirst()
, ์กฐํ ๊ฒฐ๊ณผ์ ๊ฐ์๋ fetchCount()
, ์กฐํ ๊ฒฐ๊ณผ ๋ฆฌ์คํธ์ ๊ฐ์๋ฅผ ํฌํจํ ๊ฒ์ fetchResults()
๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.BooleanBuilder
์ BooleanExpression
์ ์ฐจ์ด์ BooleanBuilder
- ๊ฐ๋ณ ๊ฐ์ฒด์ด๋ฉฐ, ์กฐ๊ฑด์ ๋์ ์ผ๋ก ์ถ๊ฐํ๊ฑฐ๋ ์ ๊ฑฐํ ์ ์๋ค๋ ์ ์ด ์กด์ฌํ๋ค. ๊ทธ๋ฌ๋ฏ๋ก ์ํฉ์ ๋ฐ๋ผ ์กฐ๊ฑด์ ์กฐํฉํด์ผํ๋ ๋์ ์ฟผ๋ฆฌ์ ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
BooleanExpression
- ๋ถ๋ณ ๊ฐ์ฒด์ด๋ฉฐ, ํ๋ฒ ์ค์ ๋ ์กฐ๊ฑด์ ๋ณ๊ฒฝํ ์ ์๋ค๋ ์ ์ด ์กด์ฌํ๋ค. ๊ทธ๋ฌ๋ฏ๋ก ์๋ก์ด ์กฐ๊ฑด์ด ์ถ๊ฐ๋๋ฉด ์๋ก์ด BooleanExpression
์ ์์ฑํด์ผ๋๋ค.
package com.springboot.querydsltutorial.service;
import com.springboot.querydsltutorial.dto.UserResponseDto;
import com.springboot.querydsltutorial.entity.User;
import com.springboot.querydsltutorial.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public List<UserResponseDto> findUsersByName(String name) {
List<User> userList = userRepository.findUsersByName(name);
List<UserResponseDto> userResponseDtoList = userList.stream().map(UserResponseDto::of).collect(Collectors.toList());
return userResponseDtoList;
}
@Transactional(readOnly = true)
public List<UserResponseDto> findUsersByNameAndEmail(String name, String email) {
List<User> users = userRepository.findUsersByNameAndEmail(name, email);
List<UserResponseDto> userResponseDtoList = users.stream().map(UserResponseDto::of).collect(Collectors.toList());
return userResponseDtoList;
}
}
Service๋จ์์๋ ๊ทธ๋ฅ UserRepository์ ๊ฐ์ด JpaRepository์ CustomRepository (ํ์ฌ ์ฝ๋์์์๋ UserRepository)๋ฅผ ์์๋ฐ์ ์ธํฐํ์ด์ค๋ฅผ DI๋ฐ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.
package com.springboot.querydsltutorial.controller;
import com.springboot.querydsltutorial.dto.UserResponseDto;
import com.springboot.querydsltutorial.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users")
public List<UserResponseDto> getUsersByName(@RequestParam("name") String name) {
return userService.findUsersByName(name);
}
@GetMapping("/users/search")
public List<UserResponseDto> getUsersByNameAndEmail(@RequestParam(required = false) String name,
@RequestParam(required = false) String email) {
return userService.findUsersByNameAndEmail(name, email);
}
// /users/search : ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ค.
// /users/search?name=์๋
: ์ด๋ฆ์ด ์๋
์ธ ์ฌ์ฉ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐํํ๋ค.
// /users/search?email=hello@example.com : ์ด๋ฉ์ผ์ด hello@example.com์ธ ์ฌ์ฉ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐํํ๋ค.
// /users/search?name=์๋
&email=hello@example.com : ์ด๋ฆ์ด "์๋
"์ด๊ณ ์ด๋ฉ์ผ์ด "hello@example.com"์ธ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ค.
}
/users?name=์๋
: ์ด๋ฆ์ด ์๋
์ธ ์ฌ์ฉ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐํํ๋ค.
/users/search?name=์๋
: ์ด๋ฆ์ด ์๋
์ธ ์ฌ์ฉ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐํํ๋ค.
/users/search?email=email@naver.com : ์ด๋ฉ์ผ์ด email@naver.com์ธ ์ฌ์ฉ์์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐํํ๋ค.
/users/search?name=์๋
&email=email@naver.com : ์ด๋ฆ์ด "์๋
"์ด๊ณ ์ด๋ฉ์ผ์ด "email@naver.com"์ธ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ค.
์ฆ ๊ฐ๋จํ๊ฒ ์ ๋ฆฌํ์๋ฉด ๋์ ์ผ๋ก ์กฐ๊ฑด๋ฌธ์ ์ฃผ๊ณ ์ถ์๋, ๋ฐํ์ ์๋ฌ๋ฅผ ๋ฐฉ์งํ๊ณ ์ถ์๋ ์ฌ์ฉํ๋ค.
(Ex. ํํฐ๋ฅผ ๊ฑธ์ด์ ๊ฒ์์ ํ๋ ๊ฒฝ์ฐ)