QueryDSL

분류없음 2015.09.13 22:12

QueryDSL (Query Domain Specific Language)

Querydsl의 특징

Querydsl의 특징이 가장 잘 설명된 공식 문서의 앞부분입니다.

서론
 
Querydsl 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해 주는 프레임워크다. 문자열로 작성하거나 XML 파일에 쿼리를 작성하는 대신, Querydsl이 제공하는 플루언트(Fluent) API를 이용해서 쿼리를 생성할 수 있다.
 
단순 문자열과 비교해서 Fluent API를 사용할 때의 장점은 다음과 같다.
 
1. IDE의 코드 자동 완성 기능 사용
2. 문법적으로 잘못된 쿼리를 허용하지 않음
3. 도메인 타입과 프로퍼티를 안전하게 참조할 수 있음
4. 도메인 타입의 리팩토링을 더 잘 할 수 있음
 
1. Introduction
 
1.1. Background
 
Querydsl은 타입에 안전한 방식으로 HQL 쿼리를 실행하기 위한 목적으로 만들어졌다. HQL 쿼리를 작성하다보면 String 연결을 이용하게 되고, 이는 결과적으로 읽기 어려운 코드를 만드는 문제를 야기한다. String을 이용해서 도메인 타입과 프로퍼티를 참조하다보면 오타 등으로 잘못된 참조를 하게 될 수 있으며, 이는 String을 이용해서 HQL 작성할 때 발생하는 또 다른 문제다.
 
타입에 안전하도록 도메인 모델을 변경하면 소프트웨어 개발에서 큰 이득을 얻게 된다. 도메인의 변경이 직접적으로 쿼리에 반영되고, 쿼리 작성 과정에서 코드 자동완성 기능을 사용함으로써 쿼리를 더 빠르고 안전하게 만들 수 있게 된다.
 
Querydsl의 최초 쿼리 언어 대상은 Hibernate HQL이었으나, 현재는 JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, 콜렉션 그리고 RDFBean을 지원한다.
 
1.2. 원칙
 
Querydsl의 핵심 원칙은 타입 안정성(Type safety)이다. 도메인 타입의 프로퍼티를 반영해서 생성한 쿼리 타입을 이용해서 쿼리를 작성하게 된다. 또한, 완전히 타입에 안전한 방법으로 함수/메서드 호출이 이루어진다.
 
또 다른 중요한 원칙은 일관성(consistency)이다. 기반 기술에 상관없이 쿼리 경로와 오퍼레이션은 모두 동일하며, Query 인터페이스는 공통의 상위 인터페이스를 갖는다.
 
... (중략)

간단하게 정리하자면자동완성도 되고 compile 단계에서 오류도 잡을 수 있고 이래저래 좋다는 말이네요.

2. Querydsl로 할 수 있는 것들 : Reference document

공식 문서의 목차만 보셔도 querydsl로 어떤 작업을 할 수 있는지 알 수 있습니다일단 대충 눈도장만 찍어주세요.

http://www.querydsl.com/static/querydsl/3.6.3/reference/ko-KR/html_single/
(
한글화된 공식 문서 - 2015-04-23 기준 최신 버전 3.6.3)

