728x90
반응형
728x90
반응형
반응형

jsonpath: JSONPath는 JSON 객체의 요소를 쿼리하는 표준화된 방법으로 경로 표현식을 사용하여 JSON 문서에서 요소, 중첩된 요소 및 배열을 탐색할 수 있다.

spring-boot2.5.6 사용 시 자동으로 들어있는 com.jayway.jsonpath:json-path 라이브러리를 사용한다.

 

사용 예: 디비의 extraData 콜롬의 값이 string json 이고, 그 json 안에서 쿼리를 날려야 할 경우

@Query(value = "select round," +
                "     sum(money) as totalMoney," +
                "     sum(lua) as totalLua," +
                "     sum(ticket) as totalTicket, " +
                "     sum(stuff) as totalStuff, " +
                "     sum(emoticon) as totalEmoticon  " +
                "from (" +
                "    select l.extra_data->>'$.week' round" +
                "        , if (item_type = 1, l.cnt, 0) money" +
                "        , if (item_type = 2, l.cnt, 0) lua" +
                "        , if (item_type = 3, l.cnt, 0) ticket" +
                "        , if (item_type = 6, l.cnt, 0) stuff" +
                "        , if (item_type = 7, l.cnt, 0) emoticon" +
                "    from table_l as l" +
                "    where event_id = 'aaa'" +
                ") as h" +
              " group by h.round", 
nativeQuery = true)

위 쿼리에서

 l.extra_data->>'$.week' round

의 의미를 알아보자면

1. table_l 에는 extra_data 라는 콜롬이 있고, 그 내용은 {week:1, level:0}  이런 형식으로 생겼다.

2. mysql ->> operator는 unquote를 해준다('1' 로 나올 데이터를 1로 바꿔준다)

https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#operator_json-inline-path

 

MySQL :: MySQL 5.7 Reference Manual :: 12.18.3 Functions That Search JSON Values

12.18.3 Functions That Search JSON Values The functions in this section perform search operations on JSON values to extract data from them, report whether data exists at a location within them, or report the path to data within them. JSON_CONTAINS(target,

dev.mysql.com

3. 그 결과를 round라는 이름으로 꺼내온다(alias).

json의 형식으로 든 디비 데이터를 쿼리에서 꺼내서 사용할 때 유용할 듯 하다.

 


다른 이야기. 후에 비슷한 캐이스가 발생하여 있어서 추가로 남긴다.

작동되는 버전

 @Query(value = "select " +
            "JSON_EXTRACT(data, '$.clazz') as clazz, "+
            "JSON_EXTRACT(data, '$.totalScore') as totalScore "+
            "from hd_user_event e " +
            "where e.gid = :gid " +
            "and e.event_id = 'CoolTimeEvent' " +
            "order by e.base_date desc ",
        nativeQuery = true)
Stream<DoubleWheelUserRes> getDoubleWheelUserInfoList(String gid, Pageable pageable);

아래처럼 nativeQuery = false로 해서 바로 pojo로 바꾸고 싶었는데, 절대 불가였다..ㅠㅠ

그 이유는 jpql로 db function을 가져올 방법이 없었음.. 아래와 같이 하고 실행하면 Jqpl validation fail 이라고 뜨면서 실행불가다.. 아쉽.. 그래서 결국 native query true로 주고 DoubleWheelUserRes을 projection로 만들어서 해결했다..

참고) 작동 안되는 버전

    @Query(value = "select new com.model.event.DoubleWheelUserRes( " +
            "function('JSON_EXTRACT', e.data, '$.clazz'), "+
            "function('JSON_EXTRACT', e.data, '$.totalScore') "+
            ") from UserEvent e " +
            "where e.gid = :gid " +
            "and e.eventId = 'CoolTimeEvent' " +
            "order by e.baseDate desc "
            )

 


참고:

https://www.lesstif.com/dbms/mysql-json-data-54952420.html

 

MySQL 에서 JSON Data사용하기

 

www.lesstif.com

https://www.lesstif.com/dbms/jsonpath-54952418.html

 

JSONPath 사용법

 

www.lesstif.com

 

728x90
반응형
반응형

@Transactional 하위에 또 다른 @Transactional을 준다면?

REQUIRED is the default propagation. Spring checks if there is an active transaction, and if nothing exists, it creates a new one. Otherwise, the business logic appends to the currently active transaction:

propagation type의 기본 값이 REQUIRED라서 자식 트랜젝션은 부모 트랜젝션에 종속된다.

begin tran {   ##부모 트랜잭션
    begin tran   ##자식 트랜젝션
    commit tran
} commit tran

