Skip to content

Latest commit

 

History

History
590 lines (383 loc) · 14.8 KB

queryDsl.asc

File metadata and controls

590 lines (383 loc) · 14.8 KB

객체지향 쿼리 소개

객체와 관계형 데이터베이스를 매핑해주는 ORM(Object Relational Mapping)프레임워크의
기술 표준인 JPA(Java Persistent API)를 사용하는 쿼리 언어의 종류와 기술에 대해서 소개한다.

객체지향 쿼리란게 뭐지

ORM으로 개발할시 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발을 하는데, 단순한 엔티티 하나를 조회하는 경우가 아닌 복잡한 검색 방법도 필요하다. 이럴때 모든 엔티티를 메모리에 올려두고 에플리케이션에서 검색필터를 하는것은 현실성이 없다. 이런 엔티티 객체를 대상으로 검색을 용의하게 할수 있도록 도와주는게 이번에 설명할 객체지향 쿼리이다.

  • JPQL

  • Criteria

  • QueryDSL

  • Native SQL

.

.

.

JPQL (Java Persistence Query language)

JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공해 준다. JPQL은 가장 중요한 객체지향 쿼리 언어로 앞으로 소개할 Criteria, QueryDsl은 결국 JPQL을 편리하게 사용하도록 돕는 빌더 클래스일 뿐이다. JPA를 다루는 개발자라면 JPQL은 필수로 학습해야 한다.

SQL :  데이터베이스 중심 쿼리
JPQL : 엔티티 객체를 중심으로 하는 객체지향 쿼리

JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한후 엔티티 겍체를 생성해서 반환한다.

일반적인 조회

[JPQL]
 SELECT m FROM Member m WHERE m.age > 30

[실행된 SQL]
 SELECT
  m.id ,
  m.name ,
  m.phone_num ,
  m.type ,
  .
  .
  .
 FROM member m
 WHERE m > 30
  • JPQL 키워드는 대소문자 구분 안함

  • 엔티티와 속성은 대소문자를 구분하며, 테이블명이 아닌 엔티티명을 사용한다.

  • 별칭은 필수적으로 사용한다.

페이징 API

JPA는 페이징 처리를 아래 두 API로 추상화 하였다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)

  • setMaxResults(int maxResult) : 조회할 데이터 수

string jpqlQuery = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpqlQuery , Member.class)
        .setFirstResult(20)
        .setMaxResults(10)
        .getResultList();

각 데이터베이스(HSQLDB,MySQL,PostgreSQL,ORACLE,SQLServer) 방언으로 변환 처리된다.

JPQL 내부조인(inner join)

[JPQL]
SELECT m , d
FROM Member m JOIN o.Department d
WHERE m.name = '근우'

[실행된 SQL]
SELECT
  m.id ,
  m.phone_num ,
  m.type ,
  m.name ,
  d.department_id ,
  .
  .
  .
FROM member m, department d
ON m.id = d.department_id
WHERE m.name = '근우'

JPQL 외부조인(Outer join)

[JPQL]
SELECT m , d
FROM Menber m LEFT JOIN m.Department d

[실행된 SQL]
SELECT
  m.id, m.name, m.type, d.address_id, d.department_id, d.department_name
FROM menber m LEFT OUTER JOIN department d
ON m.id = d.department_id


// jpa 2.1버전부터 outer joinon절을 사용하여 조건을 추가하는 기능이 추가되었다.
// 조인 대상을 필터링한 후 조인을 할수 있다.

[JPQL]
SELECT m , d
FROM Menber m LEFT JOIN m.Department d
    ON d.department_name LIKE 'NTS%'

[실행된 SQL]
SELECT
  m.* , d.*
FROM member m LEFT OUTER JOIN department d
    ON ((m.id = d.department_id) and (d.department_name like 'NTS%'))

JPQL 세타조인

WHERE절을 사용하면 전혀 관계없는 엔티티도 조인이 가능하다.

// 회원 이름과 팀 이름이 똑같은 사람들의 수를 구하는 예

[JPQL]
select
  count(m)
from
  Member m , Team t
where m.username = t.name

[실행된 SQL]
SELECT count(m.id)
FROM
  member m CROSS JOIN team t
WHERE
  m.username = t.name

세타조인은 내부 조인만 사용 가능합니다.

JPQL 페치조인(Fetch join)

  • sql의 조인이 아닌 jpql의 성능 최적화를 위해 제공되는 기능

  • 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능

[JPQL]
SELECT e
FROM Employee e JOIN FETCH e.address
//여기서 e.address 가 경로식(fetch)이다.

[실행된 SQL]
SELECT e.*, a.*
FROM Employee e JOIN e.address a

SELECT e로 Employee테이블의 엔티티만 조회 하였지만, 실행된 sql에서는 e. * , a. * 으로, 연관된 address테이블도 함께 조회가 된것을 볼수 있다.