서문
1. Introduction
... (생략)
2. 튜토리얼
2.1. JPA 쿼리
2.1.1. 메이븐 통합
2.1.2. Ant 통합
2.1.3. Roo에서 Querydsl JPA 사용하기
2.1.4. hbm.xml 파일에서 쿼리 모델 생성하기
2.1.5. 쿼리 타입 사용하기
2.1.6. 쿼리
2.1.7. 조인
2.1.8. 일반 용법
2.1.9. 정렬
2.1.10. 그룹핑
2.1.11. DeleteClause
2.1.12. UpdateClause
2.1.13. 서브쿼리
2.1.14. 원래의 JPA Query 구하기
2.1.15. JPA 쿼리에서 네이티브 SQL 사용하기
2.2. JDO 쿼리
... (생략)
2.3. SQL 쿼리
... (생략)
2.4. 루신 쿼리
... (생략)
2.5. Hibernate Search 쿼리
... (생략)
2.6. Mongodb 쿼리
... (생략)
2.7. 콜렉션 쿼리
... (생략)
2.8. Scala에서 쿼리하기
... (생략)
3. 일반 사용법
3.1. 쿼리 생성
3.1.1. 복합 조건(complext predicates)
3.1.2. 동적 표현식
3.1.3. 동적 경로
3.1.4. Case 표현식
3.1.5. Casting 표현식
3.1.6. 리터럴 선택
3.2. 결과 처리
3.2.1. 다중 컬럼 리턴
3.2.2. 빈 생성(population)
3.2.3. 생성자 사용
3.2.4. 결과 집합(aggregation)
3.3. 코드 생성
3.3.1. 경로 초기화
3.3.2. 커스터마이징
3.3.3. 커스텀 타입 매핑
3.3.4. 위임 메서드(Delegate methods)
3.3.5. 애노테이션 비적용 타입
3.3.6. 클래스패스 기반 코드 생성
3.3.6.1. 메이븐 사용법
3.3.6.2. Scala 지원
3.4. 별칭 사용법
4. 문제해결
4.1. 불충분한 타입 인자
4.2. 멀티쓰레드 환경에서 Querydsl Q타입의 초기화
4.3. JDK5 사용

사용법을 나타낸 부분 몇 개를 발췌하였습니다역시 눈도장 찍어주세요.

2.1.5. 쿼리 타입 사용하기

Querydsl을 이용해서 쿼리를 작성하려면변수와 Query 구현체를 생성해야 한다먼저 변수부터 시작해보자.

다음과 같은 도메인 타입이 있다고 가정하자.

@Entitypublic class Customer {
    private String firstName;
    private String lastName;
 
    public String getFirstName(){
        return firstName;
    }
 
    public String getLastName(){
        return lastName;
    }
 
    public void setFirstName(String fn){
        firstName = fn;
    }
 
    public void setLastName(String ln) {
        lastName = ln;
    }
}

Querydsl Customer와 동일한 패키지에 QCustomer라는 이름을 가진 쿼리 타입을 생성한다. Querydsl 쿼리에서 Customer타입을 위한 정적 타입 변수로 QCustomer를 사용한다.

QCustomer는 기본 인스턴스 변수를 갖고 있으며다음과 같이 정적 필드로 접근할 수 있다.

QCustomer customer = QCustomer.customer;

다음처럼 Customer 변수를 직접 정의할 수도 있다.

QCustomer customer = new QCustomer("myCustomer");

2.1.6. 쿼리

Querdsl JPA 모듈은 JPA Hibernate API를 모두 지원한다.

JPA API를 사용하려면 다음과 같이 JPAQuery 인스턴스를 사용하면 된다.

// where entityManager is a JPA EntityManager
JPAQuery query = new JPAQuery(entityManager);

Hibernate를 사용한다면, HibernateQuery를 사용하면 된다.

// where session is a Hibernate session
HibernateQuery query = new HibernateQuery(session);

JPAQuery HibernateQuery는 둘 다 JPQLQuery 인터페이스를 구현하고 있다.

firstName 프로퍼티가 Bob Customer를 조회하고 싶다면 다음의 쿼리를 사용하면 된다.

QCustomer customer = QCustomer.customer;
JPAQuery query = new JPAQuery(entityManager);
Customer bob = query.from(customer)
  .where(customer.firstName.eq("Bob"))
  .uniqueResult(customer);

from 메서드는 쿼리 대상(소스)을 지정하고, where 부분은 필터를 정의하고, uniqueResult는 프로젝션을 정의하고, 1개 결과만 리턴하라고 지시한다.

여러 소스로부터 쿼리를 만들고 싶다면 다음처럼 쿼리를 사용한다.

QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer, company);

여러 필터를 사용하는 방법은 다음과 같다.

query.from(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));

또는다음과 같이 해도 된다.