이때 자식 트랜젝션에 에러를 발생한다면?

begin tran {   ##부모 트랜잭션
    begin tran   ##자식 트랜젝션
    throw
} commit tran

>> 자식 트랜젝션이 롤백되고 부모 트랜젝션도 롤백될 것으로 보인다.

 

그럼 자식 트랜젝션에 PROPAGATION_REQUIRES_NEW를 지정한다면?

@Transactional(propagation = Propagation.REQUIRES_NEW)
begin tran {   ##부모 트랜잭션
    begin tran requires_new   ##자식 트랜젝션
    throw
} commit tran
 

부모 트랜잭션은 독립적인 자식 트랜잭션이 실행되면 일시정지되었다가 자식 트랜잭션이 종료(커밋 혹은 롤백) 되면 재개된다.

1 커넥션 - 1 트랜젝션이 성립하려면 사실 'suspended'라는 건 존재할 수 없다. 그런데 저기서 suspended라고 언급한 이유는 무엇인가?

관련 소스를 열여보니 트랜잭션 정보를 임시 보관할 때 suspend()라는 함수를 사용하여 TransactionManager의 현재 트랜잭션을 SuspendedResourcesHolder에 저장한 후 현재 트랜잭션을 초기화하고, 새로운 트랜잭션을 시작하게 처리하고 있다는 것을 확인할 수 있었다. 그리고 현재 트랜잭션이 끝나면 SuspendedResourceHolder에서 다시 기존 트랜잭션 정보 꺼내와서 현재 트랜잭션에 설정한다.

 

begin tran {   ##부모 트랜잭션
    begin tran requires_new   ##자식 트랜젝션
    throw
} commit tran

따라서 위와 같은 경우, 자식 트랜젝션은 사실 별개의 독립적인 트랜젝션이 되어 저장/작업을 하게되며, 익셉션을 날리면 자식은 영향을 받지 않고 부모는 익셉션을 받아 롤백된다.

+

https://jobc.tistory.com/214

비슷한 글: https://woodcock.tistory.com/40

728x90
반응형
반응형

JobBuilderFactory > JobBuilder > SimpleJobBuilder > SimpleJob

 

  • validate: 로직 전 job parameter 검증 가능
    • JobParametersValidator implement 해서 커스텀하게 만들 수 있음
DefaultJobParametersValidator(String[] requiredKeys, String[] optionalKeys)

 

  • prevent: job의 재시작 여부 설정
    • 기본값은 true이며 false일 경우 이 job은 재시작을 지원하지 않는다는 의미 -> 재시작하려고 하면 exception발생
    • 첫 시작과는 무관
    • job의 성공/실패와 상관없이 오직 preventRestart 설정 값에 따라 실행 여부를 판단

 

  • incrementer: jobParameters에 필요한 값을 증가시켜 다음에 사용될 jobParameters 리턴
    • 기존의 jobParameter 변경 없이, 이전에 실패하지 않았더라도, job을 여러번 시작하고자 할 때(ex. 검사하는 로직 등)
    • 사용하지 않는 파라미터를 추가, 그 값을 변경시켜 마치 다른 파라미터처럼 보이게 함(인덱스를 추가해서 ++시킨다던가, 현재 날짜를 추가한다거나)
    • RunIdIncrementer implement해서 커스텀하게 만들 수 있음

 

<SimpleJob>

SimpleJob 흐름도

728x90
반응형
반응형

spring-boot 2.7.0 기준 작성

 

  • 특정 job만 실행할 수 있는 옵션

application.yml에 spring.batch라는 prefix로 설정해 두면 BatchProperties 라는 파일에서 읽어감(,로 복수개 구분)

spring:  
  batch:
    job:
      names: ${job.name:NONE} 

# program argument에 --job.name의 옵션으로 준 이름을 받아와서 실행, 
# 해당 이름의 job 없으면 NONE이라는 이름의 배치 실행; 없으면 실행안함

 

  • 부트 실행 시 자동 실행 막는 옵션
spring:
  batch:	
    job:
      enabled: false
      
# 기본 값 true

 

  • 디비 스키마 관련 옵션
spring:
  batch:
    jdbc:
      initialize-schema: always
      table-prefix: ST_
      
# initialize-schema
#   ALWAYS : 항상 실행(없으면 생성)
#   EMBEDDED : 내장 DB일 때만 실행
#   NEVER : 항상 실행 안함