지연로딩을 설정하였다 하더라도 연관된 address 엔티티는 프록시가 아닌 실제 엔티티이므로, 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할수 있다.

1 1
Figure 1. 퍼시스턴트 객체의 영속성 생명주기 출처:(http://www.slideshare.net/)

개발자님 이건 기억해 주세요!!!

JPQL은 SQL보다 코드가 간결하다. 충분한 선행 학습을 거치지 않고 개발하면 이런 결과를 초래할수도 있다.

[JPQL]
select o.member.team
from Order o
where o.product.name = 'A' and o.address.city = 'seoul'

[실행된 SQL]
select t.*
from Order o
inner join Member m on o.member_id=m.id
inner join Team t on o.team=t.id
inner join Product p on o.product_id=p.id
where p.name = 'A' and o.city = 'seoul'

개발자 입장에서 본 JPQL 은…​.

1) 이식성이 좋다.
2) query를 문자로 처리하기 때문에 오타와 같은 오류에 취약하다.
3) 동적 쿼리 작성이 힘들다.

.

.

.

Criteria

  • JPQL 생성하는 빌더 클래스 API

  • 런타임 시점에 오류가 발생하지 않고, 컴파일 시점에 오류 발견

  • 문자기반의 JPQL보다 동적 쿼리를 안전하게 생성 가능

일반적인 조회

[기존 sql]
SELECT s.*
FROM SimpleBean s

[JPQL]
Query query = entityManager.createQuery("select s from SimpleBean s");
List<SimpleBean> list = query.getResultList();

[Criteria]
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<SimpleBean> from = criteriaQuery.from(SimpleBean.class);
CriteriaQuery<Object> select = criteriaQuery.select(from);
TypedQuery<Object> typedQuery = entityManager.createQuery(select);
List<Object> resultList = typedQuery.getResultList();
assertEqualsList(list, resultList);
1)EntityManager나 EntityManagerFactory에서 CriteriaBuilder를 얻은후
2)getCriteriaBuilder()를 통해  createQuery 이후 반환타입 지정.
3)form절 생성 : 이때 반환된 SimpleBean값은 조회의 시작점 이라는 의미로 쿼리루트라 함.
4)select절을 생성. 이후 WHERE 같은 필터 추가는 JPQL과 같음.

join query

[기존 sql]
select s.*
from OrderItem s join product p
where s.product = p.category and p.category='cat';

[JPQL]
long category=200L;
Query query = entityManager.createQuery("select s from OrderItem s " +
        "where s.product.category=:cat");
query.setParameter("cat", category);
List<OrderItem> list = query.getResultList();

[Criteria]
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<OrderItem> from = criteriaQuery.from(OrderItem.class);
Path<Object> path = from.join("product").get("category");

CriteriaQuery<Object> select = criteriaQuery.select(from);
select.where(criteriaBuilder.equal(path, category));

TypedQuery<Object> typedQuery = entityManager.createQuery(select);
List<Object> resultList = typedQuery.getResultList();

fetch join query

[기존 sql]
select s.*
from OrderItem s join product p
where s.product = p.category and p.category='cat';

[JPQL로 풀어보면]
long category=200L;
Query query = entityManager.createQuery("select s from OrderItem s " +
        "join fetch s.product where s.product.category=:cat");
query.setParameter("cat", category);
List<OrderItem> list = query.getResultList();


[Criteria SQL]
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<OrderItem> from = criteriaQuery.from(OrderItem.class);
Path<Object> path = from.join("product").get("category");

from.fetch("product"); //FETCH product

CriteriaQuery<Object> select = criteriaQuery.select(from);
select.where(criteriaBuilder.equal(path, category));

TypedQuery<Object> typedQuery = entityManager.createQuery(select);
List<Object> resultList = typedQuery.getResultList();

group by

[ 기존 JPQL ]
select min(s.pint),s.pbyte from SimpleBean s group by s.pbyte

[ Criteria SQL ]
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root from = criteriaQuery.from(SimpleBean.class);

Expression minExpression = criteriaBuilder.min(from.get("pint"));
Path pbytePath = from.get("pbyte");
CriteriaQuery<Object> select = criteriaQuery.multiselect(minExpression, pbytePath);

CriteriaQuery<Object> groupBy = select.groupBy(pbytePath);

TypedQuery<Object> typedQuery = entityManager.createQuery(select);
List listActual = typedQuery.getResultList();

개발자 입장에서 본 Criteria 는…​

.

.

.

.

.

.

.

1 3
Figure 2. 개발자의 좌절하는 소리

너무 복잡하고 어려워서 작성된 코드를 보면 복잡성으로 인해 어떤 JPQL이 생성될지 파악하는게 쉽지 않다.

그래서 복잡한 검색조건을 Spring Data JPA의 Specifcation을 이용하여 검색조건을 모아놓은 클래스를 따로 생성하는 방법도 활용 되고 있다.