query.from(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

위 코드를 JPQL 쿼리로 작성하면 다음과 같을 것이다.

from Customer as customer
    where customer.firstName = "Bob" and customer.lastName = "Wilson"

필터 조건을 or로 조합하고 싶다면 다음 패턴을 사용한다.

query.from(customer)
    .where(customer.firstName.eq("Bob").or(customer.lastName.eq("Wilson")));

2.1.7. 조인

Querydsl JPQL의 이너 조인조인레프트 조인풀조인을 지원한다조인 역시 타입에 안전하며 다음 패턴에 따라 작성한다.

QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCate kitten = new QCat("kitten");
query.from(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .list(cat);

위 쿼리를 JPQL로 작성하면 다음과 같다.

from Cat as cat
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

다음은 조인을 사용하는 또 다른 예다.

query.from(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))
    .list(cat);

위 코드의 JPQL 버전은 다음과 같다.

from Cat as cat
    left join cat.kittens as kitten
    on kitten.bodyWeight > 10.0

2.1.8. 일반 용법

JPQLQuery 인터페이스의 cascading 메서드는 다음과 같다.

from: 쿼리 소스를 추가한다.

innerJoin, join, leftJoin, fullJoin, on: 조인 부분을 추가한다조인 메서드에서 첫 번째 인자는 조인 소스이고두 번재 인자는 대상(별칭)이다.

where: 쿼리 필터를 추가한다가변인자나 and/or 메서드를 이용해서 필터를 추가한다.

groupBy: 가변인자 형식의 인자를 기준으로 그룹을 추가한다.

having: Predicate 표현식을 이용해서 “group by” 그룹핑의 필터를 추가한다.

orderBy: 정렬 표현식을 이용해서 정렬 순서를 지정한다숫자나 문자열에 대해서는 asc() desc()를 사용하고, OrderSpecifier에 접근하기 위해 다른 비교 표현식을 사용한다.

limit, offset, restrict: 결과의 페이징을 설정한다. limit은 최대 결과 개수, offset은 결과의 시작 행, restrict limit offset을 함께 정의한다.

2.1.9. 정렬

정렬을 위한 구문은 다음과 같다.

QCustomer customer = QCustomer.customer;
query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);

위 코드는 다음의 JPQL과 동일하다.

from Customer as customer
    order by customer.lastName asc, customer.firstName desc

2.1.10. 그룹핑

그룹핑은 다음과 같은 코드로 처리한다.

query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);

동등한 JPQL은 다음과 같다.

select customer.lastName
    from Customer as customer
    group by customer.lastName

2.1.11. DeleteClause

Querydsl JPA에서 DeleteClause는 간단한 delete-where-execute 형태를 취한다다음은 몇 가지 예다.

QCustomer customer = QCustomer.customer;
// delete all customers
new JPADeleteClause(entityManager, customer).execute();
// delete all customers with a level less than 3
new JPADeleteClause(entityManager, customer).where(customer.level.lt(3)).execute();

JPADeleteClause 생성자의 두 번째 파라미터는 삭제할 엔티티 대상이다. where는 필요에 따라 추가할 수 있으며, execute를 실행하면 삭제를 수행하고 삭제된 엔티티의 개수를 리턴한다.

Hibernate 이용시, HibernateDeleteClause를 사용하면 된다.

JPA DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와의 연동되지 않는다.

2.1.12. UpdateClause

Querydsl JPA UpdateClause은 간단한 update-set/where-execute 형태를 취한다다음은 몇 가지 예다.

QCustomer customer = QCustomer.customer;
// rename customers named Bob to Bobby
new JPAUpdateClause(session, customer).where(customer.name.eq("Bob"))
    .set(customer.name, "Bobby")
    .execute();

JPAUpdateClause 생성자의 두 번째 파라미터는 수정할 엔티티 대상이다. set SQL update 스타일로 프로퍼티 수정을 정의하고, execute를 실행하면 수정을 실행하고 수정된 엔티티의 개수를 리턴한다.

Hibernate 이용시, HibernateUpdateClause를 사용한다.

JPA에서 DML 절은 JPA 레벨의 영속성 전파 규칙을 따르지 않고, 2차 레벨 캐시와 연동되지 않는다.

2.1.13. 서브쿼리

서브쿼리를 만들려면 JPASubQuery를 사용하면 된다서브쿼리를 만들기 위해 from 메서드로 쿼리 파라미터를 정의하고, unique list를 이용한다. unique는 단일 결과를 위해 사용하고 list는 리스트 결과를 위해 사용한다서브쿼리도 쿼리처럼 타입에 안전한 Querydsl 표현식이다.

