JOOQ 란?
- 테이블 스키마로부터 자바 코드를 만들어주는 라이브러리 이다.
- 시스템의 설계가 자바 코드(엔티티)가 아닌, DB 에서 시작될 때 유용하다.
- 당연히 ORM framework 가 아니다.
- ORM 은 오히려 반대로 Java class 를 DB 스키마로 매핑하기 때문이다.
- "jOOQ is not a replacement for JPA"
- SQL이 잘 어울리는 곳엔, Jooq가 잘 맞는다.
- 반면, Object Persistence 가 잘 어울리는 곳엔, JPA 가 잘 맞는다.
JOOQ 의 장점
- Jooq 방식으로 사용한다면, 엔티티를 작성할 필요가 없다.
- 대신 JPA 와 반대로 스키마는 정의 되어 있어야 된다.
- 로그가 보기 좋다. -> 결과 로그가 한눈에 보기 좋다.
- query 로그 안에 binding parameter 가 함께 포함된다.
JOOQ 의 단점
- ORM기술이 아니기 때문에,SpringDataJPA와 결이 잘 맞지 않는다.
- JPA 와 정반대의 매커니즘: Jooq 클래스가 엔티티 클래스를 방해한다.
- JPA 기술이 아니어서 오는 문제점이 있다.
- Spring Data JPA 트랜잭션 연동이 힘듬: 자체 트랜잭션 기술 -> Jooq 코드가 서비스에 노출된다.
- 하이버네이트 auto-ddl 사용 불가, DB 스키마는 이미 준비되어 있어야 한다.
- 스프링과 연동되어있지 않아서 오는 불편하다.
- 스프링 Pageable 정보로부터 Jooq 쿼리를 조립하기가 상당히 까다롭다.
- gradle 플러그인을 쓸 경우, DB 정보가 build.gradle 에 침투한다.
- DB 기본 정보, 로깅, 타입 변환(enum 등) 정보, 패키지 정보, DB dialect, JDBC 드라이버 정보
- 그리고 매뉴얼도 꽤 어렵고, 레퍼런스도 적다.
- Open Source 플랜만 무료이다.
Gradle 설정
plugins {
id 'nu.studer.jooq' version '5.2.2'
}
project.ext {
jooqVersion = dependencyManagement.importedProperties['jooq.version']
mysqlVersion = dependencyManagement.importedProperties['mysql.version']
}
dependencies {
// Jooq
implementation 'org.springframework.boot:spring-boot-starter-jooq'
jooqGenerator "mysql:mysql-connector-java:${project.mysqlVersion}"
}
//// Jooq 설정부
def generatedJooq = 'src/main/generated-jooq'
jooq {
version = project.jooqVersion
configurations {
main { // name of the jOOQ configuration
generationTool {
logging = org.jooq.meta.jaxb.Logging.WARN
jdbc {
driver = 'com.mysql.cj.jdbc.Driver'
url = 'jdbc:mysql://localhost:3306/service_desk'
user = 'root'
password = '12356'
}
generator {
name = 'org.jooq.codegen.JavaGenerator'
database {
name = 'org.jooq.meta.mysql.MySQLDatabase'
inputSchema = 'service_desk'
includes = '.*'
excludes = ''
forcedTypes {
forcedType {
userType = 'com.hyunwook.servicedesk.common.enumclass.ErrorCode'
enumConverter = true
includeExpression = '.*\\.error_code'
includeTypes = '.*'
}
forcedType {
userType = 'com.hyunwook.servicedesk.common.enumclass.RequestGroupState'
enumConverter = true
includeExpression = '.*\\.request_group_state'
includeTypes = '.*'
}
}
}
generate {
deprecated = false
records = true
immutablePojos = true
fluentSetters = true
javaTimeTypes = true
}
target {
packageName = 'com.hyunwook.servicedesk'
directory = generatedJooq
}
strategy.name = 'org.jooq.codegen.DefaultGeneratorStrategy'
}
}
}
}
}
// incremental build (증분 빌드) - Jooq 오브젝트 생성 퍼포먼스 향상
tasks.named('generateJooq').configure {
allInputsDeclared = true
outputs.cacheIf { true }
}
// java source set 에 Jooq 디렉토리 추가
sourceSets {
main.java.srcDir generatedJooq
}
// gradle clean 시에 Jooq 디렉토리 삭제 (본래의 Jooq 의도에는 맞지 않는 사용법)
tasks.named('clean') {
dependsOn 'cleanGenerateJooq'
}
- plugin, version, dependences 를 설정한다.
- 위와 같이 증분 빌드를 설정해서 성능을 높일 수 있다.
- 또한, clean 시 cleanGenerateJooq 실행하도록 설정한다.
DB 스키마 -> Java Class
- 위와 같이 DB 스키마를 정의했다.
- gradle 에서 설정한 위치에 java class 가 생성되는 것을 확인할 수 있다.
코드 예시
@RequiredArgsConstructor
@Repository
public class CustomerRepositoryJooqImpl implements CustomerRepositoryJooq {
private final DSLContext dslContext;
@Override
public Page<CustomerViewResponse> findCustomerViewPageBySearchParams(
String placeName,
String customerName,
CustomerStatus customerStatus,
LocalDateTime customerStartDatetime,
LocalDateTime customerEndDatetime,
Pageable pageable
) {
final Customer CUSTOMER = customer.CUSTOMER;
final Place PLACE = Place.PLACE;
Condition condition = trueCondition();
SelectField<?>[] select = {
CUSTOMER.ID,
PLACE.PLACE_NAME,
CUSTOMER.CUSTOMER_NAME,
CUSTOMER.CUSTOMER_STATUS,
CUSTOMER.CUSTOMER_START_DATETIME,
CUSTOMER.CUSTOMER_END_DATETIME,
CUSTOMER.CURRENT_NUMBER_OF_PEOPLE,
CUSTOMER.CAPACITY,
CUSTOMER.MEMO
};
if (placeName != null && !placeName.isBlank()) {
condition = condition.and(PLACE.PLACE_NAME.containsIgnoreCase(placeName));
}
if (customerName != null && !customerName.isBlank()) {
condition = condition.and(CUSTOMER.CUSTOMER_NAME.contains(customerName));
}
if (customerStatus != null) {
condition = condition.and(CUSTOMER.CUSTOMER_STATUS.eq(customerStatus));
}
if (customerStartDatetime != null) {
condition = condition.and(CUSTOMER.CUSTOMER_START_DATETIME.ge(customerStartDatetime));
}
if (customerEndDatetime != null) {
condition = condition.and(CUSTOMER.CUSTOMER_END_DATETIME.le(customerEndDatetime));
}
int count = dslContext
.selectCount()
.from(CUSTOMER)
.innerJoin(PLACE)
.onKey()
.where(condition)
.fetchSingleInto(int.class);
List<CustomerViewResponse> pagedList = dslContext
.select(select)
.from(CUSTOMER)
.innerJoin(PLACE)
.onKey()
.where(condition)
.limit(pageable.getOffset(), pageable.getPageSize())
.fetchInto(CustomerViewResponse.class);
return new PageImpl<>(pagedList, pageable, count);
}
}
- 위에서 생성된 Customer 클래스는 JOOQ 에 의해서 만들어진 클래스 이다.
- 위와 같이 queryDsl 과 유사하게 조건과 조인, pageable 을 설정할 수 있다.
- 하지만 위에서 언급했던 단점들 때문에 실무에서는 queryDsl 에 비해서 적게 사용한다.
REFERENCES
- 김은호 님의 Spring 강의
'Jdbc' 카테고리의 다른 글
HikariCP 코드 분석하기 2편 (HikariCP 커넥션 풀 초기화 과정) (0) | 2023.01.21 |
---|---|
HikariCP 코드 분석하기 1편 (HikariCP란?) (0) | 2023.01.21 |
Spring JDBC (0) | 2022.02.27 |
JPA vs MyBatis (0) | 2022.02.27 |
JDBC (0) | 2022.02.27 |