아래는 최범균님의 해당 내용을 설명해 놓은 내용이다. http://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage

이 방법을 사용하면 많이 간편해 지긴 하지만, 추가적인 학습이 필요하고, 직관성도 떨어지며 검색조건용 클래스들을 추가로 생성해줘야 하는 번거로움이 있다.

.

.

.

QueryDSL

환경설정

<querydsl.version>3.6.3</querydsl.version>

<dependency>
	<groupId>com.mysema.querydsl</groupId>
	<artifactId>querydsl-apt</artifactId>  //쿼리타입(Q)을 생성할떄 필요한 라이브러리
	<version>${querydsl.version.version}</version>
	<scope>provided</scope>
</dependency>

<dependency>
	<groupId>com.mysema.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>  //querydsl jpa 라이브러리
	<version>${querydsl.version.version}</version>
</dependency>

# 쿼리용 클래스 생성을 위한 코드추가
<build>
	  <plugins>
	    <plugin>
	      <groupId>com.mysema.maven</groupId>
	      <artifactId>apt-maven-plugin</artifactId>
	      <version>1.0.6</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>
1 2
Figure 3. Q 쿼리타입 생성

mvn compile 을 입력하면 outputDirectory에 지정한 target위치에 Q로 시작하는 쿼리타입들이 생성된다. 이클립스 LUNA버전 이상을 사용하면 빌드패스를 따로 지정 안해줘도 상관 없다.

일반적인 조회

1)JPAQuery 인스턴스를 사용
2)사용할 쿼리타입(Q)을 생성한후 생성자에는 별칭을 부여
3)이후 FROM, WHERE, ORDERBY 등은 아래 소스로 확인
QCustomer customer = QCustomer.customer;
JPAQuery query = new JPAQuery(entityManager);
Customer bob = query.from(customer)
  .where(customer.firstName.eq("Bob"))
  .uniqueResult(customer);

조인

QMember m = QMember.member;
QMemberCard mc = QMemberCard.memberCard;

List<Member> list =
    query.form(m)
      .join(m.memberCards,mc)
      .list(m);
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

List<Cat> list =
  query.from(order)
    .join(order.member, member)
    .leftJoin(order.orderItem, orderItem)
    .list(order);

서브쿼리

QCustomer customer = QCustomer.customer;
QCustomer customer2 = new QCustomer("customer2");
query.from(customer).where(
  customer.status.eq(new SQLSubQuery().from(customer2).unique(customer2.status.max()))
  .list(customer.all())

정렬 & 페이징

query.form(item)
    .where(item.title.like("*"))
    .orderBy(item.title.asc(), item.year.desc())
    .offset(10).limit(10)
    .list(item);

아래는 소프트웨어야 놀자에서 사용하는 정렬과 페이징 방법이다.

public Page<AdminContents> getContentsList(int page, int pageSize, Board board, SortType sortType) {
    Sort sort = new Sort(sortType.getDirection(), sortType.getFieldName()).and(new Sort(SortType.DEFAULT_SUB_PIVOT.getDirection(), SortType.DEFAULT_SUB_PIVOT.getFieldName()));
    PageRequest pageRequest = new PageRequest(page, pageSize, sort);

    return adminContentsRepository.findAll(
            QAdminContents.adminContents.board.seq.eq(board.getSeq())
                    .and(QAdminContents.adminContents.statSeq.loe(Status.EXPOSE_PIVOT)), pageRequest);
}

개발자 입장에서 본 QueryDSL…​

1) 문자가 아닌 코드로 안전하게 쿼리 작성가능하다.
2) 복잡한 동적쿼리 생성시 비교적 간단하게 생성 가능하다.

.

.

.

Native SQL

  • SQL을 직접사용 가능

    표준화 되어있지 않은 특정 데이터베이스에서만 동작하는 CONNECT BY나 SQL힌트같은 쿼리 작성시 사용
    Native SQL은 보통 JPQL로 작성하기 어려운 복잡한 SQL쿼리를 작성 하거나 SQL을 최적화해서 데이터베이스 성능을
    향상할 때 사용한다.
//service.java

@Override
public List<PerfTest> getAll(Date start, Date end, String region) {
  return perfTestRepository.findAllByCreatedTimeAndRegion(start, end, region);
}

//repository.java

@Query("select p from PerfTest  p where p.startTime between ?1 and ?2 and region=?3")
List<PerfTest> findAllByCreatedTimeAndRegion(Date start, Date end, String region);

Native SQL을 본 입장

1) 제일 눈에 잘 들어온다.
2) 기본적인 JPQL에서 제공하는 기능 사용가능 하지만, 이식성이 떨어진다.
3) 마지막 최후의 방법이라는 생각.

.

.

.

참조자료 : 자바 ORM 표준 JPA 프로그래밍(김영한)

Copyright © 2016 NHN Technology Service.