QDepartment department = QDepartment.department;
QDepartment d = new QDepartment("d");
query.from(department)
    .where(department.employees.size().eq(
        new JPASubQuery().from(d).unique(d.employees.size().max())
     )).list(department);

다른 예제

QEmployee employee = QEmployee.employee;
QEmployee e = new QEmployee("e");
query.from(employee)
    .where(employee.weeklyhours.gt(
        new JPASubQuery().from(employee.department.employees, e)
        .where(e.manager.eq(employee.manager))
        .unique(e.weeklyhours.avg())
    )).list(employee);

Hibernate를 사용할 경우, HibernateSubQuery를 사용하면 된다.

3.1.4. Case 표현식

case-when-then-else 표현식을 만들 땐다음과 같이 CaseBuilder 클래스를 사용한다.

QCustomer customer = QCustomer.customer;
Expression<String> cases = new CaseBuilder()
    .when(customer.annualSpending.gt(10000)).then("Premier")
    .when(customer.annualSpending.gt(5000)).then("Gold")
    .when(customer.annualSpending.gt(2000)).then("Silver")
    .otherwise("Bronze");
// The cases expression can now be used in a projection or condition

equals-operations을 가진 case 표현식은 다음과 같이 단순한 형태를 사용하면 된다.

QCustomer customer = QCustomer.customer;
Expression<String> cases = customer.annualSpending
    .when(10000).then("Premier")
    .when(5000).then("Gold")
    .when(2000).then("Silver")
    .otherwise("Bronze");
// The cases expression can now be used in a projection or condition

JDOQL에서는 아직 Case 표현식을 지원하지 않는다.

3. 어떻게 쓰는거야Querydsl

Querydsl Spring Data JPA 와 결합하여 사용하는 예제를 보여 드리도록 하겠습니다일단 두 가지만 기억해 주시면 될 것 같은데요,

  • QueryDslPredicateExecutor : JPA Repository  Querydsl Predicate를 결합하여 사용합니다.
  • QueryDslRepositorySupport : service layer Querydsl을 침투시키지 않고 사용하려면 persistence layer에서QueryDslRepositorySupport extends 하여 사용합니다.
  • 둘 다 같이 쓸 수도 있습니다.

다음과 같은 조건으로 대상을 가져와야 한다고 가정하겠습니다.

  • 쿼리 조건 : 나이 20‘김’씨 성을 가진 회원을 가져와라.

1.1. QueryDslPredecateExecutor 예시

QueryDslPredicateExecutor extends한 후서비스에서 Predicate 를 생성하여 사용하면 됩니다.

CustomerRepository.java

public interface CustomerRepository extends QueryDslPredicateExecutor<Customer>, JpaRepository<Customer, Long> {
 
}

CustomerService.java

public class CustomerService {
    @Autowired
    private CustomerRepository customerRepository;
    private QCustomer customer = QCustomer.customer;
 
    public Iterable<Customer> findByQuerydsl() {
        BooleanBuilder predicate = new BooleanBuilder();
        predicate.and(customer.age.between(20, 29));
        predicate.and(customer.username.like("%"));
        return customerRepository.findAll(predicate);
    }
}

1.2. QueryDslRepositorySupport 예시

이 경우에는 CustomerRepositoryCustom 인터페이스를 도출하여두 개의 인터페이스를 같이 상속받아 사용해야 합니다.

CustomerRepository.java

public interface CustomerRepository extends JpaRepository<Customer, Long>, CustomerRepositoryCustom {
 
}

CustomerRepositoryCustom.java

public interface CustomerRepositoryCustom {
    public Iterable<Customer> findByQuerydsl();
}

CustomerRepositoryCustomImpl.java

public class CustomerRepositoryImpl extends QueryDslRepositorySupport implements CustomerRepositoryCustom {
    private QCustomer customer = QCustomer.customer;
 
    public CustomerRepositoryImpl() {
        super(Customer.class);
    }
 
    @Override
    public Iterable<Customer> findByQuerydsl() {
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(customer.age.between(20, 29));
        builder.and(customer.username.like("%"));
 
        return from(customer).where(builder).list(customer);
    }
}

