안녕하세요! 이번 포스팅에서는 SpringBoot의 QueryDSL을 이용해서 동적쿼리(Dynamic SQL)를 사용해보도록 하겠습니다.
모든 코드는 Github에서 확인이 가능합니다.
📚 사전 준비
프로젝트 설명
여러 더미 데이터들을 넣고 동적으로 조건절을 생성하는 프로젝트를 작성해보겠습니다.
개발 환경
- IntelliJ Ultimate
- Gradle 6.0.1 / Maven
- Java 1.8
- SpringBoot 2.2.2
- Window
🔎 구현
1. 의존성 추가
가장 먼저 QueryDSL을 사용하기 위해 필요한 의존성을 추가해줍니다.
build.gradle
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.querydsl:querydsl-jpa'
}
def querydslDir = "$buildDir/generated/querydsl"
/* Querydsl Q-class 추가 코드 */
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
의존성 추가를 한 후에
Gradle - Tasks - build - build를 실행시켜줍니다.
Maven인 경우에는 아래와 같이 설정합니다.
pom.xml
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.1.3</version>
</dependency>
...
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.1.3</version>
</dependency>
</dependencies>
</plugin>
Maven - Lifecycle- compile을 실행시켜줍니다.
2. Entity 만들기
Person.java
@Getter
@NoArgsConstructor
@Entity
public class Person {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Integer id;
@Column(nullable=false)
private String firstName;
@Column(nullable=false)
private String lastName;
@Column(nullable=false)
private Integer age;
@Column(unique=true)
private String email;
public Person(String firstName, String lastName, int age, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.email = email;
}
}
Entity를 만들어줍니다.
Entity Person의 구성은 아래와 같습니다.
- id(Primary Key) : 고유값
- firstName, lastName : 이름
- age : 나이
- email : 이메일
3. Predicate 만들기
다음은 Predicate를 만들어줍니다. Predicate를 사용하면 동적으로 조건절을 만들 수 있습니다.
PersonPredicate.java
public class PersonPredicate {
public static Predicate search(String firstName, String lastName){
QPerson person = QPerson.person;
BooleanBuilder builder = new BooleanBuilder();
if(firstName != null){
builder.and(person.firstName.eq(firstName));
}
if(lastName != null){
builder.and(person.lastName.eq(lastName));
}
return builder;
}
}
위의 예시 같은 경우에는 firstName과 lastName을 받아
null이 아닌 경우에 입력받은 값과 같다는 조건을 **Q-class(쿼리용 클래스)**를 사용하여 추가합니다.
예시를 몇 개 들어보겠습니다.
- search("라면", "신")
- firstName이 '라면'이고 lastName이 '신'인 것을 찾음
- WHERE firstName = '라면' AND lastName = '신'
- search("라면", null)
- firstName이 '라면'인 것을 찾음
- WHERE firstName = '라면'
- search(null, null)
- 조건문을 추가하지 않음
4. Repository 만들기
PersonRepository.java
public interface PersonRepository extends JpaRepository<Person, Integer>, QuerydslPredicateExecutor<Person> {
}
Repository는 원래 JPA를 사용하여 만드는 방식으로 JpaRepository를 상속 받아 구현합니다.
하지만 Querydsl을 사용할 경우에는 QuerydslPredicateExecutor를 추가로 상속받아야 합니다.
📄 테스트
이제 구현은 끝났으니 기능이 잘 동작하는지 테스트하기 위해 PersonRepositoryTest를 생성합니다.
0. 테스트 전 데이터 삽입
PersonRepositoryTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {
@Autowired
private PersonRepository repository;
@Before
public void cleanUp() throws Exception {
repository.deleteAll();
repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
repository.save(new Person("프링", "스", 41, "spring@bo.com"));
repository.save(new Person("자바", "김", 38, "imjava@java.net"));
repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
repository.save(new Person("고당", "최", 76, "best@good.com"));
repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
}
}
Querydsl을 테스트해보기 위해 필요한 더미 데이터들을 넣어주는 메소드를 작성합니다. 테스트 전에 실행해야 하므로 @Before 어노테이션을 사용합니다.
1. 모든 리스트 가져오기
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {
@Autowired
private PersonRepository repository;
@Before
public void cleanUp() throws Exception {
repository.deleteAll();
repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
repository.save(new Person("프링", "스", 41, "spring@bo.com"));
repository.save(new Person("자바", "김", 38, "imjava@java.net"));
repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
repository.save(new Person("고당", "최", 76, "best@good.com"));
repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
}
/* 추가 */
@Test
public void 모든_리스트_가져오기() throws Exception {
List<Person> personList = repository.findAll();
assertThat(personList.size()).isEqualTo(7); // 총 갯수
}
}
첫 번째 테스트는 모든 리스트를 가져오는 테스트입니다. findAll()을 이용해 모든 리스트를 가져온 후, 제가 아까 넣어준 더미 데이터 갯수(7개)와 같은지 비교합니다.
2. 김씨 성 찾기 (queryFactory 사용)
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {
@Autowired
private PersonRepository repository;
/* 추가 */
@PersistenceContext // 영속성 객체를 자동으로 삽입해줌
private EntityManager em;
@Before
public void cleanUp() throws Exception {
repository.deleteAll();
repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
repository.save(new Person("프링", "스", 41, "spring@bo.com"));
repository.save(new Person("자바", "김", 38, "imjava@java.net"));
repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
repository.save(new Person("고당", "최", 76, "best@good.com"));
repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
}
@Test
public void 모든_리스트_가져오기() throws Exception {
List<Person> personList = (List<Person>) repository.findAll();
assertThat(personList.size()).isEqualTo(7); // 총 갯수
}
/* 추가 */
@Test
public void 김씨_성_찾기() throws Exception {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QPerson person = QPerson.person;
long result = queryFactory.selectFrom(person).where(person.lastName.eq("김")).fetchCount(); // 성이 김씨인 사람 찾기
List<Person> result2 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.email.like("%.com")).fetch(); // 성이 김씨이고 메일 뒤가 com으로 끝나는 사람 찾기
QueryResults<Person> result3 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.age.between(20,40)).fetchResults(); // 성이 김씨이고 나이가 20~40세 사이인 사람 찾기
assertThat(result).isEqualTo(3);
assertThat(result2.get(0).getFirstName()).isEqualTo("철수");
assertThat(result3.getTotal()).isEqualTo(2);
}
}
두 번째 테스트는 JpaQueryFactory를 사용한 테스트입니다. JPAQueryFactory를 사용하면 동적으로 조건절을 만들 수 있습니다. selectFrom()을 이용해서 조회할 객체(Q-class)를 정해주고, where()로 원하는 조건문을 만들어주면 됩니다.
결과를 반환하는 메소드들은 아래와 같습니다.
- fetch() : 조회 결과가 여러 건일 때 사용
- fetchOne() : 조회 결과가 1건만 있을 때 사용 (1건 이상이면 NonUniqueResultException 발생)
- fetchFirst() : 무조건 첫 번째건 반환
- fetchResults() : 조회 결과 + 개수 반환
- fetchCount() : 조회 결과의 개수 반환
아래같이 코드를 분리하여 동적으로 사용도 가능합니다.
JPAQuery<Person> query = queryFactory.selectFrom(person);
// 입력받은 이름이 김이면 김씨 성을 가진 이름을, 아니면 최를 가진 이름의 리스트를 리턴
if(lastName.equals("김")){
query = query.where(person.lastName.eq("김"));
}
else{
query = query.where(person.lastName.eq("최"));
}
long result = query.fetchCount();
3. 스선화 찾기 (Predicate 사용)
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTest {
@Autowired
private PersonRepository repository;
@PersistenceContext
private EntityManager em; // 영속성 객체를 자동으로 삽입해줌
@Before
public void cleanUp() throws Exception {
repository.deleteAll();
repository.save(new Person("철수", "김", 22, "kim@chulsu.com"));
repository.save(new Person("차차", "김", 15, "cha@cha.cha"));
repository.save(new Person("프링", "스", 41, "spring@bo.com"));
repository.save(new Person("자바", "김", 38, "imjava@java.net"));
repository.save(new Person("쿼리", "최", 19, "choi@que.com"));
repository.save(new Person("고당", "최", 76, "best@good.com"));
repository.save(new Person("선화", "스", 11, "flower@cha.cha"));
}
@Test
public void 모든_리스트_가져오기() throws Exception {
List<Person> personList = (List<Person>) repository.findAll();
assertThat(personList.size()).isEqualTo(7); // 총 갯수
}
@Test
public void 김씨_성_찾기() throws Exception {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QPerson person = QPerson.person;
String lastName = "김";
JPAQuery<Person> query = queryFactory.selectFrom(person);
if(lastName.equals("김")){
query = query.where(person.lastName.eq("김"));
}
else{
query = query.where(person.lastName.eq("최"));
}
long result = query.fetchCount();
//long result = queryFactory.selectFrom(person).where(person.lastName.eq("김")).fetchCount(); // 성이 김씨인 사람 찾기
List<Person> result2 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.email.like("%.com")).fetch(); // 성이 김씨이고 메일 뒤가 com으로 끝나는 사람 찾기
QueryResults<Person> result3 = queryFactory.selectFrom(person).where(person.lastName.eq("김"), person.age.between(20,40)).fetchResults(); // 성이 김씨이고 나이가 20~40세 사이인 사람 찾기
assertThat(result).isEqualTo(3);
assertThat(result2.get(0).getFirstName()).isEqualTo("철수");
assertThat(result3.getTotal()).isEqualTo(2);
}
/* 추가 */
@Test
public void 스선화_찾기() throws Exception {
List<Person> result = (List<Person>) repository.findAll(PersonPredicate.search("선화", "스")); // 성이 스이고 이름이 선화인 사람 찾기
List<Person> result2 = (List<Person>) repository.findAll(PersonPredicate.search(null, "스")); // 성이 스씨인 사람 찾기
List<Person> result3 = (List<Person>) repository.findAll(PersonPredicate.search(null, null));
assertThat(result.size()).isEqualTo(1);
assertThat(result2.size()).isEqualTo(2);
assertThat(result3.size()).isEqualTo(7);
}
}
세 번째 테스트는 Predicate를 사용한 테스트입니다.
아까 Repository에서 JpaRepository뿐만 아니라 QuerydslPredicateExecutor를 상속받은걸 기억하시나요? QuerydslPredicateExecutor를 상속받음으로 인해 findAll() 안에 아까 만든 Predicate를 넣을 수 있게 되었습니다. (구현 - 3. Predicate 만들기에서 만든 코드입니다!) 따라서 Predicat에서 동적으로 생성한 조건절을 넣어줄 수 있습니다.
❤️ 정리
이런 식으로 Querydsl을 이용해 동적 쿼리를 생성하는 방법을 알아봤습니다. 정말 많은 삽질을 한 부분이라서 조금이라도 도움이 되었으면 하는 마음으로 포스팅을 했는데 도움이 되었는지 모르겠네요 ㅠㅠ 혹시 위 글에서 잘못된 내용이나 보충하고 싶은 내용을 알려주시면 감사하겠습니다!
읽어주셔서 감사합니다 :)
😇 참고 링크
'Backend > SpringBoot' 카테고리의 다른 글
SpringBoot의 AOP을 이용해서 로그 남기기 (1) | 2020.03.23 |
---|---|
SpringBoot의 Jsoup을 이용해 코로나 현황 크롤링하기 프로젝트 (3) | 2020.03.08 |
SpringBoot의 ControllerAdvice를 이용하여 JSON 형태로 예외처리하기 (0) | 2020.02.18 |
SpringBoot의 MockMvc를 사용하여 GET, POST 응답 테스트하기 (6) | 2020.01.31 |
SpringBoot의 @IdClass를 사용해서 복합 Primary Key 적용하기 (0) | 2020.01.22 |