Spring/JPA

[JPA] Querydsl 찍먹해보기

ummchicken 2023. 2. 6. 19:55

Querydsl에 들어가기 전...

 

먼저 

✔️ JPQL이란?

JPQL(Java Persistence Query Language)은 엔티티 객체를 조회하는 객체지향 쿼리다.

→ JPQL을 한마디로 정의하면 객체 지향 SQL.

 

  • @Query 어노테이션 이용.
  • 문법은 SQL과 비슷하고, SQL이 제공하는 기능을 유사하게 지원한다.
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공한다.
  • JPQL은 SQL을 추상화하기 때문에 특정 데이터베이스에 의존하지 않는다.
  • 데이터베이스 방언(Dialect)만 변경하면, JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.
  • SQL보다 간결하다.
  • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원.
  • JPQL은 엔티티 객체를 대상으로 쿼리
  • SQL은 데이터베이스 테이블을 대상으로 쿼리
  • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL, JPQL은 문자, Type-chek 불가능. 따라서 해당 로직 실행 전까지 작동여부 확인 불가하다.

 

 

✔️ JPQL 예를 들어보자

@Query 어노테이션 이용.

게시물 데이터 조회 예제.

 

  • 게시물 데이터 조회 예제
  • 조건 : 게시물 상세 설명을 파라미터로 받아 해당 내용을 게시물 상세 설명에 포함하고 있는 데이터를 조회, 정렬순서는 가격이 높은 순.
@Query("select r from Recipe r where r.recipeDetail like %:recipeDetail% order by r.price desc")
List<Recipe> findByRecipeDetail(@Param("recipeDetail") String recipeDetail);
→ @Query 어노테이션 안에 JPQL로 작성한 쿼리문을 넣어준다.
→ from 뒤에는 엔티티 클래스로 작성한 Recipe를 지정해준다.
→ Recipe로부터 데이터를 select 하겠다는 뜻이다.

→ 파라미터에 @Param 어노테이션을 이용하여, 파라미터로 넘어온 값을 JPQL에 들어갈 변수로 지정해줄 수 있다.
→ 현재는 recipeDetail 변수를 "like % %" 사이에 ":recipeDetail"로 값이 들어가도록 한 것이다.

 

 

 

💡 만약, 기존의 데이터베이스에서 사용하던 쿼리를 그대로 사용해야 할 때는?

→ @Query의 nativeQuery 속성을 사용하면, 기존 쿼리를 그대로 사용할 수 있다.

🚨 하지만 특정 데이터베이스에 종속되는 쿼리문을 사용하기 때문에 데이터베이스에 대해 독립적이라는 장점을 잃어버린다.

 

 

 


 

✔️ QueryDSL이란?

JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API이다.

 

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
  • JPQL 빌더 역할.
  • 소스코드로 SQL문을 문자열이 아닌 코드로 작성하기 때문에, 컴파일 시점에 문법 오류를 찾을 수 있다. (소스 작성 시 오타가 발생하면 개발자에게 오타가 있음을 바로 알려줌. 빨간줄)
  • 고정된 SQL문이 아닌, 조건에 맞게 동적으로 쿼리를 생성할 수 있다. (JPQL은 문자를 계속 더해야 하기 때문에 작성이 힘듦)
  • 자동 완성 등 IDE의 도움을 받을 수 있다.
  • 비슷한 쿼리를 재사용할 수 있다.
  • 제약 조건 조립 및 가독성을 향상시킬 수 있다.
  • 단순하고 쉬움 (난 어려움)
  • 실무 사용 권장

 

 

💡 Querydsl을 통해서 쿼리를 생성할 때, Qdomain 객체를 사용한다.

Ex) QMember : Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스이다.

 

 

 

 

✔️ JPAQuery 데이터 반환 메소드

메소드 기능
List<T> fetch() 조회 결과 리스트 반환
T fetchOne 조회 대상이 1건인 경우, 제네릭으로 지정한 타입 반환
T fetchFirsh() 조회 대상 중 1건만 반환
Long fetchCount() 조회 대상 개수 반환
QueryResult<T> fetchResults() 조회한 리스트와 전체 개수를 포함한 QueryResults 반환

 

 

 

 

✔️ QueryDSL 간단 예제

 

