Person(Entity)

@Entity
class Person(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,

    @Column(name = "name")
    var name: String? = null,

    @Column(name = "age")
    var age: Int? = null,

    @Column(name = "job")
    var job: String? = null
)

PersonRepository, PersonCustomRepostiory

interface PersonRepository: JpaRepository<Person, Long>, PersonCustomRepository {
    // JPQL 예제용
    @Query("select p.id from Person p where p.age between :from and :to")
    fun findIdByAgeBetween(@Param("from") from: Int, @Param("to") to: Int)
}

interface PersonCustomRepository {
    fun findPeople(): MutableList<Person>
}

PersonCustomRepositoryImpl

import com.example.querydsl.domain.QPerson.*

@Repository
@Transactional
class PersonCustomRepositoryImpl(
    private val jpaQueryFactory: JPAQueryFactory
): PersonCustomRepository {

    override fun findPeople(): MutableList<Person> {
        return jpaQueryFactory
            .selectFrom(person)
            .fetch()
    }
}

Custom 키워드를 사용해 Repostiory를 만들었는데 이는 Spring Data JPA도 사용하고 Querydsl 둘 다 사용할 때 보통 이 구조로 구성한다. 이 구조는 Spring Data JPA 공식 문서에 나온다.

Querydsl Repository, Spring Data JPA Repository 두 개가 분리되어 있고 서로 상속/구현 관계가 맺어져 있지 않으면 두 개의 Repository가 필요한 Service 파일에 의존성을 둘 다 주입해야 한다.

하지만 위 구조로 구성한다면 하나의 Repository를 의존성 주입(DI)해도 두 가지 Repository 기능을 사용할 수 있다.

JpaQueryFactory를 사용해 쿼리를 구현한다.

위 예제에서 personQPerson.person인데 QPerson.*static import해서 person으로 사용할 수 있다.
.selectFrom(person).select(person).from(person)과 같다.
.selectFrom(person)은 Person 엔티티의 모든 컬럼을 조회하는 것이고, selectfrom을 분리해서 쓰면 select의 인자에 우리가 조회할 컬럼 하나 또는 필요한 컬럼들을 DTO로 변환해 조회할 수 있다. DTO로 변환하는 방법은 나중에 따로 게시글을 작성할 예정이다.

(Kotlin 프로젝트 기준)
Q파일이 생성되지 않았다면 gradle의 compileKotlin을 실행해서 생성시키자.

위 예제에서 findPerson() 메서드는 Person 엔티티의 모든 사람들을 조회하는 메서드다. 따라서 리턴 타입을 MutableList<Person>으로 받았다. 참고로 Kotlin에서는 앞에 Mutable이 붙은 컬렉션은 mutable 하고 앞에 Mutable이 붙지 않은 컬렉션은 immutable 하다. 위 예제에서 List나 MutableList나 둘 중 아무거나 사용해도 무방하다.

결과 조회

  • fetch()는 리스트 조회할 때 사용되고 데이터가 없으면 빈 리스트를 반환한다.

  • fetchOne()은 한 건을 조회할 때 사용되고 데이터가 없으면 null을 반환한다. 한 건 조회인데 결과가 두 개 이상이라면 com.querydsl.core.NonUniqueResultException을 발생시킨다.

이외에도 fetchFirst(), fetchResults(), fetchCount()가 있다.

  • fetchFirst()는 첫 번째 한 건을 조회할 때 사용된다.

  • fetchResults()는 페이징 정보를 포함하고 total count 쿼리를 추가로 실행한다.

  • fetchCount()는 count 쿼리로 변경해서 count 수를 조회한다.

하지만 fetchResults()fetchCount()는 deprecated 되었다.

테스트 코드를 만들어 위 예제를 테스트 해보자.

@SpringBootTest
class Test {

    @Autowired
    private lateinit var personRepository: PersonRepository

    @Test
    fun findPeopleTest() {
        // given
        val person1 = Person(1, "person1", 20, "backend-programmer")
        val person2 = Person(2, "person1", 25, "frontend-programmer")

        // when
        personRepository.saveAll(mutableListOf(person1, person2))

        // then
        assertThat(personRepository.findPeople().size).isEqualTo(2)
        assertThat(personRepository.findPeople().first().id).isEqualTo(person1.id)
        assertThat(personRepository.findPeople().first().name).isEqualTo(person1.name)
        assertThat(personRepository.findPeople().first().age).isEqualTo(person1.age)
        assertThat(personRepository.findPeople().first().job).isEqualTo(person1.job)
    }
}

querydsl2

조건 (where)

위 예제는 조건 없이 조회해서 코드에서 where절을 볼 수 없었다.

where절도 간단하다.

override fun findPeopleByAge(age: Int): MutableList<Person> {
        return jpaQueryFactory
            .selectFrom(person)
            .where(person.age.eq(20))
            .fetch()
    }

나이가 20살인 사람들을 조회하는 쿼리다. eq() 메서드를 사용해 인자값과 같은 사람을 조회한다. 테스트 코드로 확인해보자.

@Test
fun findPeopleByAgeTest() {
    // given
    val person1 = Person(1, "person1", 20, "backend-programmer")
    val person2 = Person(2, "person1", 25, "frontend-programmer")

    // when
    personRepository.saveAll(mutableListOf(person1, person2))

    // then
    assertThat(personRepository.findPeopleByAge(20).size).isEqualTo(1)
    assertThat(personRepository.findPeopleByAge(20).first().id).isEqualTo(person1.id)
    assertThat(personRepository.findPeopleByAge(20).first().name).isEqualTo(person1.name)
    assertThat(personRepository.findPeopleByAge(20).first().job).isEqualTo(person1.job)
}

querydsl3

eq() 이외에도

  • gt(): 큰지
  • goe(): 크거나 같은지
  • lt(): 작은지
  • loe(): 작거나 같은지
  • between(): 사이 범위에 있는지
  • like(): %를 사용해 해당 위치에 부합하는지
    • person.name.like("김%"): 성이 김씨인지
  • contains(): ~에 포함되었는지
    • like("%문자열%")와 같다.
  • isNull(): null인지
  • isNotNull(): null이 아닌지
  • and(): 그리고
    • 콤마(,)로 대체 가능하다.
    • person.age.eq(20).and(person.job.eq("backend-programmer"))
    • person.age.eq(20), person.job.eq("backend-programmer")
  • or(): 또는

등등… 엄청 많이 있다. 필요한 부분은 검색해보면서 적용해보도록 하자!