쿼리 조건에 다음과 같은 조건이 추가 된다면?

  • 쿼리 조건 : 나이 20‘김’씨 성을 가진 회원을 나이 많은 순으로 3을 가져와라.

각각 다음 부분이 변경됩니다.

2.1. QueryDslPredecateExecutor 의 경우
CustomerService.java의 다음 부분이 수정됩니다.

    public Iterable<Customer> findByQuerydsl() {
        BooleanBuilder predicate = new BooleanBuilder();
        predicate.and(customer.age.between(20, 29));
        predicate.and(customer.username.like("%"));
        // 수정된 부분
        return customerRepository.findAll(predicate, new PageRequest(0, 3, new Sort(Sort.Direction.DESC, customer.age.getMetadata().getName())));
    }

2.2. QueryDslRepositorySupport 의 경우
CustomerRepositoryImpl.java의 다음 부분이 수정됩니다.

    @Override
    public Iterable<Customer> findByQuerydsl() {
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(customer.age.between(20, 29));
        builder.and(customer.username.like("%"));
        // 수정된 부분
        return from(customer).where(builder).orderBy(customer.age.desc()).limit(3).list(customer);
    }

3. 따라해보기

아주 잠깐 시간을 내셔서 다음 과정을 따라해보시면‘나도 QueryDsl 한 번 써봤다’ 하실 수 있습니다(?)
앞서 설명한 2가지 케이스를 모두 보여주는 예제이며, CustomerRepository.findByQuerydsl() CustomerService.findByQuerydsl() 을 구현해 보겠습니다.

File>New>Project

Spring>Spring Starter Project 선택

Java Version : 1.8
Name, Artifact, Package Name : 아무거나

Finish

생성된 프로젝트

pom.xml 편집
querydsl.version 추가

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <start-class>springstarterweb.SpringstarterwebApplication</start-class>
    <java.version>1.8</java.version>
    <querydsl.version>3.6.3</querydsl.version>
</properties>

