[SpringBoot] QueryDSL 사용법

Jae Hun Lee·2023년 3월 24일
0

QueryDSL이란?

  • 자바를 사용하는 개발자들이 SQL문을 작성하지 않고도 데이터베이스 쿼리를 생성할 수 있도록 도와주는 오픈소스 라이브러리입니다.

왜 사용할까?

  • 객체지향적인 방식으로 쿼리를 작성할 수 있게 해주기 때문에, 코드의 가독성이 높아지고 유지보수성이 좋아집니다. 또한, 동적 쿼리를 작성할 때 유용하게 사용될 수 있습니다.
  • 다양한 데이터베이스 시스템과 연동할 수 있으며, JPA, Hibernate, MyBatis 등 다양한 ORM 프레임워크와도 연동이 가능합니다.
  • SQL문을 직접 작성하는 것보다 쿼리 작성에 필요한 시간과 노력을 줄여줄 뿐 아니라, 자바 코드를 통해 안전하고 쉽게 쿼리를 작성할 수 있게 해줍니다.
  • 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있습니다.

예시

  • Spring JPA를 사용할경우
    public interface MemberRepository extends JpaRepository<Member, Long> {
        List<Member> findByAgeGreaterThanEqual(int age);
    }
  • NativeQuery를 사용할경우
    public interface MemberRepository extends JpaRepository<Member, Long> {
        @Query(value = "SELECT * FROM member WHERE age >= ?1", nativeQuery = true)
        List<Member> findByAgeGreaterThanEqualNative(int age);
    }
  • JPQL을 사용할경우
    public interface MemberRepository extends JpaRepository<Member, Long> {
        @Query("SELECT m FROM Member m WHERE m.age >= :age")
        List<Member> findByAgeGreaterThanEqualJPQL(@Param("age") int age);
    }
  • QueryDSL을 사용할경우
    @Repository
    public class MemberCustomRepositoryImpl extends QuerydslRepositorySupport implements MemberCustomRepository {
        public MemberCustomRepositoryImpl() {
            super(Member.class);
        }
    
        @Override
        public List<Member> findByAgeGreaterThanEqual(int age) {
            QMember member = QMember.member;
            return from(member).where(member.age.goe(age)).fetch();
        }
    }

QueryDSL 사용법

Gradle설정

dependencies {
	//QueryDSL을 사용하여 JPA 엔티티를 쉽게 조회하고 조작할 수 있습니다.
	implementation 'com.querydsl:querydsl-jpa'
	//Annotation Processor를 사용하면 컴파일 시점에 QueryDSL이 생성하는 Q객체를 생성할 수 있습니다.
	implementation 'com.querydsl:querydsl-apt'
	//Annotation Processor를 사용하여 Q객체를 생성하기 위해, 컴파일 시점에 JPA 엔티티 클래스를 스캔하고 Q객체를 생성합니다.
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
	//JPA 엔티티를 정의할 때 사용되는 어노테이션들이 포함되어 있습니다.
	annotationProcessor("jakarta.persistence:jakarta.persistence-api")
	//어노테이션을 사용하여 QueryDSL Annotation Processor를 구현할 때 필요합니다.
	annotationProcessor("jakarta.annotation:jakarta.annotation-api")
}

//Annotation Processor가 생성한 Q객체를 저장할 디렉토리 경로를 설정합니다.
def generated='src/main/generated'
//Java 소스 파일의 위치를 지정합니다.
sourceSets {
	//Java 소스 파일의 위치에 Annotation Processor가 생성한 Q객체를 저장하는 디렉토리를 추가합니다.
	main.java.srcDirs += [ generated ]
}
//Java 소스 코드 컴파일 태스크에 대한 설정을 지정합니다.
tasks.withType(JavaCompile) {
  //options.annotationProcessorGeneratedSourcesDirectory 옵션에 Annotation Processor가 생성한 Q객체를 저장하는 디렉토리를 지정합니다.
	options.annotationProcessorGeneratedSourcesDirectory = file(generated)
}

clean.doLast {
	//Annotation Processor가 생성한 Q객체를 저장하는 디렉토리를 삭제합니다.
	file(generated).deleteDir()
}

QueryDsl Config 설정

@Configuration
public class QuerydslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

Q클래스 생성

Gradle 옵션 - Tasks - other - compileJava를 실행시키면 Q클래스 파일이 생성된다.

생성생성된파일

Repository

  • MemberRepository
    public interface MemberRepository extends JpaRepository<Member,Long> {
        List<Member> findByAge(int age);
    }
  • MemberRepositoryCustom
    public interface MemberRepositoryCustom {
        List<Member> findByAgeQueryDsl(int age);
    }
  • MemberRepositoryImpl
    @Repository
    @RequiredArgsConstructor
    public class MemberRepositoryImpl implements MemberRepositoryCustom{
    
        private final JPAQueryFactory jpaQueryFactory;
    
        @Override
        public List<Member> findByAgeQueryDsl(int age) {
            QMember member = QMember.member;
            return jpaQueryFactory.selectFrom(member)
                    .where(member.age.eq(age))
                    .fetch();
        }
    }

문제점

하지만 위처럼 적용할경우 사용을할떄 두가지의 상속을 받아야 한다

		@Autowired
    private MemberRepository memberRepository;
    @Autowired
    private MemberRepositoryCustom memberRepositoryCustom;

이 문제를 해결하기위해서 MemberRepository에 memberRepositoryCustom을 상속받자

그럼 MemberRepository만 의존성 주입을해도 모두 사용 할 수 있다!

  • MemberRepository
public interface MemberRepository extends JpaRepository<Member,Long> ,MemberRepositoryCustom{
    List<Member> findByAge(int age);
}

QueryDSL 동적 쿼리

아래의 코드에선 동적으로 쿼리를 생성하는 예시를 보여준다.

만약 name값이 Null일 경우는 쿼리 조건에 포함되지않는다

public List<Member> searchMembers(String name, String email, LocalDate startDate, LocalDate endDate) {
        QMember member = QMember.member;
        BooleanBuilder builder = new BooleanBuilder();

        if(StringUtils.hasText(name)) {
            builder.and(member.name.contains(name));
        }
        if(StringUtils.hasText(email)) {
            builder.and(member.email.contains(email));
        }
        if(startDate != null) {
            builder.and(member.createdDate.goe(startDate.atStartOfDay()));
        }
        if(endDate != null) {
            builder.and(member.createdDate.lt(endDate.plusDays(1).atStartOfDay()));
        }

        return queryFactory.selectFrom(member).where(builder).fetch();
    }

테스트

테스트 코드를 작성하여 잘 실행이 되는지 확인해보자

아래 코드처럼 진행하면 Spring JPA로 찾은 findByAgefindByAgeQueryDsl 의 결과가 같아

문제없이 assertThat이 통과되는 모습을 확인 할 수 있다.

@SpringBootTest
class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    @Transactional
    public void test(){
        memberRepository.save(new Member("홍길동",10));
        memberRepository.save(new Member("김길동",10));
        memberRepository.save(new Member("오길동",10));
        memberRepository.save(new Member("홍길동",11));

        List<Member> members = memberRepository.findByAge(10);
        List<Member> members2 = memberRepository.findByAgeQueryDsl(10);

        assertThat(members).isEqualTo(members2);
    }
}

참고

Spring Boot Data Jpa 프로젝트에 Querydsl 적용하기

Querydsl: 소개와 사용법

profile
기록을 남깁니다

0개의 댓글