2020년 7월 24일 금요일

Querydsl, JPASQLQuery, SQLQueryFactory실습예제

Querydsl, JPASQLQuery, SQLQueryFactory실습예제

http://ojc.asia/bbs/board.php?bo_table=LecJpa&wr_id=343

3.4. JPASQLQuery, SQLQueryFactory

Querydsl은 JPASQLQuery 클래스를 사용하여 JPA의 네이티브 SQL쿼리 서비스를 제공합니다. 엔티티 클래스는 필요하지 않습니다. 대신 SQL 스키마 정보에 따른 Q 타입 클래스를 먼저 생성해 놓고 사용해야 합니다.

3.4.1. 프로젝트 생성

File > New > Spring Starter Project >

프로젝트 명: chapter3-4 > Next >

디펜던시 선택: Web, MySQL, JPA, Lombok > Finish

3.4.2. 데이터베이스 및 테이블 생성

엔티티 클래스를 만들지 않고 Querydsl이 제공하는 기술을 사용하여 테이블로부터 Q 타입 클래스를 생성합니다.

Maria DB에 HeidiSQL로 접속하여 다음 작업을 수행합니다.

1. DB생성

다음 DB이름을 사용하여 DB를 생성합니다.

데이터베이스 이름: test2유

2. 테이블 생성

다음 파일 schema.sql 파일의 쿼리를 사용하여 테이블을 생성합니다.

schema.sql

CREATE TABLE `dept` (

`deptno` INT(11) NOT NULL,

`dname` VARCHAR(14) NOT NULL,

`loc` VARCHAR(13) NULL DEFAULT NULL,

PRIMARY KEY (`deptno`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `emp` (

`empno` INT(11) NOT NULL,

`comm` DOUBLE NULL DEFAULT NULL,

`ename` VARCHAR(10) NOT NULL,

`hiredate` DATE NULL DEFAULT NULL,

`job` VARCHAR(9) NULL DEFAULT NULL,

`sal` DOUBLE NULL DEFAULT NULL,

`deptno` INT(11) NULL DEFAULT NULL,

`mgr` INT(11) NULL DEFAULT NULL,

PRIMARY KEY (`empno`),

INDEX `FKfqt0j25nlvjwt7qt1t3x7v6qf` (`deptno`),

INDEX `FKfehivfm7m674r8qrrnug1of2q` (`mgr`),

CONSTRAINT `FKfehivfm7m674r8qrrnug1of2q` FOREIGN KEY (`mgr`)

REFERENCES `emp` (`empno`),

CONSTRAINT `FKfqt0j25nlvjwt7qt1t3x7v6qf` FOREIGN KEY (`deptno`)

REFERENCES `dept` (`deptno`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `salgrade` (

`grade` INT(11) NOT NULL,

`hisal` DOUBLE NOT NULL,

`losal` DOUBLE NOT NULL,

PRIMARY KEY (`grade`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

3. 테스트 더미 데이터 입력

다음 data.sql 파일의 쿼리를 사용하여 테스트 데이터를 입력합니다.

data.sql

INSERT INTO SALGRADE(grade, losal, hisal) VALUES (1, 700,1200);

INSERT INTO SALGRADE(grade, losal, hisal) VALUES (2,1201,1400);

INSERT INTO SALGRADE(grade, losal, hisal) VALUES (3,1401,2000);

INSERT INTO SALGRADE(grade, losal, hisal) VALUES (4,2001,3000);

INSERT INTO SALGRADE(grade, losal, hisal) VALUES (5,3001,9999);

INSERT INTO DEPT(deptno, dname, loc) VALUES (10,'ACCOUNTING','NEW YORK');

INSERT INTO DEPT(deptno, dname, loc) VALUES (20,'RESEARCH','DALLAS');

INSERT INTO DEPT(deptno, dname, loc) VALUES (30,'SALES','CHICAGO');

INSERT INTO DEPT(deptno, dname, loc) VALUES (40,'OPERATIONS','BOSTON');

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7839,'KING','PRESIDENT', NULL,STR_TO_DATE('17-11-1981','%d-%m-%Y'),5000,NULL,10);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7566,'JONES','MANAGER', 7839,STR_TO_DATE('2-4-1981', '%d-%m-%Y'),2975,NULL,20);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7698,'BLAKE','MANAGER', 7839,STR_TO_DATE('1-5-1981', '%d-%m-%Y'),2850,NULL,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7782,'CLARK','MANAGER', 7839,STR_TO_DATE('9-6-1981', '%d-%m-%Y'),2450,NULL,10);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7902,'FORD','ANALYST', 7566,STR_TO_DATE('3-12-1981', '%d-%m-%Y'),3000,NULL,20);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7788,'SCOTT','ANALYST', 7566,STR_TO_DATE('13-07-1987', '%d-%m-%Y'),3000,NULL,20);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7499,'ALLEN','SALESMAN', 7698,STR_TO_DATE('20-2-1981', '%d-%m-%Y'),1600,300,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7521,'WARD','SALESMAN', 7698,STR_TO_DATE('22-2-1981', '%d-%m-%Y'),1250,500,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7654,'MARTIN','SALESMAN',7698,STR_TO_DATE('28-9-1981', '%d-%m-%Y'),1250,1400,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7844,'TURNER','SALESMAN',7698,STR_TO_DATE('8-9-1981', '%d-%m-%Y'),1500,0,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7900,'JAMES','CLERK', 7698,STR_TO_DATE('3-12-1981', '%d-%m-%Y'),950,NULL,30);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7934,'MILLER','CLERK', 7782,STR_TO_DATE('23-1-1982', '%d-%m-%Y'),1300,NULL,10);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7369,'SMITH','CLERK', 7902,STR_TO_DATE('17-12-1980','%d-%m-%Y'),800,NULL,20);

