(자바,JPA동영상)Querydsl을 이용한 JPQL 쿼리, 서브쿼리, EXISTS, ORDER BY, WHERE, 메소드기반쿼리, 스프링 Data JPA, 자바교육, 스프링교육, JPS교육, 닷넷교육, C#교육, JAVA교육
http://ojc.asia/bbs/board.php?bo_table=LecJpa&wr_id=373
ojc.asia
4장. Querydsl을 이용한 JPQL 쿼리
이번 장에서 Querydsl을 이용한 JPQL 작성방법을 상세하게 살펴보겠습니다.
4.1. 테스트 프로젝트 만들기
"부록 7.1 Querydsl JPA Query with MySQL" 부분을 참고하여 진행합니다.
application.properties
환경설정 파일에서 다음처럼 변경합니다.
spring.datasource.initialize=true
spring.jpa.hibernate.ddl-auto=create
4.2. JPA Query 학습
4.2.1. Select
Q 타입 클래스를 사용하여 메소드 기반으로 쿼리를 작성할 수 있습니다.
1. 기본 사용법
DeptDao.java
package com.example.employee.repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import com.example.employee.model.Dept;
import com.example.employee.model.QDept;
import com.querydsl.jpa.impl.JPAQuery;
@Repository
public class DeptDao {
@PersistenceContext
private EntityManager entityManager;
public Dept getByDname(String dname){
// 쿼리타입 클래스내 static 멤버변수가 가리키는 인스턴스를 그대로 사용할 수 있다.
QDept dept = QDept.dept;
// 별칭을 생성자에 주고 새로 쿼리타입 클래스의 인스턴스를 만들어 사용할 수도 있다.
//QDept department = new QDept("department");
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
Dept accounting = query.select(dept).from(dept)
.where(dept.dname.eq(dname)).fetchOne();
return accounting;
}
}
질의 결과가 하나의 로우라면 fetchOne 메소드를 사용합니다. 얻고자 하는 결과의 자료형에 따라 메소드를 선택합니다.
제네릭이 Long 인 경우 | 제네릭이 엔티티 클래스 Dept 인 경우 |
fetchOne 메소드는 결과가 하나 이상이면 TooManyRowsException 예외가 발생합니다. fetchFirst 메소드는 TooManyRowsException 예외가 발생하지 않고 데이터가 있다면 첫 번째 로우를 리턴합니다. 둘 다 데이터가 없으면 null을 리턴합니다.
DeptDaoTest.java
package com.example.employee.repository;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
import com.example.employee.model.Dept;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DeptDaoTest {
@Autowired
private DeptDao deptDao;
@Test
public void testGetByDname() {
String dname = "ACCOUNTING";
Dept accounting = deptDao.getByDname(dname);
System.out.println(accounting);
assertThat(accounting, is(notNullValue()));
}
}
SQL 로그
콘솔창에 기록된 SQL쿼리를 살펴보겠습니다.
결과 로그에서 앨리어스를 'dept0_'에서 'd'로 바꾸어 보기 편하게 수정했습니다.
select
d.deptno as deptno1_0_,
d.dname as dname2_0_,
d.loc as loc3_0_
from dept d
where d.dname='ACCOUNTING'
2. 1차 캐싱
1차 캐싱 서비스를 제공받는지 테스트해 보겠습니다.
DeptDaoTest.java
클래스에 testEntityManagerCaching 메소드를 직접 추가합니다.
@Transactional
@Test
public void testEntityManagerCaching() {
String dname = "ACCOUNTING";
// 쿼리 수행
Dept accounting1 = deptDao.getDeptByDname(dname);
System.out.println(accounting1);
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~");
// 한번 더 같은 쿼리를 수행
Dept accounting2 = deptDao.getDeptByDname(dname);
System.out.println(accounting2);
// 테이블의 로우(행)에 일치여부는 키 값이 같은지 여부로 판단한다.
// 객체도 그에 맞게 처리하기 위해서 equals, hashCode 메소드를
재정의해서 사용한다.
System.out.println(accounting1.hashCode() == accounting2.hashCode());
System.out.println(accounting1.equals(accounting2));
assertThat(accounting1, equalTo(accounting2));
// true 결과는 두 객체의 메모리 주소가 같다는 것을 의미한다.
System.out.println(accounting1 == accounting2);
assertThat(accounting1, is(accounting2));
assertThat(accounting1, sameInstance(accounting2));
}
EntityManager는 PersistenceContext로 데이터베이스로부터 질의하여 얻은 결과를 담고 있는 엔티티 객체를 보관합니다. 다음에 같은 결과를 요구하는 요청을 받으면 갖고 있는 엔티티 객체를 돌려준다는 것을 확인 할 수 있습니다. 이는 같은 트랜잭션 범위 안에서만 유효합니다.
JPAQuery, JPAQueryFactory 클래스를 이용하면 엔티티매니저가 제공하는 메소드를 직접 이용할 때와 마찬가지로 동일한 기능을 이용할 수 있습니다.
3. 필터 설정방법
DeptDao.java
public List<String> getDeptDnameWhenEmpCountExist(){
QDept dept = QDept.dept;
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
List<String> dnames = query.select(dept.dname).from(dept)
.where(dept.emps.size().gt(0)).fetch();
return dnames;
}
DeptDaoTest.java
@Test
public void testGetDeptDnameWhenEmpCountExist(){
List<String> dnames = deptDao.getDeptDnameWhenEmpCountExist();
for (String dname : dnames) {
System.out.println(dname);
}
assertThat(dnames.size(), is(3));
}
SQL 로그
select
d.dname as col_0_0_
from dept d
where (
select
count(e.deptno)
from emp e
where d.deptno=e.deptno
)>0