dependency 추가 (모두 추가)

  • querydsl-apt, querydsl-jpa : QueryDsl 관련 dependency
  • spring-boot-starter-data-jpa : Spring Data JPA
  • hsqldb : 메모리DB를 사용
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>
<dependency>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>${querydsl.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>${querydsl.version}</version>
</dependency>

plugin 추가 (모두 추가)

<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.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

수정된 pom.xml 전문입니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.test</groupId>
    <artifactId>querydsldemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>querydsldemo</name>
    <description>Demo QueryDsl project for Spring Boot</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>querydsldemo.QuerydsldemoApplication</start-class>
        <java.version>1.8</java.version>
        <querydsl.version>3.6.3</querydsl.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <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.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

여기까지 하셨으면 Maven -> Update Project 를 한 번 해줍니다.
 
data.sql 파일 추가 (이 파일을 추가함으로써 테이블 및 데이터가 자동 생성됩니다.)

data.sql 내용

INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn0', '이성근', 30, '1990-01-01 00:00:00.000', '2010-01-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn1', '이강준', 29, '1991-02-02 00:00:00.000', '2010-02-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn2', '김승화', 28, '1992-03-03 00:00:00.000', '2011-01-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn3', '허성욱', 31, '1993-04-04 00:00:00.000', '2011-02-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn4', '최종경', 31, '1994-05-05 00:00:00.000', '2012-01-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn5', '서석교', 28, '1995-06-06 00:00:00.000', '2012-02-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn6', '윤은정', 27, '1996-07-07 00:00:00.000', '2013-01-01 00:00:00.000');
INSERT INTO customer(userid, username, age, created_at, lastvisited_at) VALUES ('rainn7', '김태형', 26, '1997-08-08 00:00:00.000', '2013-01-01 00:00:00.000');

다음 그림의 패키지와 파일을 순서대로 만들어 갈 것입니다.

Customer.java : Customer Entity

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long customerSrl;
 
    private String userid;
    private String username;
    private int age;
    private Date createdAt;
    private Date lastvisitedAt;
 
    public Long getCustomerSrl() {
        return customerSrl;
    }
    public String getUserid() {
        return userid;
    }
    public void setUserid(String userid) {
        this.userid = userid;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
    public Date getLastvisitedAt() {
        return lastvisitedAt;
    }
    public void setLastvisitedAt(Date lastvisitedAt) {
        this.lastvisitedAt = lastvisitedAt;
    }
}

- 이 부분 잘 읽어 주세요.

entity를 만들었으면 QCustomer Class 를 만들기 위해

Run As>Maven clean
Run As>Maven install 를 하신 후,
target/generated-sources/java 폴더에 QCustomer.java 파일이 만들어졌는지 확인합니다.
확인 후에, Maven>Update Project 를 실행하시면 target/generated-source/java폴더가 자동으로 리소스로 등록되며, QCustomer Class를 사용할 수 있게 됩니다.

CustomerRepositoryCustom.java : QueryDslRepositorySupport를 사용하기 위한 interface

package querydsldemo.customer;
 
import java.util.List;
 
public interface CustomerRepositoryCustom {
    public List<Customer> findByQuerydsl();
}

CustomerRepositoryImpl.java : CustomRepositoryCustom 인터페이스 구현체

package querydsldemo.customer;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
 
import com.mysema.query.BooleanBuilder;
 
public class CustomerRepositoryImpl extends QueryDslRepositorySupport implements CustomerRepositoryCustom {
    private QCustomer customer = QCustomer.customer;
 
    public CustomerRepositoryImpl() {
        super(Customer.class);
    }
 
    @Override
    public List<Customer> findByQuerydsl() {
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(customer.age.between(20, 29));
        builder.and(customer.username.like("김%"));
 
        return from(customer).where(builder).orderBy(customer.age.desc()).limit(3).list(customer);
    }
}

CustomerRepository.java : CustomerRepository interface

package querydsldemo.customer;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.stereotype.Repository;
 
@Repository
public interface CustomerRepository extends QueryDslPredicateExecutor<Customer>, JpaRepository<Customer, Long>, CustomerRepositoryCustom {
 
}

CustomerService.java : CustomerService

package querydsldemo.customer;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
 
import com.mysema.query.BooleanBuilder;
 
@Servicepublic class CustomerService {
    @Autowired
    private CustomerRepository customerRepository;
    private QCustomer customer = QCustomer.customer;
 
    public List<Customer> findByQuerydsl() {
        BooleanBuilder predicate = new BooleanBuilder();
        predicate.and(customer.age.between(20, 29));
        predicate.and(customer.username.like("김%"));
        return customerRepository.findAll(predicate, new PageRequest(0, 3, new Sort(Sort.Direction.DESC, customer.age.getMetadata().getName()))).getContent();
    }
}

자, 이제 필요한 파일은 모두 만들었습니다. 다음은 unit test를 통해 잘 동작하는지 확인해 보겠습니다.

- 예상 결과는 : [0] : 김승화(age:28), [1] : 김태형(age:26) 입니다.

src/test/java/querydsldemo/QuerydsldemoApplicationTests.java 파일에 테스트 메서드를 추가해보겠습니다.

package querydsldemo;
 
import java.util.List;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import querydsldemo.customer.Customer;
import querydsldemo.customer.CustomerRepository;
import querydsldemo.customer.CustomerService;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = QuerydsldemoApplication.class)
public class QuerydsldemoApplicationTests {
    @Autowired
    private CustomerService customerService;
    @Autowired
    private CustomerRepository customerRepository;
 
    @Test
    public void testRepositoryFindByQuerydsl() {
        checkSizeAndCompareUserNameOfFirstElement(customerRepository.findByQuerydsl());
    }
 
    @Test
    public void testServiceFindByQuerydsl() {
        checkSizeAndCompareUserNameOfFirstElement(customerService.findByQuerydsl());
    }
 
    private void checkSizeAndCompareUserNameOfFirstElement(List<Customer> result) {
        Assert.assertTrue(result.size() > 0);
        final String expectedUsername = "김승화";
        Assert.assertTrue(expectedUsername.equals(result.get(0).getUsername()));
    }
}

JUnit 실행 결과

마치며…