INSERT INTO EMP(empno, ename, job, mgr, hiredate, sal, comm, deptno)

VALUES(7876,'ADAMS','CLERK', 7788,STR_TO_DATE('13-07-1987', '%d-%m-%Y'),1100,NULL,20);

3.4.3. 프로젝트 환경설정

application.properties

# DATASOURCE

spring.datasource.platform=mariadb

spring.datasource.sqlScriptEncoding=UTF-8

spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/test2db?createDatabaseIfNotExist=true

spring.datasource.username=root

spring.datasource.password=1111

spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy

# ignore data.sql

spring.datasource.initialize=false

# JPA

spring.jpa.hibernate.ddl-auto=none

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

spring.jpa.show-sql=true

spring.data.jpa.repositories.enabled=true

# Logging

logging.config=classpath:logback-spring.xml

spring.datasource.initialize=false

dept, emp, slagrade 테이블이 이미 DB에 존재하고 초기 데이터도 입력되어 있기 때문에 schema.sql, data.sql은 쿼리를 프로젝트 기동 시 수행하지 않고 보관하는 용도로만 사용합니다.

spring.jpa.hibernate.ddl-auto=none

엔티티 클래스를 바탕으로 테이블을 생성하거나 수정하는 기능은 사용하지 않습니다.

스프링 부트 spring-boot-starter-parent 버전이 1.4.1.RELEASE 이면 querydsl은 자동적으로 4.1.3 버전이 설정됩니다. 4.1.3 버전에 약간의 버그가 있어서 테스트를 수행하기 적합하지 않다고 판단되었습니다. 따라서 실습에서는 4.1.2 버전을 사용하도록 하겠습니다.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>

<artifactId>boot</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.4.1.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

<querydsl.version>4.1.2</querydsl.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.bgee.log4jdbc-log4j2</groupId>

<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>

<version>1.16</version>

</dependency>

<dependency>

<groupId>com.querydsl</groupId>