where 조건절에 서브쿼리를 사용하여 부서에 배정된 직원이 있는 부서명만 조회합니다. 이 요청을 dept.emps.size().gt(0) 코드처럼 객체를 이용하는 방식 그대로 처리할 수 있습니다.
위 SQL을 exists 구문을 사용하는 것으로 바꾸면 다음과 같습니다.
select dname
from dept
where exists (
select 1 from emp where deptno=dept.deptno)
위 SQL을 사용하도록 요청하는 메소드 기반 쿼리 작성예시는 다음과 같습니다.
public List<String> getDeptDnameWhenEmpCountExist(){
QDept dept = QDept.dept;
QEmp emp = QEmp.emp;
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
List<String> dnames = query.select(dept.dname).from(dept)
.where(JPAExpressions
.select(Expressions.constant(1))
.from(emp).where(emp.dept.deptno.eq(dept.deptno)).exists()).fetch();
return dnames;
}
4. 쿼리 작성용 Methods
JPAQuery 메소드들은 모두 자신이 속한 객체를 리턴하므로 메소드 체이닝기법으로 "."을 찍고 연속해서 메소드를 사용할 수 있습니다. JPAQuery가 구현한 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을 함께 정의한다. |
5. Order by
EmpDao.java
package com.example.employee.repository;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import com.example.employee.model.Emp;
import com.example.employee.model.QEmp;
import com.querydsl.jpa.impl.JPAQuery;
@Repository
public class EmpDao {
@PersistenceContext
private EntityManager entityManager;
public List<Emp> getEmpOrderByDeptnoAscEmpnoDesc(){
QEmp emp = QEmp.emp;
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
List<Emp> emps = query.select(emp).from(emp)
.orderBy(emp.dept.deptno.asc(), emp.empno.desc()).fetch();
return emps;
}
}
EmpDaoTest.java
package com.example.employee.repository;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import com.example.employee.model.Emp;
@Transactional
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class EmpDaoTest {
@Autowired
private EmpDao empDao;
@Test
public void testGetEmpOrderByDeptnoAscEmpnoDesc() {
List<Emp> emps = empDao.getEmpOrderByDeptnoAscEmpnoDesc();
for (Emp emp : emps) {
System.out.println(emp);
}
assertThat(emps.size(), is(14));
}
}
SQL 로그
select
e.empno as empno1_1_,
e.comm as comm2_1_,
e.deptno as deptno7_1_,
e.ename as ename3_1_,
e.hiredate as hiredate4_1_,
e.job as job5_1_,
e.mgr as mgr8_1_,
e.sal as sal6_1_
from emp e left outer join dept d
on e.deptno=d.deptno
order by d.deptno asc, e.empno desc

