아래처럼 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 "
)
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
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
따라서 위와 같은 경우, 자식 트랜젝션은 사실 별개의 독립적인 트랜젝션이 되어 저장/작업을 하게되며, 익셉션을 날리면 자식은 영향을 받지 않고 부모는 익셉션을 받아 롤백된다.
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_)
# 이 때 테이블을 미리 생성해주어야 한다. 그 이름 테이블이 없다고 다시 만들지는 않음.
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
repository에서 반환되는 값을 class로 받아도 실제로 그리 안 들어가고 interface에 머무는 듯하다.
왜냐면
@JsonFormat부분을 class에 넣으면 날짜 변환이 안된다.
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 할 때 사용할 수 있는 것 같아 보이지는 않았다.
개인적으로
request parameter를 method req변수로 쫙 늘어뜨리고 싶지 않아서(object로 전달하고 그 안에서 꺼내서 매핑하고 싶어서)
entitiy 위에 매핑 룰/쿼리를 쫙 쓰면서 관리하고 싶지 않아서(쿼리는 repository에서 관리하는 게 보기에도 찾기에도 좋다고 생각) 빠르게 포기했다.
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의 특징을 배울 수 있었던 시간이었다..