<artifactId>querydsl-apt</artifactId>

<scope>provided</scope>

</dependency>

<dependency>

<groupId>com.querydsl</groupId>

<artifactId>querydsl-jpa</artifactId>

</dependency>

<dependency>

<groupId>com.querydsl</groupId>

<artifactId>querydsl-sql</artifactId>

<version>4.0.8</version>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<!-- without Entity Class, make QType for JPASQLQuery, SQLQueryFactory -->

<plugin>

<groupId>com.querydsl</groupId>

<artifactId>querydsl-maven-plugin</artifactId>

<version>${querydsl.version}</version>

<executions>

<execution>

<goals>

<goal>export</goal>

</goals>

</execution>

</executions>

<configuration>

<jdbcDriver>com.mysql.jdbc.Driver</jdbcDriver>

<jdbcUrl>jdbc:mysql://localhost:3306/test2db</jdbcUrl>

<jdbcUser>root</jdbcUser>

<jdbcPassword>1111</jdbcPassword>

<packageName>com.example.gen.model</packageName>

<targetFolder>target/generated-sources/java</targetFolder>

<namePrefix>S</namePrefix>

<exportBeans>true</exportBeans>

<!-- must match the table name as it is stored in the database,

multiple can be separated by comma (default: null) -->

<tableNamePattern>dept,emp,salgrade</tableNamePattern>

</configuration>

<dependencies>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>5.1.38</version>

<scope>compile</scope>

</dependency>

</dependencies>

</plugin>

</plugins>

</build>

</project>

<namePrefix>S</namePrefix>

명시적으로 설정하지 않으면 두문자 'Q'를 사용하여 Q 타입 클래스를 생성합니다. 개발자가 변경할 수 있습니다.

<exportBeans>true</exportBeans>

테이블 스키마를 바탕으로 모델클래스를 생성할 수 있습니다.

<tableNamePattern>dept,emp,salgrade</tableNamePattern>

테이블명을 콤마로 구분해서 대상 테이블들을 지정합니다. 일부 데이터베이스에서는 테이블명을 대문자로 설정해야 인식하니 Q 타입 클래스가 생성되지 않는 경우 대문자로 바꾸어서 시도해 보세요.

3.4.4. Q 타입클래스 생성

프로젝트 선택 > 마우스 오른쪽 클릭 > Run As > Maven generate-sources 클릭

QType 클래스 3개와 모델 클래스 3개가 생성되었습니다.

해당 패키지가 빌드 소스로 등록되지 않는 경우 직접 등록하세요. 앞에서 설명한 방법 외에 프로젝트의 프로퍼티 뷰에서 수정할 수도 있습니다.

프로젝트 선택 > 마우스 오른쪽 클릭 > Properties > Java Build Path

3.4.5. Persistence Layer

첫 번째 작업으로 DAO 로직부터 작성하겠습니다.

DAO 클래스 작성

EmpRepository.java

package com.example.employee.repository;

import java.util.List;

import com.example.employee.model.Emp;

import com.querydsl.core.Tuple;

public interface EmpRepository {

// 기동되지 않을 것이다. 설명을 위해 둔 메소드이다.

public List<Emp> getEmps0(String ename);

public List<Tuple> getEmps1(String ename);

public List<Tuple> getEmps2(String ename);

}

EmpRepositoryImpl.java

package com.example.employee.repository;

import java.util.List;

import javax.persistence.EntityManager;

import javax.persistence.PersistenceContext;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Repository;

import com.example.employee.model.Emp;

import com.example.employee.model.SEmp;

import com.querydsl.core.Tuple;

import com.querydsl.jpa.sql.JPASQLQuery;

import com.querydsl.sql.Configuration;

import com.querydsl.sql.MySQLTemplates;

import com.querydsl.sql.SQLQueryFactory;

import com.querydsl.sql.SQLTemplates;