Emp : Dept = N : 1 의 연관관계에서 Emp의 정보를 요청했습니다. 이 경우 EAGER 로딩정책에 따라 Dept의 정보를 구하는 쿼리가 수행되는 것을 로그에서 볼 수 있습니다.
emp.dept.deptno.asc()
위 구문을 처리하기 위해서 조인을 사용하였습니다. 사용된 SQL 쿼리를 살펴보면 "order by d.deptno asc" 처럼 처리하기 위해서 조인을 사용했다고 판단할 수 있습니다.
6. Where
EmpDao.java
다음 메소드를 추가합니다.
public Emp getEmpByEnameAndJob(String ename, String job){
QEmp emp = QEmp.emp;
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
Emp result = query.select(emp).from(emp)
.where(emp.ename.eq(ename), emp.job.eq(job)).fetchFirst();
return result;
}
where 메소드에 전달하는 구문으로 콤마(,) 대신 and 메소드로 대체할 수 있습니다.
Emp result = query.select(emp).from(emp)
.where(emp.ename.eq(ename).and(emp.job.eq(job))).fetchFirst();
필터 조건을 or로 조합하고 싶다면 다음 패턴을 사용합니다.
List<Emp> result = query.select(emp).from(emp)
.where(emp.ename.eq(ename).or(emp.job.eq(job))).fetch();
EmpDaoTest.java
@Test
public void testGetEmpByEnameAndJob() {
Emp emp = empDao.getEmpByEnameAndJob("SMITH", "CLERK");
System.out.println(emp);
assertThat(emp.getEname(), is("SMITH"));
assertThat(emp.getJob(), is("CLERK"));
}
SQL 로그
select
e.empno as empno1_1_,
e.comm as comm2_1_,
e.deptno as deptno7_1_,
e.ename as ename3_1_,
e.hiredate as hiredate4_1_,
e.job as job5_1_,
e.mgr as mgr8_1_,
e.sal as sal6_1_
from emp e
where e.ename='SMITH' and e.job='CLERK'
limit 1

fetchFirst 메소드를 사용하면 ' limit 1' 구문이 사용되는 것을 알 수 있습니다. 추가로 deptno=20에 해당하는 정보를 구합니다.

객체 그래프 탐색을 위한 결과
'SMITH'에 상사 직원정보를 연속적으로 쿼리해서 얻어진 결과 로그입니다.
Emp(
empno=7369,
ename=SMITH,
job=CLERK,
mgr=Emp(
empno=7902,
ename=FORD,
job=ANALYST,
mgr=Emp(
empno=7566,
ename=JONES,
job=MANAGER,
mgr=Emp(
empno=7839,
ename=KING,
job=PRESIDENT,
mgr=null,
hiredate=1981-11-17,
sal=5000.0,
comm=null,
dept=Dept(deptno=10, dname=ACCOUNTING, loc=NEW YORK)
),
hiredate=1981-04-02,
sal=2975.0,
comm=null,
dept=Dept(deptno=20, dname=RESEARCH, loc=DALLAS)
),
hiredate=1981-12-03,
sal=3000.0,
comm=null,
dept=Dept(deptno=20, dname=RESEARCH, loc=DALLAS)
),
hiredate=1980-12-17,
sal=800.0,
comm=null,
dept=Dept(deptno=20, dname=RESEARCH, loc=DALLAS)
)