# table-prefix 테이블 프리픽스 변경(기본: BATCH_)
# 이 때 테이블을 미리 생성해주어야 한다. 그 이름 테이블이 없다고 다시 만들지는 않음.
728x90
반응형

'개발 > spring-batch' 카테고리의 다른 글

[spring-batch] 소개  (0) 2023.12.04
[spring-batch] simpleJob  (0) 2022.05.26
[spring-batch] h2 연결 및 설정  (0) 2022.05.23
[spring-batch] 도메인 이해  (0) 2022.05.23
[spring-batch] 기초  (0) 2022.05.20
반응형

1. h2 설치

https://inma.tistory.com/149

 

[SpringBoot] H2 데이터베이스 설치 및 실행

이번 포스팅에서는 MacOS에서 H2 데이터베이스를 설치하고, H2 데이터베이스에서 에러 없이 실행하는 방법에 대해 알아보겠습니다! 🧐 H2 데이터베이스는 brew를 통해 쉽게 설치할 수 있습니다. (설

inma.tistory.com

https://so-easy-coding.tistory.com/5?category=968591 

 

[Spring Boot] H2 Database 설치 (Mac, Linux)

Spring Boot로 간단한 CRUD를 만들어보려고 하다가 H2라는 데이터베이스를 알게되었다. H2는 경량 DB이다. mySQL보다 훨씬 간단하기 때문에 학습할 때 매우 적절하므로 애용할 예정이다. 설치도 매우 쉽

so-easy-coding.tistory.com

 

2. spring-batch 에 필요한 테이블들

  •  spring-batch-core/org.springframework/batch/core/* 에 위치

  • 스크립트 실행 설정은 application.properties의 spring.batch.initialize-schema 로 구분
    • ALWAYS : 항상 실행
    • EMBEDDED : 내장 DB일 때만 실행 (기본 값)
    • NEVER : 항상 실행 안 함

 

3. springboot 와 h2 연결

spring:
  datasource:
    hikari:
      jdbc-url: jdbc:h2:tcp://localhost/~/spring-batch-test
      username: sa
      password:
      driver-class-name: org.h2.Driver
  batch:
    job:
      enabled: false #구동 시 자동실행 안함
    jdbc:
      #ALWAYS : 항상 실행
      #EMBEDDED : 내장 DB일 때만 실행
      #NEVER : 항상 실행 안함
      initialize-schema: embedded

참고) h2는 다양한 모드를 제공

https://www.h2database.com/html/features.html

 

Features

  Features Feature List H2 in Use Connection Modes Database URL Overview Connecting to an Embedded (Local) Database In-Memory Databases Database Files Encryption Database File Locking Opening a Database Only if it Already Exists Closing a Database Ignore

www.h2database.com

  • 엠비디드 연결 : jdbc:h2:[file:][<path>][databaseName]
  • 인 메모리 : jdbc:h2:mem:<databaseName>
  • 서버 모드 : jdbc:h2:tcp://<server>[:<port>]/[<path>]<databaseName>

 

intellij 안의 database로 테이블을 보는게 더 편할 것 같아서 설정했는데 connection이 성공하는 듯 하다가 아래와 같이 실패하는 현상이 있다.

https://youtrack.jetbrains.com/issue/DBE-15020

 

H2 2.1.210 not supported in Database tool : DBE-15020

What steps will reproduce the issue? 1. Add H2 v. 2.1.210 as jdbc Driver 2. Open the H2 Database What is the expected result? Databases and Tables are showed What happens instead? 2022-02-08 11:45:18,643 [90050563] WARN - lij.database.util.ErrorHandler - T

youtrack.jetbrains.com

뭔가 intelij 버그 같아서 기다려야 할 것 같다.

 

728x90
반응형

'개발 > spring-batch' 카테고리의 다른 글

[spring-batch] 소개  (0) 2023.12.04
[spring-batch] simpleJob  (0) 2022.05.26
[spring-batch] application.yml 설정 값  (0) 2022.05.25
[spring-batch] 도메인 이해  (0) 2022.05.23
[spring-batch] 기초  (0) 2022.05.20
반응형

Job

  • 가장 상위 개념, 하나의 배치작업 자체
  • 전체적으로 설정하고 명세한 객체
  • 여러 step을 포함하고 있는 컨테이너, 반드시 한 개 이상의 step으로 구성해야 함
  • 최상위 인터페이스
  • 기본 구현체:
    • SimpleJob: 순차적으로 step을 실행시키는 job. 모든 job에서 유용하게 사용할 수 있는 표준 기능을 가지고 있음. steps 리스트 안에 step 객체를 가지고 있음
    • FlowJob: 특정한 조건과 흐름에 따라 step을 구성하여 실행시킴, 더 유연, 조건과 흐름에 따라 구성을 다르게 할 수 있음. flow 객체를 실행히켜서 작업을 진행

 

JobInstance

  • job이 실행될 때 만들어지는 논리적 실행 단위
  • job의 설정과 구성은 동일하지만 job이 실행되는 시점에 처리하는 내용(jobParameter)은 다르기 때문에 job의 실행을 구분해야 함
  • job : jobInstance = 1 : M

 

JobParameter

  • job 실행 시 함께 포함하여 사용하는 파라미터 객체
  • 하나의 job에 존재할 수 있는 여러 jobInstance를 구분하기 위한 용도
  • jobParameter : jobInstance = 1 : 1
  • STRING, DATE, LONG, DOUBLE 타입 제공

바인딩 방법

  • jar 실행 시 주입
    • java -jar dd.jar requestDate=111
  • 코드로 생성
    • JobParameterBuilder, DefaultJobParametersConverter
  • SpEL 이용
    • @Value("#{jobParameter[requestDate]}")

 

참고) jar 실행 시 주입 하는 방법으로 테스트 해봤더니 아래와 같은 오류가 난다. 

libs % java -jar springbatch-test-0.0.1-SNAPSHOT.jar name=user3 seq(long)=1L date(date)=2021/02/03 age(double)=12.34
zsh: no matches found: seq(long)=1L
아래와 같이 escape 해주면 해결..
java -jar batch.jar executionDate\(date\)=2021/02/21
or
java -jar batch.jar 'executionDate(date)=2021/02/21'

 

 

JobExecution

  • jobInstance에 대한 한 번의 시도를 의미하는 객체. job이 실행될 때 마다 생성되며 job 실행 중에 발생한 정보를 저장하는 객체
  • jobExecution은 FAILED or COMPLETED 등의 job 실행 결과를 가지고 있음
  • COMPLETED가 될 때 까지 하나의 jobInstance 내에서 여러번 시도가 생길 수 있음
  • JobInstance : JobExecution = 1 : M

 

Step

  • job을 구성하는 독립적인 하나의 단계로 실제 배치 처리를 정의하고 컨트롤하는데 필요한 모든 정보를 가진 객체
  • 배치 작업을 어떻게 구성하고 실행할지 세부작업을 task기반으로 설정하고 명세해 놓은 객체
  • 비지니스 로직을 담음
  • 기본 구현체 
    • taskletStep : 기본이 되는 클래스, 구현체 제어
    • partitionStep : 멀티 스레드 방식, step을 여러 개로 분리해서 실행
    • jobStep : step 내에서 job을 실행하도록 함, chaining
    • flowStep : step 내에서 flow를 실행하게 함

 

StepExecution

  • step에 대한 한 번의 시도를 의미하는 객체, step 실행 중에 발생한 정보를 저장하는 객체
  • 각 step별로 생성, step이 실제로 시작되었을 때만 생성
  • job이 실패해 재시작하더라도 이미 성공한 step은 재실행되지 않고 실패한 step만 실행됨(실패하면 그 이후 step은 실행되지 않고 종료)
  • 모든 stepExecution이 성공해야 jobExecution도 정상적으로 완료
  • JobExecution : StepExecution =  1 : M

 

StepContribution

  • 청크 프로세스의 변경사항을 버퍼링한 수 stepExecution상태를 업데이트하는 도메인 객체
  • 청크 커밋 직전에 stepExecution의 apply 메서드를 호출하여 상태를 업데이트
  • exitStatus의 기본 종료토드 외 사용자 정릐 종료코드를 생성해서 적용할 수 있음
  • stepExecution 객체 안에 있음

 

ExecutionContext

  • map의 형태로 stepExecution 이나 jobExecution 객체의 상태를 저장하는 공유 객체; 유지 관리에 필요한 키-값 저장
  • DB에는 직렬화 한 값으로 저장됨({"키":"값"})
  • 공유 범위
    • step: 각 step의 stepExecution에 저장되며 step간 공유 안 됨
    • job: 각 job의 jobExecution에 저장되며 job간 공유 안되고 job의 step간에는 서로 공유 됨
      • job 재시작 시 이미 처리한 데이터는 건너뛰고 이후를 수행하도록할 때 상태정 보를 활용 

 

JobRepository

  • 배치 작업 중 정보를 저장하는 저장소, 인터페이스; simpleJobRepository 구현체 있고, 커스텀 가능
  • job이 언제 수행되었고 언제 끝났고 몇 번 실행되었고 등 메타데이터를 저장함
  • @EnableBatchProcessing 어노테이션만 선언하면 job repository가 자동으로 빈으로 생성됨
  • jdbc 방식
    • JobRepostioryFactoryBean
    • 내부적으로 aop를 통해 트랜잭션 처리 해주고 있음
    • 트랜잭션 isolation 기본 값은 serializable로 최고 수준이지만 다른 레벨로도 지정 가능
    • 메타테이블 테이블 prefix가 기본으로는 BATCH_ 이지만 수정 가능
  • in memory 방식
    • MapJobRepositoryFactoryBean
    • 성능 등의 이유로 굳이 DB에 넣고 싶지 않은 경우
    • test나 프로토타입의 빠른 개발이 필요할 때 사용

 

JobLauncher

  • job을 실행하는 역할
  • job과 JobParameter를 인자로 받아 요청된 배치 작업을 수행한 후 jobExecution을 반환
  • 부트가 구동되면 jobLauncher 빈이 자동 생성됨
  • job 실행
    • 동기적 실행: SyncTaskExecutor
      • 모든 단계를 다 마친 후에 jobExecution 반환
      • 스케줄러에 의한 배치 처리에 적합
      • 배치 처리 시간이 길어도 상관 없을 경우
    • 비동기적 실행: SimpleAsyncTaskExecutor
      • jobExecution을 생성하고 획득, 획득하자마자 반환(exitStauts.UNKNOWN) , 배치 완료
      • http 요청에 의한 배치처리에 적합; 배치처리 시간이 길 경우 응답이 늦어지지 않도록 함
       

728x90
반응형

'개발 > spring-batch' 카테고리의 다른 글

[spring-batch] 소개  (0) 2023.12.04
[spring-batch] simpleJob  (0) 2022.05.26
[spring-batch] application.yml 설정 값  (0) 2022.05.25
[spring-batch] h2 연결 및 설정  (0) 2022.05.23
[spring-batch] 기초  (0) 2022.05.20
반응형

<배치 패턴>

  • read: 데이터 조회
  • process: 데이터 가공
  • write: 수정된 양식으로 다시 저장

 

<아키텍쳐>

spring batch layer

  • 개발자가 만든 모든 배치 job과 커스텀 코드 포함
  • 개발자는 로직의 구현에만 집충, 공통 기반기술은 프레임워크가 담당

 

  • job을 실행, 모니터링, 관리하는 api로 구성

 

  • application, core 모두 공통 Infrastructure 위에서 빌드
  • job실행의 흐름과 처리를 위한 틀 제공
  • reader, processor, writer, skip, retry 등

 

 

<배치 활성화>

@EnableBatchProcessing  어노테이션을 달아야 실행되며, 아래 클래스들이 활성화 됨

  • BatchAutoConfiguration -> JobLauncherApplicationRunner 빈 생성
  • SimpleBatchConfiguration -> 스프링 배치 주요 구성 요소 생성; 프록시 객체로 생성됨
  • BatchConfigurerConfiguration -> BasicBatchConfigurer

batch loading 순서

  1. BatchAutoConfiguration
  2. BatchProperties 주입받아서 설정 읽고(특정 job만 실행 가능)
  3. JobLauncherApplicationRunner 빈 생성
  4. JobLauncherApplicationRunner.run 함수 실행할 때 ApplicationArguments(부트 run config에 설정한 값) 받아서 넘겨줌
  5. JobLauncherApplicationRunner.execute 함수 실행

 

 

<배치 시작>

  1. configurer 선언
  2. JobBuilderFactory
  3. StepBuilderFactory
  4. Job; 여러개의 스텝을 하나로
  5. Step; 하나의 일
  6. tasklet; 작업내용, 비즈니스 로직
  7. Job 구동 -> Step 실행 -> tasklet 실행

 

<메타데이터 저장>

배치가 실행하고 과정을 저장하기 위한 테이블이 필요하다. job에 관련한 테이블 4개, step에 관련한 테이블 2개가 필수적으로 필요하다.(디비 특성에 따라 시퀀스를 저장하는 테이블이 추가될 수 있음)

따라서 DB연결이 필수!

728x90
반응형

'개발 > spring-batch' 카테고리의 다른 글

[spring-batch] 소개  (0) 2023.12.04
[spring-batch] simpleJob  (0) 2022.05.26
[spring-batch] application.yml 설정 값  (0) 2022.05.25
[spring-batch] h2 연결 및 설정  (0) 2022.05.23
[spring-batch] 도메인 이해  (0) 2022.05.23
반응형
Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation.

위와 같은 에러가 날 경우 프로퍼티로 설정한 driverClassName을 살펴보자. com.mysql.jdbc.driver는 deprecated 된 이름이다.

"driverClassName": "com.mysql.cj.jdbc.Driver"

위와 같이 driverClassName을 수정해야한다.

728x90
반응형
반응형

조건: 두 list object가 같은 결과로 리턴, 후에 합쳐서 데이터 추가 세팅 후, 날짜를 기준으로 정렬한 값을 반환해야 한다.

환경: springboot2.6.2

 

1. nativeQuery=true, projection interface 사용

@Query(value = "select hur.gid, hur.ci, hur.nick, hur.last_logout as lastLogout, " +
        "huld.begin_chip as beginChip, huld.last_chip as lastChip, huld.last_chip - huld.begin_chip as changeChip, huld.win_chip as winChip, timestamp(huld.base_date) as baseDateTime, hll.expired_Date as expiredDate, " +
        "case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease " +
        "from hd_user_loss_daily huld " +
        "join hd_user_rat hur on hur.gid = huld.gid " +
        "join hd_loss_limit hll on hll.ci = hur.ci " +
        "where hur.ci = :#{#req.ci} " +
        "and huld.base_date between :#{#req.startDate} and :#{#req.endDate} ; "
        , nativeQuery = true)
<S extends LossChipDailyProjection> List<S> getListUserLossDailyWithUserRat(@Param("req") LossChipDailyReq req);



@Query(value = "select log.ci, " +
        "case when log.gid = 'ADMIN' then log.sval1 else log.gid end as releaser, " +
        "case when log.gid = 'ADMIN' then log.log_date else null end as adminReleaseDate, " +
        "case when log.gid <> 'ADMIN' then log.log_date else null end as elseReleaseDate, " +
//            "log.ival2 as lastChip, " +
        "log.log_date as baseDateTime " +
        "from hd_loss_limit_log log " +
        "where log.log_type = 'RELEASED' " +
        "and log.ci = :#{#req.ci} " +
        "and log.log_date between :#{#req.startDateTime} and :#{#req.endDateTime} ; "
        , nativeQuery = true)
<S extends LossChipDailyProjection> List<S> getListReleaseInfoByCi(@Param("req") LossChipDailyReq req);

request param을 object로 넘기고 싶으면, SpEL을 사용하면 된다.

public interface LossChipDailyProjection {
    String getCi();

    String getGid();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getAdminReleaseDate();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getElseReleaseDate();

    String getReleaser();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getExpiredDate();

    Boolean getCanRelease();

    String getNick();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getLastLogout();

    Long getBeginChip();

    Long getLastChip();

    Long getChangeChip();

    Long getWinChip();

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DateFormatConst.DEFAULT)
    LocalDateTime getBaseDateTime();
}

서비스 로직은 아래와 같다.

//public class LossChipDailyRes implements LossChipDailyProjection
//LossChipDailyRes -> LossChipDailyProjection로 바꿔도 동일

public List<LossChipDailyRes> getList(LossChipDailyReq req){
	//같은 response list로 가져와서
    List<LossChipDailyRes> listUser = lossDailyRepository.getListUserLossDailyWithUserRat(req);
    List<LossChipDailyRes> listRelease = logRepository.getListReleaseInfoByCi(req);

	//검사 후 추가 값 세팅
    for(LossChipDailyRes eachR : listRelease){
        LocalDateTime dailyBaseDateTime = eachR.getBaseDateTime().withHour(0).withMinute(0).withSecond(0);
        for(LossChipDailyRes eachD : listUser){
            if(eachD.getBaseDateTime().equals(dailyBaseDateTime)){
                eachR.setBeginChip(eachD.getBeginChip());  //에러발생
            }
        }
    }
	
    //두 리스트 합치고 정렬해서 반환
    List<LossChipDailyRes> unionContent = Stream.concat(listUser.stream(), listRelease.stream())
                                        .sorted(Comparator.comparing(LossChipDailyProjection::getBaseDateTime).reversed())
                                        .collect(Collectors.toList());

    return unionContent;
}

repository에서 반환되는 값을 class로 받아도 실제로 그리 안 들어가고 interface에 머무는 듯하다. 

왜냐면

  1. @JsonFormat부분을 class에 넣으면 날짜 변환이 안된다.
  2. setter 쪽 로직에서 아래와 같은 에러가 난다.
java.lang.UnsupportedOperationException: A TupleBackedMap cannot be modified.

인터페이스 -> 클래스로 매핑하는 로직 따위를 넣으면서까지 이 방식을 고수하고 싶지 않아서 다른 방법을 찾아본다.

(조금만 더 찾아보면 될 것 같기도 한데.... 잘 모르겠다) 


2.  @NamedNativeQuery를 사용

위와 같이 native query를 사용하되 return 값을 class로 받을 수 있는 방법을 고민하다가 @NatedNativeQuery를 사용하면 result mapping class와 매핑 룰을 지정하여 class로 리턴할 수 있을 것 같아서 시도해보았다. entity클래스 상단에 아래 부분을 추가해보았다.

@NamedNativeQuery(name = "UserLossDaily.getListUserLossDailyWithUserRat2",
query = "select hur.gid, hur.ci, hur.nick, hur.last_logout as lastLogout, " +
        "huld.begin_chip as beginChip, huld.last_chip as lastChip, huld.last_chip - huld.begin_chip as changeChip, huld.win_chip as winChip, timestamp(huld.base_date) as baseDateTime, hll.expired_Date as expiredDate, " +
        "case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease " +
        "from hd_user_loss_daily huld " +
        "join hd_user_rat hur on hur.gid = huld.gid " +
        "join hd_loss_limit hll on hll.ci = hur.ci " +
        "where hur.ci = :#{#req.ci} " +  //에러발생
        "and huld.base_date between :#{#req.startDate} and :#{#req.endDate} ;"
        , resultSetMapping = "Mapping.LossChipDailyRes")
@SqlResultSetMapping(name =  "Mapping.LossChipDailyRes",
classes = @ConstructorResult(targetClass = LossChipDailyRes.class, columns = {
        @ColumnResult(name = "gid"),
        @ColumnResult(name = "ci"),
        @ColumnResult(name = "nick"),
        @ColumnResult(name = "lastLogout"),
        @ColumnResult(name = "beginChip"),
        @ColumnResult(name = "lastChip"),
        @ColumnResult(name = "changeChip"),
        @ColumnResult(name = "winChip"),
        @ColumnResult(name = "baseDateTime"),
        @ColumnResult(name = "expiredDate"),
        @ColumnResult(name = "canRelease"),
}))
@Entity
...

그런데 신박한 에러가 난다. 바로 SpEl을 사용할 수 없는 것이었다.

Space is not allowed after parameter prefix ':'

구글링 해보니 : 앞에 \\를 줘서 escape 하라는 글이 있었는데, 그건 select절 내에서 named parameter를 가져올 때나 가능하고 저렇게 SpEl문법을 escape 할 때 사용할 수 있는 것 같아 보이지는 않았다.

개인적으로

  1. request parameter를 method req변수로 쫙 늘어뜨리고 싶지 않아서(object로 전달하고 그 안에서 꺼내서 매핑하고 싶어서)
  2. entitiy 위에 매핑 룰/쿼리를 쫙 쓰면서 관리하고 싶지 않아서(쿼리는 repository에서 관리하는 게 보기에도 찾기에도 좋다고 생각) 빠르게 포기했다.

3. 결국 돌고 돌아 JQPL로..

사실 jpql로 쓰면 바로 될 것이라는 것을 알고 있었지만

  1. entity에 (단순히 select를 위한) relation표기를 별로 안 좋아해서
  2. response dto에 변수가 많은, 순서에 예민한 constructor생성을 피해보려고

최대한 다른 방식으로 해보고 싶었는데.. 짧은 시간 안에 해답을 찾기가 어려웠다.

@Query(value = "select new com.model.restriction.LossChipDailyRes( " +
        "hur.gid, hur.ci, hur.nick, hur.lastLogout, " +
        "huld.beginChip, huld.lastChip, huld.winChip, huld.pk.baseDate, hll.expiredDate, " +
        "case when hll.expiredDate > current_timestamp and huld.pk.baseDate = function('date_format', current_date, '%Y-%m-%d') then true else false end ) " +
        "from UserLossDaily huld " +
        "join huld.userRat hur " +
        "join hur.restrictionLawCi hll " +
        "where hur.ci = :#{#req.ci} " +
        "and huld.pk.baseDate between :#{#req.startDate} and :#{#req.endDate} "
        )
List<LossChipDailyRes> getListUserLossDailyWithUserRat3(@Param("req") LossChipDailyReq req);


 @Query(value = "select new com.model.restriction.LossChipDailyRes(" +
        "log.ci, " +
        "case when log.gid = 'ADMIN' then log.sval1 else log.gid end, " +
        "case when log.gid = 'ADMIN' then log.pk.logDate else null end, " +
        "case when log.gid <> 'ADMIN' then log.pk.logDate else null end, " +
//            "log.ival2, " +
        "log.pk.logDate) " +
        "from UserRestrictionLawCiLog log " +
        "where log.logType = 'RELEASED' " +
        "and log.ci = :#{#req.ci} " +
        "and log.pk.logDate between :#{#req.startDateTime} and :#{#req.endDateTime} "
        )
List<LossChipDailyRes> getListReleaseInfoByCi3(@Param("req") LossChipDailyReq req);
//조인이 필요한 엔티티마다 릴레이션 추가
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "gid", updatable = false, insertable = false)
UserRat userRat;
//변수 순서가 개복치..
public LossChipDailyRes(String gid, String ci, String nick, LocalDateTime lastLogout, Long beginChip, Long lastChip, Long winChip, LocalDate baseDate, LocalDateTime expiredDate, Boolean canRelease) {
    this.ci = ci;
    this.gid = gid;
    this.expiredDate = expiredDate;
    this.canRelease = canRelease;
    this.nick = nick;
    this.lastLogout = lastLogout;
    this.beginChip = beginChip;
    this.lastChip = lastChip;
    this.changeChip = lastChip - beginChip;
    this.winChip = winChip;
    this.baseDate = baseDate;
    this.baseDateTime = baseDate.atTime(0, 0, 0);
}

native query -> JPQL로 변환 시 주의사항

  • native query에서는 dto이름 그대로 매핑하기 때문에 as로 이름을 다시 지정해줬어야 핬지만, jpql query로 바꿀 때는 as를 쓰면 안된다(쿼리 파싱이 안되서 서버가 안 뜸). 순서만 dto constructor랑 맞추줘야 함.
  • sql function을 사용할 때 function으로 다 바꿔야하고 now()(mysql전용)도 current_date(jpa용) 등으로 바꿔야한다.
  • true/false도 quotation 처리 확인해야한다. boolean으로 못 가져오고 string으로 인식할 수 있음..
//native query
case when hll.expired_date > now() and huld.base_date = DATE_FORMAT(now(), '%Y-%m-%d') then 'true' else 'false' end as canRelease 

//jpql
case when hll.expiredDate > current_timestamp and huld.pk.baseDate = function('date_format', current_date, '%Y-%m-%d') then true else false end

 

위와 같이 jpql로 바꾸니.. 다행히 서비스 로직을 그대로 사용할 수 있었다.

native query를 자주 사용하지 않아 + 복합적인 조건으로 인해 시간이 걸린 삽질이었지만, native query의 특징을 배울 수 있었던 시간이었다..

 


SpEl

https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions

 

SpEL support in Spring Data JPA @Query definitions

<p>Spring Data JPA allows manually defining the query to be executed by a repository method using the <code>@Query</code> annotation. Unfortunately parameter binding in JPQL is quite limited only allowing you to set a value and providing some type conversi

spring.io

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.named-parameters

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

@NamedNativeQuery

https://thorben-janssen.com/spring-data-jpa-dto-native-queries/

 

Spring Data JPA - How to Return DTOs from Native Queries

Returning DTOs from a native query with Spring Data JPA is a little harder than you might expect. But there are 2 good solutions for it.

thorben-janssen.com

 

jpa projection

https://www.baeldung.com/spring-data-jpa-projections

 

Spring Data JPA Projections | Baeldung

A quick and practical overview of Spring Data JPA Projections.

www.baeldung.com

https://moonsiri.tistory.com/95

 

[SpringBoot] JPA Projection

Projection은 Entity의 속성이 많을 때 일부 데이터만 조회하는 방법입니다. 아래 UserEntity를 참고하여 설명하겠습니다. @Entity @Table(name = "user") @NoArgsConstructor(access = AccessLevel.PROTECTED) pu..

moonsiri.tistory.com

 

728x90
반응형
반응형

CASE, IF Function(IF 함수), IF Statement(IF 문)의 차이

  • case는 스위치문 같고
  • if는 조건이 두개인 case와 같다.
  • if statement는 프로시져에나 사용되는 완전 다른 이야기.

https://stackoverflow.com/questions/30047983/mysql-case-vs-if-statement-vs-if-function

 

MySQL - CASE vs IF Statement vs IF function

Who can please explain the difference between CASE-statement, IF-statement and IF-function? What is the difference in terms of usage and "how it works"?

stackoverflow.com

 

728x90
반응형

+ Recent posts