import com.querydsl.sql.spring.SpringConnectionProvider;

import com.querydsl.sql.spring.SpringExceptionTranslator;

@Repository

public class EmpRepositoryImpl implements EmpRepository {

@PersistenceContext

private EntityManager em;

@Autowired

private DataSource dataSource;

@Override

public List<Emp> getEmps0(String ename) {

String sql = "select * from emp where ename=:ename";

@SuppressWarnings("unchecked")

List<Emp> emps = em.createNativeQuery(sql, Emp.class)

.setParameter("ename", ename).getResultList();

return emps;

}

@Override

public List<Tuple> getEmps1(String ename) {

SEmp emp = SEmp.emp;

SQLTemplates templates = new MySQLTemplates();

JPASQLQuery<?> query = new JPASQLQuery<Void>(em, templates);

List<Tuple> rows = query.select(emp.all())

.from(emp).where(emp.ename.eq(ename)).fetch();

return rows;

}

@Override

public List<Tuple> getEmps2(String ename) {

SEmp emp = SEmp.emp;

SQLQueryFactory queryFactory = new SQLQueryFactory(

querydslConfiguration(), new SpringConnectionProvider(dataSource));

List<Tuple> rows = queryFactory.select(emp.all())

.from(emp).where(emp.ename.eq(ename)).fetch();

return rows;

}

public Configuration querydslConfiguration() {

SQLTemplates templates = MySQLTemplates.builder().build();

Configuration configuration = new Configuration(templates);

configuration.setExceptionTranslator(new SpringExceptionTranslator());

return configuration;

}

}

@PersistenceContext

스프링 빈으로 등록되어 있는 EntityManagerFactory로부터 새로 EntityManager를 만들어서 어노테이션이 붙어 있는 변수에 DI 합니다.

SQLTemplates templates = new MySQLTemplates()

연결 데이터베이스가 MySQL 계열에 데이터베이스이므로 그에 맞는 템플릿 객체를 선택합니다. 이는 EntityManager에게 사용해야 하는 방언을 알려준다는 의미를 갖습니다.

new SQLQueryFactory(querydslConfiguration(), new SpringConnectionProvider(dataSource))

스프링의 예외전환 서비스를 이용하기 위한 설정을 추가합니다. SQLQuery, SQLQueryFactory 클래스는 JDBC 기술을 사용하므로 Connection 객체를 알려주어야 합니다. 대신 DataSource 객체를 알려주는 것으로 처리할 수 있습니다.

com.querydsl.core.Tuple

모델 클래스가 없거나 모델 클래스의 필드변수 구성과 질의 결과가 일치하지 않아 사용할 수 없을 때 대신 사용하는 Querydsl이 만들어 놓은 일종의 DTO 클래스입니다.

3.4.6. JUnit 테스트 클래스 작성

EmpRepositoryImplTest.java

package com.example.employee.repository;

import static org.hamcrest.CoreMatchers.equalTo;

import static org.hamcrest.CoreMatchers.is;

import static org.hamcrest.CoreMatchers.not;

import static org.hamcrest.CoreMatchers.sameInstance;

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;

import com.example.employee.model.SEmp;

import com.querydsl.core.Tuple;

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