1. QueryDSL - 사용

// JPQL
// select m form Member m where m.age > 18

JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list = query
	.selectFrom(m)
        .where(m.age.gt(18))
        .orderBy(m.name.desc())
        .fetch();

 

 

 

2. QueryDSL - 동적 쿼리

String name = "member";
int age = 9;

QMember m = QMember.member;

BooleanBuilder builder = new BooleanBuilder();
if(name != null) {
	builder.and(m.name.contains(name));
}
if(age != 0) {
	builder.and(m.age.gt(age));
}

List<Member> list = query
	.selectFrom(m)
        .where(builder)
        .fetch();

 

 

 

3. QueryDSL - 조인

JPAQueryFactory query = new JPAQueryFactory(em);
QMember m = QMember.member;
QTeam t = QTeam.team;

List<Member> list = query
        .selectFrom(m)
        .join(m.team, t)
        .where(t.name.eq("teamA"))
        .fetch();

 

 

 

4. QueryDSL - 이것은 자바다!

  • 제약조건 조립 가능
  • 가독성, 재사용

(화면 캡쳐한 거라 화질 구지ㅋ;;)

 

 

 

5. 걍 예제

/**
 * 검색어가 null이 아니면, 게시물명에 해당 검색어가 포함되는 게시물을 조회하는 조건 반환
 *
 * BooleanExpression ; null 반환 시 자동으로 조건절에서 제거됨
 * 단, 모든 조건이 null인 경우 장애 발생
 * */
private BooleanExpression recipeNameLike(String searchQuery) {
    return StringUtils.isEmpty(searchQuery) ? null : QRecipe.recipe.recipeName.like("%" + searchQuery + "%");
}

@Override
public Page<MainItemDto> getMainRecipePage(RecipeSearchDto recipeSearchDto, Pageable pageable) {
    QRecipe recipe = QRecipe.recipe; // Querydsl을 통해 쿼리를 생성하기 위해, 플러그인을 통해 자동으로 생성된 QRecipe 객체를 이용함.
    QRecipeImg recipeImg = QRecipeImg.recipeImg;

    List<MainItemDto> content = queryFactory
            .select(
                    new QMainItemDto( // QMainItemDto의 생성자에 반환할 값 넣어줌. @QueryProjection 덕분에 엔티티 조회 후 DTO 변환 과정 없이 DTO로 바로 조회 가능.
                            recipe.id,
                            recipe.recipeName,
                            recipe.recipeDetail,
                            recipeImg.imgUrl,
                            recipe.price)
            )
            .from(recipeImg)
            .join(recipeImg.recipe, recipe) // RecipeImg와 Recipe를 내부 조인
            .where(recipeImg.repimgYn.eq("Y")) // 대표 이미지만 가져옴
            .where(recipeNameLike(recipeSearchDto.getSearchQuery()))
            .orderBy(recipe.id.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch(); // JPAQuery 메소드 중 하나인 fetch를 이용해서 쿼리 결과를 리스트로 반환한다. fetch() 메소드 실행 시점에 쿼리문이 실행된다.

    long total = queryFactory
            .select(Wildcard.count) // select count(*)
            .from(recipeImg)
            .join(recipeImg.recipe, recipe)
            .where(recipeImg.repimgYn.eq("Y"))
            .where(recipeNameLike(recipeSearchDto.getSearchQuery()))
            .fetchOne()
            ;

    // Page<User> page = new PageImpl<>(users.subList(start, end), pageable, users.size());
    // content의 타입을 바꿔준 list, pageable, 총 데이터 개수
    return new PageImpl<>(content, pageable, total);
}

 

 

 

 

 

 

 

아 어렵네;;

계속 진행 중...

계속 추가 예정.

 

 

 

 

 

 


출처

 

 

 

 

 

'Spring > JPA' 카테고리의 다른 글

[JPA] OSIV란?  (6) 2023.02.07
[JPA] MyBatis와 JPA, 도대체 뭐가 다를까?  (0) 2023.02.06
스프링 데이터 JPA - (2)  (0) 2022.12.20
스프링 데이터 JPA - (1)  (0) 2022.12.19
JPA 활용 2 - API 개발과 성능 최적화  (0) 2022.12.19