객체와 관계형 데이터베이스를 매핑해주는 ORM(Object Relational Mapping)프레임워크의 기술 표준인 JPA(Java Persistent API)를 사용하는 쿼리 언어의 종류와 기술에 대해서 소개한다.
ORM으로 개발할시 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발을 하는데, 단순한 엔티티 하나를 조회하는 경우가 아닌 복잡한 검색 방법도 필요하다. 이럴때 모든 엔티티를 메모리에 올려두고 에플리케이션에서 검색필터를 하는것은 현실성이 없다. 이런 엔티티 객체를 대상으로 검색을 용의하게 할수 있도록 도와주는게 이번에 설명할 객체지향 쿼리이다.
-
JPQL
-
Criteria
-
QueryDSL
-
Native SQL
.
.
.
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 키워드는 대소문자 구분 안함
-
엔티티와 속성은 대소문자를 구분하며, 테이블명이 아닌 엔티티명을 사용한다.
-
별칭은 필수적으로 사용한다.
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]
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]
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 join에 on절을 사용하여 조건을 추가하는 기능이 추가되었다.
// 조인 대상을 필터링한 후 조인을 할수 있다.
[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%'))
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
세타조인은 내부 조인만 사용 가능합니다.
-
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 엔티티는 프록시가 아닌 실제 엔티티이므로, 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할수 있다.
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 생성하는 빌더 클래스 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과 같음.
[기존 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();
[기존 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();
[ 기존 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();
.
.
.
.
.
.
.
너무 복잡하고 어려워서 작성된 코드를 보면 복잡성으로 인해 어떤 JPQL이 생성될지 파악하는게 쉽지 않다.
그래서 복잡한 검색조건을 Spring Data JPA의 Specifcation을 이용하여 검색조건을 모아놓은 클래스를 따로 생성하는 방법도 활용 되고 있다.
아래는 최범균님의 해당 내용을 설명해 놓은 내용이다. http://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage
이 방법을 사용하면 많이 간편해 지긴 하지만, 추가적인 학습이 필요하고, 직관성도 떨어지며 검색조건용 클래스들을 추가로 생성해줘야 하는 번거로움이 있다.
.
.
.
-
JPQL 생성하는 빌더 클래스 API
-
Criteria에 비해 단순하고, 사용하기 편함
-
type-safe : query type 을 생성
-
한글 레퍼런스문서 주소 ( http://www.querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single )
<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>
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);
}
-
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);