Querydsl을 간단하게 소개해 드렸습니다. 많은 내용을 함축하고 있어서, 쉽게 보시기 어려우실지도 모르겠습니다.
JPARepository 와 결합하여 사용하는 예를 보여드렸지만 JPARepository에 대한 설명은 생략하였습니다. 
(간단 명료하게 설명할 능력이 안됩니다.. ㅠㅠ 사실 이만큼 작성하는 것도 엄청난 일이네요.)
제 지식이 짧아서 얼마나 도움이 되셨는지 모르겠습니다.
읽어주셔서 감사합니다.



Posted by @RAINN

Killing Unterminated Tomcat Process (Windows)

H:\Users\rainn>netstat -ona | findstr 8080
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING       4224
  TCP    [::]:8080              [::]:0                 LISTENING       4224

H:\Users\rainn>taskkill /F /PID 4224
성공: 프로세스(PID 4224)가 종료되었습니다.

Tomcat,강제,종료

Posted by @RAINN

Spring Boot : SpringLoaded (Hot Swap)

- tomcat 재시작 없이 변경 사항을 적용시켜 봐요. (일부 제한적)

Spring Boot로 개발을 진행하다 보면 수정 사항을 적용하고, application을 재시작하는 과정이 느리게 느껴질 때가 많습니다. 그런데 이미 hot-swap 이라는 개념으로 java에서도 수정된 부분만을 바로 적용할 수 있는 기술들이 있었네요. JREBEL같은 상용 소프트웨어를 써보신 분은 알겠지만, 무척이나 편한 기능이지요.

제가 “일부 제한적”이라는 표현을 쓴 이유는, 모든 소스의 변경이 바로 적용되는지는 확인을 하지 못했기 때문입니다. 하지만 method 추가, method 내에서의 변경은 잘 되는 것을 확인했습니다.

위 링크에서는 Spring Boot에서 어떻게 hot-swapping을 적용할 수 있는지 나와있습니다. SpringLoaded라는 프로젝트를 사용하여 적용하는 방법이 나와 있구요,

1. Get SpringLoaded

2. Configure Eclipse STS

-javaagent:"<path-to>/springloaded-1.2.1.RELEASE.jar" -noverify

<path-to>jar파일이 위치한 곳입니다.

위 옵션을 Run As1로 실행했을 때와, tomcat2을 사용하여 실행했을 때, 각각에 맞는 부분에 넣어주면 됩니다.

1 Run As로 실행했을 때 적용하는 방법

프로젝트 우클릭 → Run As → Run Configurations… → Java Application → <project_name> → Arguments → VM arguments 텍스트 박스에 추가

2 tomcat으로 실행했을 때 적용하는 방법

프로젝트 우클릭 → Run As → Run Configurations… → Apache Tomcat → <Tomcat v7.0 Server at localhost> → Arguments → VM arguments 텍스트 박스에 추가

설정이 끝난 후에는 enjoy! 하라고 하네요.

3. Configure with Maven Plug-in

만약, 프로젝트 실행을 embedded-tomcat을 사용할 수 있다면, maven-plugin을 사용하여 각각의 개발자가 옵션을 추가하지 않고도 적용할 수 있는 방법이 있습니다.

아래, pom.xml을 참고하여 플러그인을 추가한 후, Run as → maven build… 로 들어가서 Goals에 spring-boot:run 으로 실행시키면 됩니다.

3-1. pom.xml
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
            <version>1.2.1.RELEASE</version>
        </dependency>
    </dependencies>
</plugin>
3-2. Run As → maven buld… → Goals : spring-boot:run

(Run As → Spring Boot App로 실행하면 적용되지 않습니다.)

4. Reload Thymeleaf, FreeMarker, Groovy, Velocity templates without restarting the container

이 부분 역시 맨 위에 소개한 링크에 잘 소개되어 있습니다.

spring.thymeleaf.cache=false
spring.freemarker.cache=false
spring.groovy.template.cache=false
spring.velocity.cache=false

refernces

http://docs.spring.io/spring-boot/docs/current/reference/html/howto-hotswapping.html
https://github.com/spring-projects/spring-loaded
http://blog.netgloo.com/2014/05/21/hot-swapping-in-spring-boot-with-eclipse-sts/
http://reiphiel.tistory.com/89

Spring,Boot,SpringLoaded,HotSwap,ReloadWithoutRestart

Posted by @RAINN