์คํธ๋ง ์ฟผ๋ฆฌ(String Query)๋ JPA ์์ JPQL ์ด๋ SQL ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์์ด๋ก ์ง์ ์์ฑํ๋ ๊ฒ์ ๋งํ๋ฉฐ
๋ณดํต @Query ์ด๋
ธํ
์ด์
์ ํตํด ์ฌ์ฉ๋๋ค.
๋ณต์กํ ์กฐ๊ฑด์ ๋ช
์์ ์ผ๋ก ๋ค๋ฃฐ ์ ์์ด ๋น ๋ฅด๊ฒ ๊ตฌํํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง ์คํ๋ ๋ฌธ๋ฒ ์ค๋ฅ๊ฐ
์ปดํ์ผ ํ์์ ๊ฒ์ถ๋์ง ์์ ๋ฐํ์ ์ค๋ฅ๋ก ์ด์ด์ง ์ ์๋ค๋ ์น๋ช
์ ์ธ ๋จ์ ์ด ์๋ค.
๋ํ ๋ฌธ์์ด๋ก ์กฐ๊ฑด์ ์กฐํฉํด์ผ ํ๋ฏ๋ก ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ๋ณต์กํ๊ณ ๋ถํธํ๋ค๋ ํ๊ณ๋ ์กด์ฌํ๋ค.
public List<Task> searchTasks(String title, String content, String status) {
//์กฐ๊ฑด๋ฌธ์ ์ถ๊ฐํ ๊ธฐ๋ณธ jpql
String jpql = "select t from Task t";
//์กฐ๊ฑด๋ฌธ์ ๋ด์ ๋ฆฌ์คํธ
List<String> conditionList = new ArrayList<>();
if(StringUtils.hasText(title)) {
conditionList.add("t.title like concat('%', :title, '%')"); //์์ผ๋์นด๋ ํฌํจ
}
if(StringUtils.hasText(content)) {
conditionList.add("t.content like concat('%', :content, '%')");
}
if(StringUtils.hasText(status)) {
conditionList.add("t.status = :status");
}
//์กฐ๊ฑด๋ฆฌ์คํธ๊ฐ ํ๋๋ผ๋ ์๋ค๋ฉด where + conditionList.get(0) + and + .get(1) ...
if(!conditionList.isEmpty()) {
jpql += " where " + String.join(" and ", conditionList);
}
//์์ฑ๋ jpql ์ ๊ธฐ๋ฐ์ผ๋ก ์ฟผ๋ฆฌ ๊ฐ์ฒด ์์ฑ
TypedQuery<Task> query = em.createQuery(jpql, Task.class);
//ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ
if(StringUtils.hasText(title)) {
query.setParameter("title", title); //์์ผ๋์นด๋ ํฌํจ
}
if(StringUtils.hasText(content)) {
query.setParameter("content", content);
}
if(StringUtils.hasText(status)) {
//์ด๋ํ์
๋ณํ ๋ฉ์๋ ์๋ฌธ์๋ ๊ฐ๋ฅ
Status fromStatus = Status.from(status);
query.setParameter("status", fromStatus);
}
System.out.println("JPQL: " + jpql);
//๊ฒฐ๊ณผ ๋ฆฌ์คํธ ๋ฐํ
return query.getResultList();
}
3๊ฐ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋์ ์ฟผ๋ฆฌ์์๋ ๋ถ๊ตฌํ๊ณ ์ฝ๋๊ฐ ๊ฝค ๋ณต์กํ๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
์กฐ๊ฑด์ด ๋์ด๊ฐ์๋ก ์ฝ๋์ ๊ฐ๋
์ฑ์ ๊ธ๊ฒฉํ๊ฒ ๋จ์ด์ง๊ณ ๊ฐ ์กฐ๊ฑด์ ์ผ์ผ์ด ๊ฒ์ฌํ๊ณ ๋ฌธ์์ด์ ์กฐ๋ฆฝํ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๊ณผ์ ์ด ๋ฒ๊ฑฐ๋กญ๊ณ ์ค์๊ฐ ๋ฐ์ํ๊ธฐ ์ฝ๋ค.
์ด์ฒ๋ผ ๋จ์ํ ์กฐ๊ฑด์ด๋ผ๋ ๋ก์ง์ด ๋ณต์กํด์ง๋ฉด ๊ด๋ฆฌ๊ฐ ์ด๋ ค์์ง๊ธฐ ๋๋ฌธ์ ์ค๋ฌด์์๋ ์ฝ๋์ ์์ ์ฑ๊ณผ ๊ฐ๋
์ฑ, ์ ์ง๋ณด์์ฑ์ ์ํด QueryDSL ๊ฐ์ ๋๊ตฌ๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ค.
๋ค์ด๊ฐ๊ธฐ์ ์์ QueryDSL์ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๋ด์ฅ๋ ๊ธฐ๋ฅ์ด ์๋๋ค.
๋ฐ๋ผ์ QueryDsl์ ์ฌ์ฉํ๊ธฐ ์ํด์ ์ ํ๋์ด์ผ ํ ์์
๋ค์ด ์๋ค.
๋จผ์ ์ถ๊ฐํด์ค ์์กด์ฑ์ ์๋์ ๊ฐ๋ค.
//QueryDSL ์ถ๊ฐ
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
- implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
- QueryDSL ์์ JPA๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ์ด ์์กด์ฑ์ ํตํด
JPAQueryFactory,Qํด๋์ค๋ฑ Qํด๋์ค๋ฅผ ํ์ฉํ ์ฟผ๋ฆฌ ์์ฑ์ด ๊ฐ๋ฅ- annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
- APT: Annotation Processing Tool (์์ค์ฝ๋ ์์ฑ๋๊ตฌ)
@Entity๊ฐ ๋ถ์ ํด๋์ค๋ค์ ๋ถ์ํด์Qํด๋์ค๋ฅผ ์๋ ์์ฑํด์ฃผ๋ ๋๊ตฌ- annotationProcessor "jakarta.annotation:jakarta.annotation-api"
- QueryDSL APT๊ฐ ์๋ํ ๋ ํ์ํ ์๋ฐ ์ด๋ ธํ ์ด์ ์ฒ๋ฆฌ๊ด๋ จ API
@Generated,@Nullable,@PostConstruct๊ฐ์ ์ด๋ ธํ ์ด์ ์ ์ ์๊ฐ ๋ค์ด ์์- annotationProcessor "jakarta.persistence:jakarta.persistence-api"
- JPA ์ด๋ ธํ ์ด์ ์ ์(
@Entity,@Id,@Column)๋ฅผ ์ ๊ณต- QueryDsl ์ด ์ํฐํฐ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฑ์ ๋ณด๊ณ Qํด๋์ค๋ฅผ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ํด๋น API๊ฐ ํ์
์์กด์ฑ ์ถ๊ฐํ
compileJava ์คํ ์ build/generated/.../Qํด๋์ค ์๋์ผ๋ก ์์ฑQueryDSL์ ํ์ฉํ๊ธฐ ์ํด์๋ JPAQueryFactory ๊ฐ์ฒด๊ฐ ํ์ํ๋ค.
์ด๋ฅผ ์ํด ๋ณ๋์ ์ค์ ํด๋์ค๋ฅผ ์์ฑํ๊ณ EntityManager๋ฅผ ์ฃผ์
๋ฐ์ JPAQueryFactory๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๋ค.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
์ด๋ ๊ฒ ๋ฑ๋ก๋ JPAQueryFactory๋ Spring Bean์ผ๋ก ๊ด๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ์๋น์ค๋ ์ปค์คํ
๋ฆฌํฌ์งํ ๋ฆฌ ํด๋์ค์์ ๋ฐ๋ก ์ฃผ์
๋ฐ์ QueryDSL ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
QueryDsl์์๋ JPA์ ๊ธฐ๋ณธ ํ๋ก์ ์ (SELECT new ํจํค์ง๋ช ...)๊ณผ ๋ค๋ฅด๊ฒ ๋ค์ํ ๋ฐฉ์์ผ๋ก DTO์ ์ง์ ๋ฐ์ดํฐ๋ฅผ ๋งคํํ ์ ์๋ค.
@Getter
public class TaskSearchRequestDto {
private String title;
private String content;
private String status;
public TaskSearchRequestDto(String title, String content, String status) {
this.title = title;
this.content = content;
this.status = status;
}
}
์ด์ฒ๋ผ ์์ฑ์๋ฅผ ์ ์ํด๋๊ณ ๋ค์๊ณผ ๊ฐ์ด ํ๋ก์ ์ ์ด ๊ฐ๋ฅํ๋ค.
.select(Projections.constructor(TaskSearchRequestDto.class, task.title, task.content, task.status))
์์ฑ์์ @QueryProjection ์ด๋
ธํ
์ด์
์ ๋ถ์ฌ์ค ๋ค compileJava๋ฅผ ์คํํ๋ฉด ํด๋น DTO์ ๋ํ Qํด๋์ค๊ฐ ํจ๊ป ์์ฑ๋๋ค.
ํด๋น ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉ์ ๋ค์ํ ์ฅ์ ์ ์ป์ ์ ์๋ค.
- ํ์ ์์ ์ฑ: ์ปดํ์ผ ํ์์ ์ค๋ฅ๋ฅผ ์ก์์ค(ํ๋ ๋๋ฝ, ์๋ชป๋ ์์ ๋ฑ)
- ๋ฆฌํฉํ ๋ง ์์ : ์์ฑ์ ๋ณ๊ฒฝ ์ ์ปดํ์ผ ์๋ฌ๋ก ์ฆ์ ํใ ๋ ๊ฐ๋ฅ
- ๊น๋ํ ๋ฌธ๋ฒ: Projections.constructor(...) ๋ณด๋ค ์ง๊ด์ ์ด๊ณ ์งง์
.select(new QTaskSearchRequestDto(task.title, task.content, task.status))ํ์ง๋ง DTO๊น์ง Qํด๋์ค๊ฐ ์์ฑ๋จ์ ๋ฐ๋ผ QueryDSL์ ๋ํ ์์กด๋๊ฐ ๋๋ฌด ๋์์ง๊ฒ ๋๋ค.
๋ชจ๋ํ๋ ๊ณ์ธต ๋ถ๋ฆฌ๋ฅผ ์ค์ํ๋ ๊ตฌ์กฐ์์๋ ์ฌ์ฉ์ ๋ํด ๊ณ ๋ คํด๋ณผ ํ์๊ฐ ์๋ค.
QueryDsl์์ .where() ์ ์์ ์ฌ๋ฌ๊ฐ์ ์กฐ๊ฑด์ ๋์ดํด์ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ์กฐ๊ฑด๋ค์ด null์ธ์ง ์ฌ๋ถ์ ์๊ด์์ด ํ ๋ฒ์ ์ฒ๋ฆฌํ ์ ์๋ค๋ ์ฅ์ ์ ๊ฐ์ง๋ค.
@Repository
@RequiredArgsConstructor
public class TaskCustomRepositoryImpl implements TaskCustomRepository{
private final JPAQueryFactory queryFactory;
@Override
public List<Task> searchTasksByQueryDsl(String title, String content, String status) {
return queryFactory
.selectFrom(task)
.where(titleContains(title), contentContains(content), statusEq(status))
.fetch();
}
private BooleanExpression titleContains(String title) {
return title != null ? task.title.contains(title) : null;
}
private BooleanExpression contentContains(String content) {
return content != null ? task.content.contains(content) : null;
}
private BooleanExpression statusEq(String status) {
return status != null ? task.status.eq(Status.from(status)) : null;
}
}
.where() ์ ์์ BooleanExpression ์ ๋ฐํํ๋ ๋ฉ์๋๋ฅผ ๋์ดํด์ ์ฌ์ฉํ๋ค.
ํด๋น ๋ฉ์๋๋ค์ ํ๋ผ๋ฏธํฐ์ ์ ๋ฌ๋ ๊ฐ์ด ์์ ๊ฒฝ์ฐ ๋ฌธ์์ด ์กฐ๊ฑด์ ๋ง๋ ๊ฐ์ ๋ฐํํ๋ค.
queryDsl์ ์กฐ๊ฑด์ null์ด ํฌํจ๋ ๊ฒฝ์ฐ ์กฐ๊ฑด์ ๋ฌด์ํ๋ฏ๋ก ์ค๋ฅ ์์ด ๋์ ์ฟผ๋ฆฌ๊ฐ ์์ฑ๋๋ค.
private BooleanExpression titleContains(String title) {
//์ผํญ ์ฐ์ฐ์๋ก ๊ฐ๊ฒฐํ๊ฒ ํํ
return title != null ? task.title.contains(title) : null;
}
์์ฒ๋ผ BooleanExpression ๋ฉ์๋๋ง ์ ์ํด ๋์ผ๋ฉด ์ฟผ๋ฆฌ๋ฌธ์ด ๊น๋ํด์ง๊ณ ์ฌ์ฌ์ฉ๋ ์ฉ์ดํด์ง๋ค.
BooleanExpression ๋ฐฉ์ ์ธ์๋ ๋ณด๋ค ๋ณต์กํ ์กฐ๊ฑด์ ์ธ ๋ BooleanBuilder๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์๋ ์๋ค.
๋ค์ ๊ธฐํ์ BooleanBuilder๋ฅผ ํตํด ์กฐ๊ธ ๋ ๋ณต์กํ ์ฟผ๋ฆฌ๋ฌธ๋ ์์ฑํด ๋ณผ ์์ ์ด๋ค.
์ถ๊ฐ๋ก QueryDSL ๋ฌธ์์ด ์กฐ๊ฑด ๋ฉ์๋์ ๋ํด ์ ๋ฆฌํ ํ๋ฅผ ์ฒจ๋ถํ๋ฉฐ ๋ง๋ฌด๋ฆฌํ๊ฒ ๋ค.
| ๋ฉ์๋ | SQL ๋ณํ | ์ค๋ช |
|---|---|---|
eq("abc") | = 'abc' | ์ ํํ ์ผ์น |
ne("abc") | != 'abc' | ์ผ์นํ์ง ์์ |
contains("abc") | LIKE '%abc%' | ํน์ ๋ฌธ์์ด ํฌํจ |
startsWith("abc") | LIKE 'abc%' | ํน์ ๋ฌธ์์ด๋ก ์์ |
endsWith("abc") | LIKE '%abc' | ํน์ ๋ฌธ์์ด๋ก ๋๋จ |
like("%abc%") | LIKE '%abc%' | ์์ผ๋์นด๋ ์ง์ ์ง์ |
in(list) | IN (...) | ์ฌ๋ฌ ๊ฐ ์ค ํฌํจ |
isNull() | IS NULL | ๊ฐ์ด null ์ธ ๊ฒฝ์ฐ |
isNotNull() | IS NOT NULL | null ์ด ์๋ ๊ฒฝ์ฐ |