public class EmpRepositoryImplTest {

@Autowired

private EmpRepository empRepository;

private final String TEST_ENAME = "SMITH";

// 예외가 발생하여 수행되지 않는다.

@Transactional

@Test

public void testGetEmps0() {

List<Emp> emps = empRepository.getEmps0(TEST_ENAME);

for (Emp emp : emps) {

System.out.println(emp);

}

assertThat(emps.size(), is(1));

}

@Transactional

@Test

public void testGetEmps1() {

SEmp emp = SEmp.emp;

// 쿼리 수행

List<Tuple> rows = empRepository.getEmps1(TEST_ENAME);

for (Tuple row : rows) {

System.out.print(row.get(emp.empno)+",");

System.out.print(row.get(emp.ename)+",");

System.out.println(row.get(emp.job));

}

assertThat(rows.size(), is(1));

System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~");

// 한번 더 같은 쿼리를 수행

List<Tuple> rows2 = empRepository.getEmps1(TEST_ENAME);

// 테이블의 로우(행)에 일치여부는 키 값이 같은지 여부로 판단한다.

// 객체도 그에맞게 처리하기 위해서 equals, hashCode메소드를 재정의해서 사용한다.

System.out.println(rows.hashCode() == rows2.hashCode());

System.out.println(rows.get(0).hashCode() == rows2.get(0).hashCode());

assertThat(rows, equalTo(rows2));

assertThat(rows.get(0), equalTo(rows2.get(0)));

// 엔티티 클래스가 존재하지 않으므로 1차 캐시 기능을 적용받지 못한다.

// 따라서 요청할 때 마다 매번 질의를 수행한다.

// rows 객체와 rows2 객체는 같은 값을 담고 있지만 메모리 차원에서는 다른 객체다.

System.out.println(rows != rows2);

System.out.println(rows.get(0) != rows2.get(0));

assertThat(rows, is(not(sameInstance(rows2))));

assertThat(rows.get(0), is(not(sameInstance(rows2.get(0)))));

}

/*

* java.lang.IllegalStateException: Connection is not transactional

* 트랜잭션 처리가 필수다. 어노테이션 @Transactional을 붙여서 간단하게 처리할 수 있다.

*/

@Transactional

@Test

public void testGetEmps2() {

SEmp emp = SEmp.emp;

List<Tuple> rows = empRepository.getEmps2(TEST_ENAME);

for (Tuple row : rows) {

System.out.print(row.get(emp.empno)+",");

System.out.print(row.get(emp.ename)+",");

System.out.println(row.get(emp.job));

}

assertThat(rows.size(), is(1));

}

}

3.4.7. 테스트

@Test testGetEmps0

Caused by: org.hibernate.MappingException: Unknown entity: com.example.employee.model.Emp

Emp 클래스는 엔티티 클래스가 아니고 테이블 스키마를 바탕으로 생성한 모델 클래스일 뿐입니다. 따라서 em.createNativeQuery(sql, Emp.class) 방식으로 사용할 수 없습니다.

@Test testGetEmps1

JPASQLQuery 클래스를 사용하여 잘 처리되는지 확인합니다.

JPASQLQuery 클래스를 사용하는 경우 1차 캐싱 서비스를 제공하는지 알아보기 위해서 empRepository.getEmps1(TEST_ENAME) 메소드를 연속해서 두 번 호출했습니다.

assertThat(rows, equalTo(rows2));

assertThat(rows.get(0), equalTo(rows2.get(0)));

assertThat(rows, is(not(sameInstance(rows2))));

assertThat(rows.get(0), is(not(sameInstance(rows2.get(0)))));

JUnit의 단정메소드들의 실행결과는 모두 true 입니다. 돌려 받은 두 건의 결과를 메모리 차원에서 비교하면 일치하지 않습니다. 이는 1차 캐싱 서비스를 제공받지 못한다는 의미입니다.

@Test testGetEmps2

SQLQueryFactory 클래스를 사용하여 잘 처리되는지 확인합니다.

결과는 testGetEmps1 테스트 메소드의 결과와 같습니다.

댓글 없음:

댓글 쓰기

(C#교육동영상)C# ADO.NET 실습 ODP.NET/ODAC 설치 오라클 함수 호출 실습, C#학원, WPF학원, 닷넷학원, 자바학원

  (C#교육동영상)C# ADO.NET 실습  ODP.NET/ODAC 설치  오라클 함수 호출 실습, C#학원, WPF학원, 닷넷학원, 자바학원 https://www.youtube.com/watch?v=qIPU85yAlzc&list=PLxU-i...