diff --git a/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/article/Article.kt b/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/article/Article.kt index 8611313..b62cc8f 100644 --- a/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/article/Article.kt +++ b/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/article/Article.kt @@ -8,7 +8,7 @@ import javax.persistence.* name = "article_content", pkJoinColumns = [PrimaryKeyJoinColumn(name = "id")] ) -class Article private constructor() { +class Article protected constructor() { companion object { fun create( title: String = "", diff --git a/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/product/Product.kt b/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/product/Product.kt index 3fedecf..4bf64d0 100644 --- a/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/product/Product.kt +++ b/ddd-with-jpa/src/main/kotlin/me/daniel/dddwithjpa/product/Product.kt @@ -11,7 +11,8 @@ class Product protected constructor() { @ElementCollection @CollectionTable( - name = "product_category", joinColumns = [JoinColumn(name = "product_id")] + name = "product_category", + joinColumns = [JoinColumn(name = "product_id")] ) lateinit var categoryIds: MutableSet protected set diff --git a/enhanced-performance-jpa-insert/.gitignore b/enhanced-performance-jpa-insert/.gitignore new file mode 100644 index 0000000..9b57ecd --- /dev/null +++ b/enhanced-performance-jpa-insert/.gitignore @@ -0,0 +1,34 @@ +README.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/enhanced-performance-jpa-insert/README.md b/enhanced-performance-jpa-insert/README.md new file mode 100644 index 0000000..86b3025 --- /dev/null +++ b/enhanced-performance-jpa-insert/README.md @@ -0,0 +1,178 @@ +# JPA Insert 성능 올려보기 +JPA를 사용하다보면, 불필요하게 발생하는 쿼리를 종종 볼수 있다. 물론 도메인의 상황이 select 쿼리가 반드시 필요한 상황이라면 문제가 되지 않겠지만, 경우에 따라서 insert 쿼리만 발생하도록 하는 것이 최선일 경우도 있다. 실제 업무에서 불필요하게 발생하는 select 쿼리로 인해 DBA분들에게 문의를 받기로 해서 이에 관련된 부분을 해결(?)하고자 이것저것 찾아본 내용을 정리하려고 한다. + +### Persistable 인터페이스 구현 +Data JPA 문서를 살펴보면 `Persistable`이라는 인터페이스를 언급한 부분이 있다. 인터페이스 코드를 살펴보면 간단하게 엔티티의 상태를 표현할 수 있으며, 이에 따라 실제 JpaRepository의 구현 클래스인 `SimpleJpaRepository`에서 isNew 상태를 판단하여 불필요한 Select를 줄이고 바로 Insert문을 실행할 수 있게 된다. 간단한 엔티티 구현 코드를 아래와 같다. + +```kotlin +package me.daniel.enhancedperformancejpainsert.user_access_log + +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import org.springframework.data.domain.Persistable +import java.time.LocalDateTime +import java.util.* +import javax.persistence.* + +@Entity +@Table(name = "USER_ACCESS_LOGS") +class UserAccessLog protected constructor() : Persistable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id = 0L + + var userId: String = "" + + @CreationTimestamp + lateinit var createdAt: LocalDateTime + + @UpdateTimestamp + lateinit var updatedAt: LocalDateTime + + companion object { + fun create(userId: String) = UserAccessLog().apply { + this.userId = userId + } + } + + @Transient + private var isNew = true + + override fun isNew() = isNew + + @PrePersist + @PostLoad + internal fun markNewState() { + this.isNew = false + } + + override fun getId(): String? { + TODO("Not yet implemented") + } +} +``` + +`isNew`의 상태는 `@Transient` 어노테이션을 사용하여 영속화 대상에서 배제했으며, 실제 JPA의 영속화 관련 이벤트를 잡아 @PrePersist와 @PostLoad 단계에서 상태를 다시 false로 바꾸도록 했다. 이렇게 되면 `repository.save()`를 하는 시점에는 엔티티의 isNew상태가 true일 경우 바로 insert 쿼리만 발생하게 된다. 물론 update를 처리하는 경우도 있을 수 있으니 이에 대한 처리를 위해 @PostLoad를 붙여서 해당 케이스를 피했다. + +### Batch Insert 구현 추가해보기 +사용자들의 로그를 만약 RDBMS를 통하여 적재하는 경우를 생각해보자. 로그는 대부분 수정성격의 트랜잭션이 아닌 단순 적재만 하는 케이스일 경우가 농후하다. Persistable 인터페이스를 구현하여 insert만 발생하도록 하는 것도 차선책이 될수는 있지만, 이런 작업에 매번 트랜잭션을 발생시켜 커넥션을 낭비하는 건 매우 비효율적이므로 이에 대한 처리를 해줄 수 있는 코드를 추가해보도록 하겠다. 여러 사용자들의 로그를 집계하여 저장해야하기 때문에 기존 SimpleJpaRepository의 saveAll 구현이 아닌 별도의 커스텀 쿼리 메소드를 만들어서 처리할 것이다. + +```kotlin +// (1) +@SpringBootApplication +@EnableJpaRepositories( + repositoryBaseClass = BatchRepositoryImpl::class +) +class EnhancedPerformanceJpaInsertApplication + +// (2) +@NoRepositoryBean +interface BatchRepository : JpaRepository { + fun saveInBatch(entities: Iterable) +} + +// (3) +@Transactional(propagation = Propagation.REQUIRES_NEW) +class BatchRepositoryImpl( + private val entityInformation: JpaEntityInformation, + private val entityManager: EntityManager +) : SimpleJpaRepository(entityInformation, entityManager), BatchRepository { + + @Value("\${spring.jpa.properties.hibernate.jdbc.batch_size}") + private var batchSize: Int? = 30 + + private val logger = LoggerFactory.getLogger(BatchRepository::class.java) + + override fun saveInBatch(entities: Iterable) { + val entityTransaction = entityManager + .entityManagerFactory + .createEntityManager() + .transaction + try { + entityTransaction.begin() + for ((i, entity) in entities.withIndex()) { + if (i % (batchSize ?: 30) == 0 && i > 0) { + logger.info("Flushing the EntityManager containing $batchSize entities ...") + entityTransaction.commit() + entityTransaction.begin() + entityManager.clear() + } + entityManager.persist(entity) + } + logger.info("Flushing the remaining entities ...") + entityTransaction.commit() + } catch (e: RuntimeException) { + if (entityTransaction.isActive) { + entityTransaction.rollback() + } + throw e + } finally { + entityManager.close() + } + } +} + +``` + +(1) `@EnableJpaRepositories`을 선언하고 repositoryBaseClass에 확장하려는 인터페이스 클래스를 지정해준다. +(2) 확장하려는 인터페이스 스펙을 지정해준다 +(3) 확장하려는 인터페이스 구현을 추가해준다. + +신규로 확장하는 레파지토리에는 saveInBatch에 대한 구현이 포함되어 있다. saveAll을 사용해도 되겠지만, saveAll의 경우 SimpleJpaRepository의 기본 구현을 따르기 때문에 다량의 insert가 발생할 경우 이에 대한 청크를 지정해주기 곤란하고, 트랜잭션에 대한 플러쉬/커밋 시점에 대한 지정이 어렵다. batchSize는 프로퍼티를 통하여 참조하고 해당 길이 기준으로 청크 처리하여 영속성 컨텍스트를 플러시/커밋 처리한다. + +### 로그 처리 프로세서 추가해보기 +불특정 다수의 사용자들의 로그를 수집하고 이를 batchSize만큼 모아서 데이터베이스에 플러시해야 하는 요구사항을 처리하기 위해 리액티브 스트림`Reactor`을 사용하려고 한다. 처리 프로세서는 스프링 빈(@Component)로 선언할것이며, 코드는 아래와 같다. + +```kotlin +@Component +class UserAccessLogProcessor( + private val repository: UserAccessLogRepository +) { + private val logger = LoggerFactory.getLogger(this.javaClass) + private val emitterProcessor = EmitterProcessor.create() + private val flusSink = emitterProcessor.sink() + private lateinit var disposable: Disposable + + fun send(item: UserAccessLog) { + flusSink.next(item) + } + + @PostConstruct + protected fun init() { + disposable = emitterProcessor + .bufferTimeout(30, Duration.ofSeconds(5)) + .delaySequence(Duration.ofMillis(100)) + .limitRate(2) + .parallel(20) + .runOn(Schedulers.boundedElastic()) + .doOnNext { + logger.info("items = {}", it) + } + .flatMap { + Mono.fromCallable { repository.saveInBatch(it) } + .subscribeOn(Schedulers.boundedElastic()) + } + .doOnError { + logger.error("Error", it) + } + .subscribe() + } + + @PreDestroy + protected fun destroy() { + disposable.dispose() + } + +} +``` + +- EmitterProcessor를 활용하여 불특정 다수(멀티 스레드)에서 들어오는 이벤트를 수집하여 처리 +- 이벤트에 대한 구독은 스프링 빈의 이벤트 라이프사이클을 활용했다. +- 배치 처리를 해야 하므로 이를 위해 bufferTimeout을 지정하여 개수를 배치 사이즈만큼 모아서 이벤트를 방출 +- 만약 다량의 이벤트가 동시에 들어올 경우, 이를 처리하는 다운스트림의 연산자에서 문제가 생길 수 있기 때문에 이에 대한 적절한 배압 처리를 위해 limitRate와 delaySequence를 지정해줬다.(필요하다면 배압 정책을 지정해줘도 괜찮다) +- 데이터베이스에 대한 Blocking 연산을 회피하기 위해 데이터베이스 트랜잭션 부분은 별도의 스레드에서 처리하도록 분리 + * 적절한 배압 정책이나 딜레이 정책이 없다면 디비 커넥션 고갈이나 스레드 고갈 문제를 겪을 수 밖에 없다. 논블록킹 연산을 지원하는 데이터베이스라면 모르겠지만, 지금은 JDBC 베이스의 JPA를 사용하기 때문에 이에 대한 스레드 분리는 필수적이다. + + +#### 고려헤야 할 부분 & 개선해야 할 부분은? +- 결국 메모리에 이벤트 스트림을 모아서 처리하는 로직이기 때문에, 유실에 대한 부분은 반드시 고려해야 된다. 백업 전략으로는 디스크를 통하여 이벤트 데이터를 저장하는 방법이 있을 수도 있고 아니면 별도의 구현체(큐 혹은 NoSQL)를 사용하는 방법이 있을 수 있다. \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/build.gradle.kts b/enhanced-performance-jpa-insert/build.gradle.kts new file mode 100644 index 0000000..5de4721 --- /dev/null +++ b/enhanced-performance-jpa-insert/build.gradle.kts @@ -0,0 +1,47 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "2.3.1.RELEASE" + id("io.spring.dependency-management") version "1.0.9.RELEASE" + kotlin("jvm") version "1.3.72" + kotlin("plugin.spring") version "1.3.72" + kotlin("plugin.jpa") version "1.3.72" +} + +group = "me.daniel" +version = "0.0.1-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +repositories { + mavenCentral() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + +dependencies { + implementation("io.projectreactor:reactor-core") + testImplementation("io.projectreactor:reactor-test") + + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + runtimeOnly("com.h2database:h2") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "1.8" + } +} diff --git a/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.jar b/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.jar differ diff --git a/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.properties b/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a4f0001 --- /dev/null +++ b/enhanced-performance-jpa-insert/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/enhanced-performance-jpa-insert/gradlew b/enhanced-performance-jpa-insert/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/enhanced-performance-jpa-insert/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/enhanced-performance-jpa-insert/gradlew.bat b/enhanced-performance-jpa-insert/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/enhanced-performance-jpa-insert/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/enhanced-performance-jpa-insert/settings.gradle.kts b/enhanced-performance-jpa-insert/settings.gradle.kts new file mode 100644 index 0000000..380c658 --- /dev/null +++ b/enhanced-performance-jpa-insert/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "enhanced-performance-jpa-insert" diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplication.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplication.kt new file mode 100644 index 0000000..eeff8f6 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplication.kt @@ -0,0 +1,16 @@ +package me.daniel.enhancedperformancejpainsert + +import me.daniel.enhancedperformancejpainsert.support.BatchRepositoryImpl +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaRepositories + +@SpringBootApplication +@EnableJpaRepositories( + repositoryBaseClass = BatchRepositoryImpl::class +) +class EnhancedPerformanceJpaInsertApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepository.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepository.kt new file mode 100644 index 0000000..73ab941 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepository.kt @@ -0,0 +1,10 @@ +package me.daniel.enhancedperformancejpainsert.support + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.NoRepositoryBean + + +@NoRepositoryBean +interface BatchRepository : JpaRepository { + fun saveInBatch(entities: Iterable) +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImpl.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImpl.kt new file mode 100644 index 0000000..721921f --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImpl.kt @@ -0,0 +1,51 @@ +package me.daniel.enhancedperformancejpainsert.support + +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.jpa.repository.support.JpaEntityInformation +import org.springframework.data.jpa.repository.support.SimpleJpaRepository +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.io.Serializable +import javax.persistence.EntityManager + +@Transactional(propagation = Propagation.REQUIRES_NEW) +class BatchRepositoryImpl( + private val entityInformation: JpaEntityInformation, + private val entityManager: EntityManager +) : SimpleJpaRepository(entityInformation, entityManager), BatchRepository { + + @Value("\${spring.jpa.properties.hibernate.jdbc.batch_size}") + private var batchSize: Int? = 30 + + private val logger = LoggerFactory.getLogger(BatchRepository::class.java) + + override fun saveInBatch(entities: Iterable) { + val entityTransaction = entityManager + .entityManagerFactory + .createEntityManager() + .transaction + try { + entityTransaction.begin() + for ((i, entity) in entities.withIndex()) { + if (i % (batchSize ?: 30) == 0 && i > 0) { + logger.info("Flushing the EntityManager containing $batchSize entities ...") + entityTransaction.commit() + entityTransaction.begin() + entityManager.clear() + } + entityManager.persist(entity) + } + logger.info("Flushing the remaining entities ...") + entityTransaction.commit() + } catch (e: RuntimeException) { + if (entityTransaction.isActive) { + entityTransaction.rollback() + } + throw e + } finally { + entityManager.close() + } + } +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/RepositoryProfiler.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/RepositoryProfiler.kt new file mode 100644 index 0000000..e0af9cf --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/support/RepositoryProfiler.kt @@ -0,0 +1,37 @@ +package me.daniel.enhancedperformancejpainsert.support + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + + +@Aspect +@Component +class RepositoryProfiler { + var logger: Logger = LoggerFactory.getLogger(this.javaClass) + + @Pointcut("execution(public * org.springframework.data.repository.Repository+.*(..))") + fun intercept() {} + + @Around("intercept()") + fun profile(joinPoint: ProceedingJoinPoint): Any? { + val startMs = System.currentTimeMillis() + var result: Any? = null + try { + result = joinPoint.proceed() + } catch (e: Throwable) { + logger.error(e.message, e) + // do whatever you want with the exception + } + val elapsedMs = System.currentTimeMillis() - startMs + // you may like to use logger.debug + logger.info(joinPoint.target.toString() + "." + joinPoint.signature + ": Execution time: " + elapsedMs + " ms") + // pay attention that this line may return null + return result + } + +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLog.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLog.kt new file mode 100644 index 0000000..a1602e6 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLog.kt @@ -0,0 +1,48 @@ +package me.daniel.enhancedperformancejpainsert.user_access_log + +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import org.springframework.data.domain.Persistable +import java.time.LocalDateTime +import java.util.* +import javax.persistence.* + +@Entity +@Table(name = "USER_ACCESS_LOGS") +class UserAccessLog protected constructor() : Persistable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id = 0L + + var userId: String = "" + + private lateinit var createdAt: LocalDateTime + + companion object { + fun create(userId: String) = UserAccessLog().apply { + this.userId = userId + this.createdAt = LocalDateTime.now() + } + } + + @Transient + private var isNew = true + + override fun isNew() = isNew + + @PrePersist + @PostLoad + internal fun markNewState() { + this.isNew = false + } + + override fun getId(): String? { + TODO("Not yet implemented") + } + + override fun toString(): String { + return "UserAccessLog(id=$id, userId='$userId', createdAt=$createdAt)" + } + + +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessor.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessor.kt new file mode 100644 index 0000000..69ae0ed --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessor.kt @@ -0,0 +1,53 @@ +package me.daniel.enhancedperformancejpainsert.user_access_log + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import reactor.core.Disposable +import reactor.core.publisher.EmitterProcessor +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import java.time.Duration +import javax.annotation.PostConstruct +import javax.annotation.PreDestroy + + +@Component +class UserAccessLogProcessor( + private val repository: UserAccessLogRepository +) { + private val logger = LoggerFactory.getLogger(this.javaClass) + private val emitterProcessor = EmitterProcessor.create() + private val flusSink = emitterProcessor.sink() + private lateinit var disposable: Disposable + + fun send(item: UserAccessLog) { + flusSink.next(item) + } + + @PostConstruct + protected fun init() { + disposable = emitterProcessor + .bufferTimeout(30, Duration.ofSeconds(5)) + .delaySequence(Duration.ofMillis(100)) + .limitRate(2) + .parallel(20) + .runOn(Schedulers.boundedElastic()) + .doOnNext { + logger.info("items = {}", it) + } + .flatMap { + Mono.fromCallable { repository.saveInBatch(it) } + .subscribeOn(Schedulers.boundedElastic()) + } + .doOnError { + logger.error("Error", it) + } + .subscribe() + } + + @PreDestroy + protected fun destroy() { + disposable.dispose() + } + +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepository.kt b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepository.kt new file mode 100644 index 0000000..af15b2d --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepository.kt @@ -0,0 +1,7 @@ +package me.daniel.enhancedperformancejpainsert.user_access_log + +import me.daniel.enhancedperformancejpainsert.support.BatchRepository +import org.springframework.stereotype.Repository + +@Repository +interface UserAccessLogRepository: BatchRepository \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/main/resources/application.properties b/enhanced-performance-jpa-insert/src/main/resources/application.properties new file mode 100644 index 0000000..826e416 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.jpa.properties.hibernate.jdbc.batch_size = 30 +spring.jpa.show-sql=true + +#spring.datasource.hikari.maximum-pool-size=30 \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplicationTests.kt b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplicationTests.kt new file mode 100644 index 0000000..c31797f --- /dev/null +++ b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/EnhancedPerformanceJpaInsertApplicationTests.kt @@ -0,0 +1,13 @@ +package me.daniel.enhancedperformancejpainsert + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class EnhancedPerformanceJpaInsertApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImplTest.kt b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImplTest.kt new file mode 100644 index 0000000..e62988a --- /dev/null +++ b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/support/BatchRepositoryImplTest.kt @@ -0,0 +1,40 @@ +package me.daniel.enhancedperformancejpainsert.support + +import me.daniel.enhancedperformancejpainsert.EnhancedPerformanceJpaInsertApplication +import me.daniel.enhancedperformancejpainsert.user_access_log.UserAccessLog +import me.daniel.enhancedperformancejpainsert.user_access_log.UserAccessLogRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + + +@SpringBootTest(classes = [EnhancedPerformanceJpaInsertApplication::class]) +internal class BatchRepositoryImplTest { + @Autowired + private lateinit var repository: UserAccessLogRepository + + @Test + @DisplayName("Batch Insert Test") + internal fun `saveInBatchTest`() { + // Given + val list = mutableListOf(); + for (i in 1..100) { + list.add(UserAccessLog.create("$i")) + } + // When && Then + repository.saveInBatch(list) + } + + @Test + @DisplayName("saveAll Test") + internal fun `saveAllTest`() { + // Given + val list = mutableListOf(); + for (i in 1..100) { + list.add(UserAccessLog.create("$i")) + } + // When && Then + repository.saveAll(list) + } +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessorTest.kt b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessorTest.kt new file mode 100644 index 0000000..4bc1438 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogProcessorTest.kt @@ -0,0 +1,20 @@ +package me.daniel.enhancedperformancejpainsert.user_access_log + +import me.daniel.enhancedperformancejpainsert.EnhancedPerformanceJpaInsertApplication +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest(classes = [EnhancedPerformanceJpaInsertApplication::class]) +internal class UserAccessLogProcessorTest { + @Autowired + private lateinit var processor: UserAccessLogProcessor + + @Test + internal fun processorTest() { + repeat(600) { + processor.send(UserAccessLog.create("userId=$it")) + } + Thread.sleep(1000 * 30L) + } +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepositoryTest.kt b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepositoryTest.kt new file mode 100644 index 0000000..35ce934 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/test/kotlin/me/daniel/enhancedperformancejpainsert/user_access_log/UserAccessLogRepositoryTest.kt @@ -0,0 +1,53 @@ +package me.daniel.enhancedperformancejpainsert.user_access_log + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.transaction.TestTransaction + +@DisplayName("Repository Test") +@DataJpaTest +internal class UserAccessLogRepositoryTest { + @Autowired + private lateinit var repository: UserAccessLogRepository + + @Autowired + private lateinit var entityManager: TestEntityManager + + @DisplayName("Only Insert Test") + @Test + fun `Only Insert 테스트`() { + // Given + val entity = UserAccessLog.create("userId") + assertThat(entity.isNew).isEqualTo(true) + // When + val persistedEntity = repository.save(entity) + // Then + assertThat(persistedEntity).isNotNull + assertThat(persistedEntity.isNew).isEqualTo(false) + } + + @DisplayName("Select-PostLoad-isNewState-Check") + @Test + fun `Select-PostLoad-isNewState-Check`() { + // Given + if (TestTransaction.isActive()) TestTransaction.end() + TestTransaction.start() + val entity = entityManager.persistAndFlush(repository.save(UserAccessLog.create("userId_1"))) + TestTransaction.flagForCommit() + TestTransaction.end() + // When + val target = repository.findByIdOrNull(entity.id) ?: throw RuntimeException("Not Founded Entity") + // Then + assertThat(target).isNotNull + assertThat(target.isNew).isEqualTo(false) + + } + + +} \ No newline at end of file diff --git a/enhanced-performance-jpa-insert/src/test/resources/application-test.properties b/enhanced-performance-jpa-insert/src/test/resources/application-test.properties new file mode 100644 index 0000000..9960df3 --- /dev/null +++ b/enhanced-performance-jpa-insert/src/test/resources/application-test.properties @@ -0,0 +1 @@ +spring.jpa.properties.hibernate.jdbc.batch_size = 30 \ No newline at end of file diff --git a/jpa-reactor/.gitignore b/jpa-reactor/.gitignore new file mode 100644 index 0000000..236a8b3 --- /dev/null +++ b/jpa-reactor/.gitignore @@ -0,0 +1,31 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/jpa-reactor/README.md b/jpa-reactor/README.md new file mode 100644 index 0000000..d86f9ab --- /dev/null +++ b/jpa-reactor/README.md @@ -0,0 +1,45 @@ +# Webflux + JPA 강제로 끼워 맞춰보기 + +### Why? +- JDBC를 Reactive하게 사용할 수 있는 안정적인 방법은 아직 존재하지 않음 + - 물론 R2DBC와 같은 구현체가 존재하지만, 아직 1.0.0 RELEASE가 나오지 않은 상황 +- 스프링 공식 문서를 참고해보면, 굳이 Reactive 모델에 JDBC와 같은 Block API를 사용하는 것보단 기존 Spring MVC를 사용하는 것이 차라리 나을 수 있다고 언급이 되어 있는 부분도 있음 + +### 그럼에도 불구하고 왜? +- 웹플럭스 그리고 리엑티브에 대한 학습 + +### 그렇다면 굳이 끼워 맞추기를 했을 때 그나마 나은 프렉티스는? +- 동기 구간에 대한 `Scheduler`를 지정하여 사용하자 + - 리액터의 `Schedulers.boundedElastic` 혹은 `Schedulers.elastic`과 같은 스케줄러를 사용 + ```java + repository.findById(id).subscribeOn(Schedulers.boundedElastic()); + ``` + - 스케줄러의 스레드 개수는 DB 커넥션풀 사이즈와 동일하게 맞춤 + ```java + @Configuration + public class SchedulerConfiguration { + private final Integer connectionPoolSize; + + public SchedulerConfiguration(@Value("${spring.datasource.hikari.maximum-pool-size}") Integer connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + } + + @Bean + @Primary + public Scheduler boundedElasticScheduler() { + return Schedulers.newBoundedElastic( + connectionPoolSize, + connectionPoolSize / 2, + "boundedElasticScheduler" + ); + } + } + ``` +### 아직 이해하지 못한 내용은? + - 스케줄링하는 큐에 대한 사이즈를 어떻게 지정하는게 좋을까? + - 적절한 DB 트랜잭션 방법은? + +### 참고링크 +- [Spring webflux and reading from database](https://stackoverflow.com/questions/42299455/spring-webflux-and-reading-from-database) +- [Project Reactor](https://kwonnam.pe.kr/wiki/reactive_programming/reactor) +- [리액티브하게 리팩토링하기 - JDBC 마이그레이션 해부](http://blog.lespinside.com/refactoring-to-react/) diff --git a/jpa-reactor/build.gradle b/jpa-reactor/build.gradle new file mode 100644 index 0000000..e3ca23d --- /dev/null +++ b/jpa-reactor/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'org.springframework.boot' version '2.2.2.RELEASE' + id 'io.spring.dependency-management' version '1.0.8.RELEASE' + id 'java' +} + +group = 'me.bazzi' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + compileOnly 'org.projectlombok:lombok' + implementation 'com.h2database:h2' + annotationProcessor 'org.projectlombok:lombok' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation 'io.projectreactor:reactor-test' +} + +test { + useJUnitPlatform() +} diff --git a/jpa-reactor/gradle/wrapper/gradle-wrapper.jar b/jpa-reactor/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..29953ea Binary files /dev/null and b/jpa-reactor/gradle/wrapper/gradle-wrapper.jar differ diff --git a/jpa-reactor/gradle/wrapper/gradle-wrapper.properties b/jpa-reactor/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9492014 --- /dev/null +++ b/jpa-reactor/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/jpa-reactor/gradlew b/jpa-reactor/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/jpa-reactor/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/jpa-reactor/gradlew.bat b/jpa-reactor/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/jpa-reactor/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jpa-reactor/settings.gradle b/jpa-reactor/settings.gradle new file mode 100644 index 0000000..658758f --- /dev/null +++ b/jpa-reactor/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jpa-reactor' diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/Action.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/Action.java new file mode 100644 index 0000000..a1e2c9a --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/Action.java @@ -0,0 +1,6 @@ +package me.bazzi.jpareactor; + +@FunctionalInterface +public interface Action { + void run(); +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/IMessageRepository.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/IMessageRepository.java new file mode 100644 index 0000000..cbc5eb1 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/IMessageRepository.java @@ -0,0 +1,5 @@ +package me.bazzi.jpareactor; + +import org.springframework.data.jpa.repository.JpaRepository; + +interface IMessageRepository extends JpaRepository {} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/JpaReactorApplication.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/JpaReactorApplication.java new file mode 100644 index 0000000..57bd817 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/JpaReactorApplication.java @@ -0,0 +1,11 @@ +package me.bazzi.jpareactor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JpaReactorApplication { + public static void main(String[] args) { + SpringApplication.run(JpaReactorApplication.class, args); + } +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/Message.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/Message.java new file mode 100644 index 0000000..a506c15 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/Message.java @@ -0,0 +1,44 @@ +package me.bazzi.jpareactor; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.springframework.util.Assert; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Message { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String userId; + private String contents; + @Enumerated(EnumType.STRING) + private ReadYn readYn; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + public enum ReadYn { + READ, UN_READ + } + + @Builder + public Message(String userId, String contents) { + Assert.notNull(userId, "required userId"); + Assert.notNull(contents, "required contents"); + this.userId = userId; + this.readYn = ReadYn.READ; + this.contents = contents; + } +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageController.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageController.java new file mode 100644 index 0000000..cdada2d --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageController.java @@ -0,0 +1,49 @@ +package me.bazzi.jpareactor; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; + +@RestController +@RequiredArgsConstructor +public class MessageController { + private final MessageRepository repository; + private final IMessageRepository iMessageRepository; + private final ReactiveTransactionAdapter adapter; + + @GetMapping("/message") + Mono get() { + return repository.save(Message.builder().userId("userId").contents("contents").build()).log(); + } + + @GetMapping("/bulk") + Flux list() { + return repository.saveAll( + Arrays.asList( + Message.builder().userId("userId").contents("contents").build(), + Message.builder().userId("userId").contents("contents").build() + ) + ).log(); + } + + @GetMapping("/{id}") + Mono list(@PathVariable Long id) { + return repository.findById(id).log(); + } + + + @PostMapping + Mono create() { + return adapter.doTransaction(() -> { + Message message = Message.builder().userId("userId").contents("contents").build(); + return iMessageRepository.save(message); + }); + } + +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageRepository.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageRepository.java new file mode 100644 index 0000000..a27e2a9 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/MessageRepository.java @@ -0,0 +1,11 @@ +package me.bazzi.jpareactor; + +import org.springframework.stereotype.Repository; +import reactor.core.scheduler.Scheduler; + +@Repository +public class MessageRepository extends ReactiveRepositoryAdapter { + public MessageRepository(IMessageRepository repository, Scheduler scheduler) { + super(repository, scheduler); + } +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveRepositoryAdapter.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveRepositoryAdapter.java new file mode 100644 index 0000000..40dbedd --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveRepositoryAdapter.java @@ -0,0 +1,150 @@ +package me.bazzi.jpareactor; + +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +@RequiredArgsConstructor +public abstract class ReactiveRepositoryAdapter> implements ReactiveCrudRepository { + + protected final I delegate; + protected final Scheduler scheduler; + + @Override + public Mono save(S entity) { + return Mono + .fromCallable(() -> delegate.save(entity)) + .subscribeOn(scheduler); + } + + @Override + public Flux saveAll(Iterable entities) { + return Mono.fromCallable(() -> delegate.saveAll(entities)) + .flatMapMany(Flux::fromIterable) + .subscribeOn(scheduler); + } + + @Override + public Flux saveAll(Publisher entityStream) { + return Flux.from(entityStream) + .flatMap(entity -> Mono.fromCallable(() -> delegate.save(entity))) + .subscribeOn(scheduler); + } + + @Override + public Mono findById(ID id) { + return Mono.fromCallable(() -> delegate.findById(id)) + .flatMap(result -> result + .map(Mono::just) + .orElseGet(Mono::empty)) + .subscribeOn(scheduler); + } + + @Override + public Mono findById(Publisher id) { + return Mono.from(id) + .flatMap(actualId -> + delegate.findById(actualId) + .map(Mono::just) + .orElseGet(Mono::empty)) + .subscribeOn(scheduler); + } + + @Override + public Mono existsById(ID id) { + return Mono + .fromCallable(() -> delegate.existsById(id)) + .subscribeOn(scheduler); + } + + @Override + public Mono existsById(Publisher id) { + return Mono.from(id) + .flatMap(actualId -> + Mono.fromCallable(() -> delegate.existsById(actualId))) + .subscribeOn(scheduler); + } + + @Override + public Flux findAll() { + return Mono + .fromCallable(delegate::findAll) + .flatMapMany(Flux::fromIterable) + .subscribeOn(scheduler); + } + + @Override + public Flux findAllById(Iterable ids) { + return Mono + .fromCallable(() -> delegate.findAllById(ids)) + .flatMapMany(Flux::fromIterable) + .subscribeOn(scheduler); + } + + @Override + public Flux findAllById(Publisher idStream) { + return Flux + .from(idStream) + .buffer() + .flatMap(ids -> Flux.fromIterable(delegate.findAllById(ids))) + .subscribeOn(scheduler); + } + + @Override + public Mono count() { + return Mono + .fromCallable(delegate::count) + .subscribeOn(scheduler); + } + + @Override + public Mono deleteById(ID id) { + return Mono + .fromRunnable(() -> delegate.deleteById(id)) + .subscribeOn(scheduler); + } + + @Override + public Mono deleteById(Publisher id) { + return Mono.from(id) + .flatMap(actualId -> + Mono + .fromRunnable(() -> delegate.deleteById(actualId)) + .subscribeOn(scheduler) + ); + } + + @Override + public Mono delete(T entity) { + return Mono + .fromRunnable(() -> delegate.delete(entity)) + .subscribeOn(scheduler); + } + + @Override + public Mono deleteAll(Iterable entities) { + return Mono + .fromRunnable(() -> delegate.deleteAll(entities)) + .subscribeOn(scheduler); + } + + @Override + public Mono deleteAll(Publisher entityStream) { + return Flux.from(entityStream) + .flatMap(entity -> Mono + .fromRunnable(() -> delegate.delete(entity)) + .subscribeOn(scheduler)) + .then(); + } + + @Override + public Mono deleteAll() { + return Mono + .fromRunnable(delegate::deleteAll) + .subscribeOn(scheduler); + } +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveTransactionAdapter.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveTransactionAdapter.java new file mode 100644 index 0000000..4452d39 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/ReactiveTransactionAdapter.java @@ -0,0 +1,59 @@ +package me.bazzi.jpareactor; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import java.util.function.Supplier; + +@RequiredArgsConstructor +@Component +@Slf4j +public class ReactiveTransactionAdapter { + private final PlatformTransactionManager transactionManager; + private final Scheduler scheduler; + + public Mono doTransaction(Supplier supplier) { + return Mono.fromCallable(() -> new TransactionTemplate(transactionManager).execute(transactionStatus -> { + try { + log.info("doTransaction started."); + T t = supplier.get(); + log.info("doTransaction complete."); + return t; + } catch (Exception e) { + log.error("doTransaction Failure", e); + transactionStatus.setRollbackOnly(); + // TODO: 에러처리 레핑해야 함 + throw e; + } + })).subscribeOn(scheduler); + } + + public Mono doTransactionWithoutResult(Action action) { + return Mono.fromRunnable(() -> new TransactionTemplate(transactionManager).executeWithoutResult(transactionStatus -> { + try { + log.info("doTransactionWithoutResult started"); + action.run(); + } catch (Exception e) { + log.error("doTransaction Failure", e); + transactionStatus.setRollbackOnly(); + } + })).subscribeOn(scheduler).then(); +// return Mono.just(new TransactionTemplate(transactionManager)) +// .flatMap(transactionTemplate -> Mono.fromRunnable(() -> transactionTemplate.executeWithoutResult(transactionStatus -> { +// try { +// action.run(); +// } catch (Exception e) { +// log.error("Transaction Failure", e); +// transactionStatus.setRollbackOnly(); +// } +// })).subscribeOn(scheduler).then()); + } + + +} diff --git a/jpa-reactor/src/main/java/me/bazzi/jpareactor/SchedulerConfiguration.java b/jpa-reactor/src/main/java/me/bazzi/jpareactor/SchedulerConfiguration.java new file mode 100644 index 0000000..c8b77d3 --- /dev/null +++ b/jpa-reactor/src/main/java/me/bazzi/jpareactor/SchedulerConfiguration.java @@ -0,0 +1,57 @@ +package me.bazzi.jpareactor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.event.EventListener; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.util.concurrent.Executors; + +@Configuration +public class SchedulerConfiguration { + private final Integer connectionPoolSize; + + public SchedulerConfiguration(@Value("${spring.datasource.hikari.maximum-pool-size}") Integer connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + } + + @Bean + @ConditionalOnMissingBean(Scheduler.class) + public Scheduler jdbcScheduler() { + return Schedulers.fromExecutor(Executors.newFixedThreadPool(connectionPoolSize)); + } + + /** + * https://stackoverflow.com/questions/58513071/spring-mvc-to-spring-webflux-migration-block-vs-subscribe + * @return + */ + @Bean + @Primary + public Scheduler boundedElasticScheduler() { + return Schedulers.newBoundedElastic( + connectionPoolSize, + connectionPoolSize / 2, + "boundedElasticScheduler" + ); + } + + private org.h2.tools.Server webServer; + private org.h2.tools.Server server; + + @EventListener(org.springframework.context.event.ContextRefreshedEvent.class) + public void start() throws java.sql.SQLException { + this.webServer = org.h2.tools.Server.createWebServer("-webPort", "8082", "-tcpAllowOthers").start(); + this.server = org.h2.tools.Server.createTcpServer("-tcpPort", "9092", "-tcpAllowOthers").start(); + } + + @EventListener(org.springframework.context.event.ContextClosedEvent.class) + public void stop() { + this.webServer.stop(); + this.server.stop(); + } + +} diff --git a/jpa-reactor/src/main/resources/application.properties b/jpa-reactor/src/main/resources/application.properties new file mode 100644 index 0000000..a65260e --- /dev/null +++ b/jpa-reactor/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.datasource.hikari.maximum-pool-size=1000 +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create-drop + + +spring.jpa.open-in-view=false + +spring.h2.console.enabled=true + +spring.h2.console.path=/h2-console +spring.main.web-application-type=reactive \ No newline at end of file diff --git a/jpa-reactor/src/test/java/me/bazzi/jpareactor/JpaReactorApplicationTests.java b/jpa-reactor/src/test/java/me/bazzi/jpareactor/JpaReactorApplicationTests.java new file mode 100644 index 0000000..479a887 --- /dev/null +++ b/jpa-reactor/src/test/java/me/bazzi/jpareactor/JpaReactorApplicationTests.java @@ -0,0 +1,13 @@ +package me.bazzi.jpareactor; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JpaReactorApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/jpa-reactor/src/test/java/me/bazzi/jpareactor/ReactiveWrapperRepositoryTest.java b/jpa-reactor/src/test/java/me/bazzi/jpareactor/ReactiveWrapperRepositoryTest.java new file mode 100644 index 0000000..89df325 --- /dev/null +++ b/jpa-reactor/src/test/java/me/bazzi/jpareactor/ReactiveWrapperRepositoryTest.java @@ -0,0 +1,14 @@ +package me.bazzi.jpareactor; + + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +public class ReactiveWrapperRepositoryTest { + + + + + +} diff --git a/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/buildOutputCleanup.lock index a42f5dc..d793c6b 100644 Binary files a/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/outputFiles.bin b/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/outputFiles.bin index d141f5c..ddce393 100644 Binary files a/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/outputFiles.bin and b/kotlin-spring-boot-mapstruct/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/KotlinSpringBootQuerydslApplication.kt b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/KotlinSpringBootQuerydslApplication.kt index b66f40c..29c5bca 100644 --- a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/KotlinSpringBootQuerydslApplication.kt +++ b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/KotlinSpringBootQuerydslApplication.kt @@ -3,6 +3,7 @@ package me.daniel.kotlinspringbootquerydsl import me.daniel.kotlinspringbootquerydsl.api.person.entity.Address import me.daniel.kotlinspringbootquerydsl.api.person.entity.Person import me.daniel.kotlinspringbootquerydsl.api.person.PersonRepository +import me.daniel.kotlinspringbootquerydsl.api.person.TestRepository import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -13,6 +14,7 @@ class KotlinSpringBootQuerydslApplication( private val personRepository: PersonRepository ) : CommandLineRunner { override fun run(vararg args: String?) { + val findAllBy = personRepository.findAllBy() for (i in 1..100) { val name = UUID.randomUUID().toString() + i personRepository.save(Person( diff --git a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonController.kt b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonController.kt index c374a8e..e75214f 100644 --- a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonController.kt +++ b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonController.kt @@ -14,7 +14,8 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/persons") class PersonController( - private val personService: PersonService + private val personService: PersonService, + private val testRepository: TestRepository ) { @GetMapping @@ -35,5 +36,9 @@ class PersonController( @PathVariable id: Long ) = personService.get(id) + @GetMapping("/test") + fun list(): MutableList? { + return testRepository.findAllBy() + } } \ No newline at end of file diff --git a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonRepository.kt b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonRepository.kt index 4d7fdc7..47281c8 100644 --- a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonRepository.kt +++ b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/PersonRepository.kt @@ -1,6 +1,7 @@ package me.daniel.kotlinspringbootquerydsl.api.person import com.querydsl.core.types.Predicate +import com.querydsl.jpa.impl.JPAQueryFactory import me.daniel.kotlinspringbootquerydsl.api.person.entity.Person import me.daniel.kotlinspringbootquerydsl.api.person.entity.QAddress import me.daniel.kotlinspringbootquerydsl.api.person.entity.QPerson @@ -11,12 +12,15 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer import org.springframework.data.querydsl.binding.QuerydslBindings +import org.springframework.transaction.annotation.Transactional +import javax.persistence.EntityManager interface PersonRepositoryWrapper { fun search(predicate: Predicate, pageable: Pageable) : Page + fun findAllBy(): List } interface PersonRepository: JpaRepository, PersonRepositoryWrapper -class PersonRepositoryImpl: QuerydslRepositorySupport(Person::class.java), PersonRepositoryWrapper, QuerydslBinderCustomizer { +class PersonRepositoryImpl(entityManager: EntityManager): QuerydslRepositorySupport(Person::class.java), PersonRepositoryWrapper, QuerydslBinderCustomizer { companion object { private val person = QPerson.person!! private val address = QAddress.address!! @@ -33,6 +37,11 @@ class PersonRepositoryImpl: QuerydslRepositorySupport(Person::class.java), Perso val page = querydsl!!.applyPagination(pageable, query).fetch() ?: emptyList() return PageImpl(page, pageable, if (page.isEmpty()) 0L else query.fetchCount()) } + + override fun findAllBy(): List { + val fetch = JPAQueryFactory(entityManager).select().from(person, address).fetch() + return fetch + } } diff --git a/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/TestRepository.kt b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/TestRepository.kt new file mode 100644 index 0000000..40e3b3a --- /dev/null +++ b/kotlin-spring-boot-querydsl/src/main/kotlin/me/daniel/kotlinspringbootquerydsl/api/person/TestRepository.kt @@ -0,0 +1,31 @@ +package me.daniel.kotlinspringbootquerydsl.api.person + +import com.querydsl.jpa.impl.JPAQueryFactory +import me.daniel.kotlinspringbootquerydsl.api.person.entity.Person +import me.daniel.kotlinspringbootquerydsl.api.person.entity.QAddress +import me.daniel.kotlinspringbootquerydsl.api.person.entity.QPerson +import org.springframework.stereotype.Repository +import javax.persistence.EntityManager +import javax.persistence.PersistenceContext + +@Repository +class TestRepository { + companion object { + private val person = QPerson.person!! + private val address = QAddress.address!! + } + private lateinit var queryFactory: JPAQueryFactory + + + @PersistenceContext + fun setEntityManager(entityManager: EntityManager) { + this.queryFactory = JPAQueryFactory(entityManager) + } + + fun findAllBy(): MutableList { + return queryFactory.from(person).fetch() as MutableList + } + + + +} \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/.gitignore b/kotlin-spring-jpa-persistence-context/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/kotlin-spring-jpa-persistence-context/build.gradle.kts b/kotlin-spring-jpa-persistence-context/build.gradle.kts new file mode 100644 index 0000000..4beb528 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/build.gradle.kts @@ -0,0 +1,44 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("plugin.jpa") version "1.3.41" + id("org.springframework.boot") version "2.1.7.RELEASE" + id("io.spring.dependency-management") version "1.0.7.RELEASE" + kotlin("jvm") version "1.3.41" + kotlin("plugin.spring") version "1.3.41" +} + +group = "me.daniel" +version = "0.0.1-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +repositories { + mavenCentral() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} +noArg { + annotation("javax.persistence.Entity") + annotation("javax.persistence.Embeddable") +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + runtimeOnly("com.h2database:h2") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "1.8" + } +} diff --git a/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.jar b/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.properties b/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f4d7b2b --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-spring-jpa-persistence-context/gradlew b/kotlin-spring-jpa-persistence-context/gradlew new file mode 100755 index 0000000..af6708f --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/kotlin-spring-jpa-persistence-context/gradlew.bat b/kotlin-spring-jpa-persistence-context/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-spring-jpa-persistence-context/settings.gradle.kts b/kotlin-spring-jpa-persistence-context/settings.gradle.kts new file mode 100644 index 0000000..e3c54f2 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "spring-jpa-persistence-context" diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplication.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplication.kt new file mode 100644 index 0000000..26ceba5 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplication.kt @@ -0,0 +1,40 @@ +package me.daniel.springjpapersistencecontext + +import me.daniel.springjpapersistencecontext.order.Order +import me.daniel.springjpapersistencecontext.order.OrderProduct +import me.daniel.springjpapersistencecontext.order.OrderRepository +import me.daniel.springjpapersistencecontext.order.OrderStatus +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.CommandLineRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class SpringJpaPersistenceContextApplication: CommandLineRunner { + + @Autowired + private lateinit var repository: OrderRepository + + + override fun run(vararg args: String?) { + val orders = mutableListOf() + for (i in (1..100).map { it.toLong() }) { + orders.add( + Order.doOrder( + memberId = i, + status = OrderStatus.PAYMENT_WAITING, + products = listOf( + OrderProduct(productId = i, price = 10000, quantity = 1, amounts = 10000, lineIdx = 0) + ) + ) + ) + } + repository.saveAll(orders) + + } + +} + +fun main(args: Array) { + runApplication(*args) +} diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/NotExistsOrderException.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/NotExistsOrderException.kt new file mode 100644 index 0000000..44b9b48 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/NotExistsOrderException.kt @@ -0,0 +1,8 @@ +package me.daniel.springjpapersistencecontext.order + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus +import java.lang.RuntimeException + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +class NotExistsOrderException(val msg: String) : RuntimeException(msg) \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/Order.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/Order.kt new file mode 100644 index 0000000..0328821 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/Order.kt @@ -0,0 +1,78 @@ +package me.daniel.springjpapersistencecontext.order + +import org.hibernate.annotations.BatchSize +import org.hibernate.annotations.CreationTimestamp +import org.hibernate.annotations.UpdateTimestamp +import java.time.LocalDateTime +import javax.persistence.* + + +@Entity +@Table(name = "orders") +class Order protected constructor() { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L + protected set + + @Column(nullable = false, updatable = false) + var memberId: Long = 0L + protected set + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + var status: OrderStatus = OrderStatus.PAYMENT_WAITING + protected set + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable( + name = "order_products", + joinColumns = [ + JoinColumn(name = "id") + ] + ) + @AttributeOverrides( + value = [ + AttributeOverride(name = "productId", column = Column(name = "product_id")), + AttributeOverride(name = "price", column = Column(name = "price")), + AttributeOverride(name = "quantity", column = Column(name = "quantity")), + AttributeOverride(name = "amounts", column = Column(name = "amounts")), + AttributeOverride(name = "line_idx", column = Column(name = "line_idx")) + ] + ) + @BatchSize(size = 10) + var orderProducts: MutableList = mutableListOf() + protected set + + @CreationTimestamp + @Column(nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now() + protected set + + @UpdateTimestamp + @Column(nullable = false) + var updatedAt: LocalDateTime = LocalDateTime.now() + protected set + + companion object { + fun doOrder( + memberId: Long, + status: OrderStatus, + products: List + ): Order { + if (status !in listOf(OrderStatus.PAYMENT_WAITING, OrderStatus.PATMENT_COMPLETED)) + throw IllegalArgumentException("올바른 주문 상태 값이 아닙니다.") + if (products.isEmpty()) + throw IllegalArgumentException("최소 한개 이상의 구매 상품을 포함해야 합니다.") + return Order().also { + it.memberId = memberId + it.status = status + it.orderProducts = products.toMutableList() + } + } + + + } + +} \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderController.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderController.kt new file mode 100644 index 0000000..d5bf5e5 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderController.kt @@ -0,0 +1,35 @@ +package me.daniel.springjpapersistencecontext.order + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.* + + +@RestController +@RequestMapping("/v1/orders") +class OrderController( + private val orderService: OrderService +) { + + @GetMapping + fun list( + @PageableDefault(size = 5, page = 0) + pageable: Pageable + ): ResponseEntity> { + return ResponseEntity.ok(orderService.getList(pageable)) + } + + @GetMapping("/{id}") + fun get( + @PathVariable id: Long = 0L + ): ResponseEntity { + return ResponseEntity.ok(orderService.get(id)) + } + +} \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderProduct.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderProduct.kt new file mode 100644 index 0000000..c92a307 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderProduct.kt @@ -0,0 +1,12 @@ +package me.daniel.springjpapersistencecontext.order + +import javax.persistence.* + +@Embeddable +data class OrderProduct( + val productId: Long = 0L, + val price: Long, + val quantity: Int, + val amounts: Long, + val lineIdx: Int +) \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderRepository.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderRepository.kt new file mode 100644 index 0000000..cd9f696 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderRepository.kt @@ -0,0 +1,7 @@ +package me.daniel.springjpapersistencecontext.order + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository: JpaRepository \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderService.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderService.kt new file mode 100644 index 0000000..ede60f9 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderService.kt @@ -0,0 +1,34 @@ +package me.daniel.springjpapersistencecontext.order + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class OrderService( + private val orderRepository: OrderRepository +) { + fun getList(pageable: Pageable): Page { + val list = orderRepository.findAll(pageable) +// for (order in list) { +// // 실제 값을 사용할 때 프록시 객체가 초기화 된다. +// order.orderProducts.forEach { it.amounts } +// } + for (order in list) { + // 실제 값을 사용할 때 프록시 객체가 초기화 된다. + order.orderProducts.forEach { it.amounts } + } + return list + } + + fun get(id: Long): Order { + val order = (orderRepository.findByIdOrNull(id) + ?: throw NotExistsOrderException("존재하지 않은 주문입니다.")) + // 실제 값을 사용할 때 프록시 객체가 초기화 된다. + order.orderProducts.forEach { it.amounts } + return order + } +} \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderStatus.kt b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderStatus.kt new file mode 100644 index 0000000..bbc2522 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/kotlin/me/daniel/springjpapersistencecontext/order/OrderStatus.kt @@ -0,0 +1,10 @@ +package me.daniel.springjpapersistencecontext.order + +enum class OrderStatus { + PAYMENT_WAITING, + PATMENT_COMPLETED, + PREPARING, + SHIPPED, + DELIVERING, + DELIVERY_COMPLETED +} \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/main/resources/application.properties b/kotlin-spring-jpa-persistence-context/src/main/resources/application.properties new file mode 100644 index 0000000..8e4e953 --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.jpa.generate-ddl=true +spring.jpa.show-sql=true +spring.jpa.open-in-view=false \ No newline at end of file diff --git a/kotlin-spring-jpa-persistence-context/src/test/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplicationTests.kt b/kotlin-spring-jpa-persistence-context/src/test/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplicationTests.kt new file mode 100644 index 0000000..ae5cd9c --- /dev/null +++ b/kotlin-spring-jpa-persistence-context/src/test/kotlin/me/daniel/springjpapersistencecontext/SpringJpaPersistenceContextApplicationTests.kt @@ -0,0 +1,16 @@ +package me.daniel.springjpapersistencecontext + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringBootTest +class SpringJpaPersistenceContextApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/spring-jpa-collection-and-fetures/.gitignore b/spring-jpa-collection-and-fetures/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/spring-jpa-collection-and-fetures/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/spring-jpa-collection-and-fetures/build.gradle.kts b/spring-jpa-collection-and-fetures/build.gradle.kts new file mode 100644 index 0000000..4897f64 --- /dev/null +++ b/spring-jpa-collection-and-fetures/build.gradle.kts @@ -0,0 +1,40 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "2.1.7.RELEASE" + id("io.spring.dependency-management") version "1.0.8.RELEASE" + kotlin("plugin.jpa") version "1.3.41" + kotlin("jvm") version "1.3.41" + kotlin("plugin.spring") version "1.3.41" +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + +group = "me.daniel" +version = "0.0.1-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_1_8 + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + runtimeOnly("com.h2database:h2") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "1.8" + } +} diff --git a/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.jar b/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.jar differ diff --git a/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.properties b/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ef9a9e0 --- /dev/null +++ b/spring-jpa-collection-and-fetures/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/spring-jpa-collection-and-fetures/gradlew b/spring-jpa-collection-and-fetures/gradlew new file mode 100755 index 0000000..83f2acf --- /dev/null +++ b/spring-jpa-collection-and-fetures/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/spring-jpa-collection-and-fetures/gradlew.bat b/spring-jpa-collection-and-fetures/gradlew.bat new file mode 100644 index 0000000..24467a1 --- /dev/null +++ b/spring-jpa-collection-and-fetures/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-jpa-collection-and-fetures/settings.gradle.kts b/spring-jpa-collection-and-fetures/settings.gradle.kts new file mode 100644 index 0000000..761e365 --- /dev/null +++ b/spring-jpa-collection-and-fetures/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "spring-jpa-collection-and-fetures" diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Board.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Board.kt new file mode 100644 index 0000000..c16193c --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Board.kt @@ -0,0 +1,84 @@ +package me.daniel.springjpacollectionandfetures + +import org.slf4j.LoggerFactory +import javax.persistence.* + +@Entity +@Table(name = "boards") +@Convert(converter = TagConverter::class, attributeName = "tags") +@EntityListeners(value = [BoardListener::class]) +@ExcludeDefaultListeners +@ExcludeSuperclassListeners +class Board protected constructor(){ + @Transient + private val logger = LoggerFactory.getLogger(this::class.java) + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id = 0L + protected set + + var title: String = "" + protected set + + var content: String = "" + protected set + + @OneToMany( + mappedBy = "board", + fetch = FetchType.LAZY, + cascade = [CascadeType.PERSIST] + ) + @OrderBy("id asc") + var comments: MutableList = mutableListOf() + protected set + + @Convert(converter = SecretConverter::class) + var secret: Boolean = false + protected set + + @Column(name = "tags", columnDefinition = "text", nullable = false) + var tags: MutableList = mutableListOf() + + companion object { + fun create(title: String, content: String): Board { + return Board().also { + it.title = title + it.content = content + } + } + } + + fun addComment(comment: String) = apply { + comments.add(Comment.create(comment).also { + it.board = this + }) + } + + fun changeSecret() { + this.secret = true + } + + fun addTag(tag: String) = apply { + if (!tag.isBlank()) this.tags.add(tag) + } + + fun removeTag(tag: String) = apply { + if (!tag.isBlank()) this.tags.remove(tag) + } + + override fun toString(): String { + return "Board(logger=$logger, id=$id, title='$title', content='$content', comments=$comments, secret=$secret, tags=$tags)" + } + +// @PrePersist +// fun prePersist() { +// logger.info("PrePersist(), {}", this) +// } +// +// @PostPersist +// fun postPersist() { +// logger.info("postPersist(), {}", this) +// } + +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardListener.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardListener.kt new file mode 100644 index 0000000..8ef6b19 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardListener.kt @@ -0,0 +1,27 @@ +package me.daniel.springjpacollectionandfetures + +import org.slf4j.LoggerFactory +import javax.persistence.PostLoad +import javax.persistence.PostPersist +import javax.persistence.PrePersist +import javax.persistence.Transient + + +class BoardListener { + private val logger = LoggerFactory.getLogger(this::class.java) + + @PostLoad + fun postLoad(obj: Any) { + logger.info("postLoad()") + } + + @PrePersist + fun prePersist(obj: Any) { + logger.info("PrePersist()") + } + + @PostPersist + fun postPersist(obj: Any) { + logger.info("postPersist()") + } +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardRepository.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardRepository.kt new file mode 100644 index 0000000..8e6d844 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/BoardRepository.kt @@ -0,0 +1,18 @@ +package me.daniel.springjpacollectionandfetures + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.PagingAndSortingRepository +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository + +@Repository +interface BoardRepository: PagingAndSortingRepository { + @Query("select c from Comment c join c.board b where b.id = :id") + fun findComments( + @Param("id") id: Long, + pageable: Pageable + ): Page + +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Comment.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Comment.kt new file mode 100644 index 0000000..097c4d3 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/Comment.kt @@ -0,0 +1,36 @@ +package me.daniel.springjpacollectionandfetures + +import org.hibernate.annotations.CreationTimestamp +import java.time.LocalDateTime +import javax.persistence.* + +@Entity +@Table(name = "comments") +class Comment protected constructor() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id = 0L + + var comment: String = "" + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "boards_id") + lateinit var board: Board + + @Enumerated(value = EnumType.STRING) + var status: CommentStatus = CommentStatus.ACTIVE + + @CreationTimestamp + var createdAt: LocalDateTime = LocalDateTime.now() + + var removedAt: LocalDateTime? = null + + companion object { + fun create(comment: String): Comment { + return Comment().also { + it.comment = comment + } + } + } +} + diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/CommentStatus.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/CommentStatus.kt new file mode 100644 index 0000000..d39789d --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/CommentStatus.kt @@ -0,0 +1,5 @@ +package me.daniel.springjpacollectionandfetures + +enum class CommentStatus { + ACTIVE, IN_ACTIVE +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SecretConverter.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SecretConverter.kt new file mode 100644 index 0000000..1851f18 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SecretConverter.kt @@ -0,0 +1,15 @@ +package me.daniel.springjpacollectionandfetures + +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +@Converter(autoApply = false) +class SecretConverter : AttributeConverter{ + override fun convertToDatabaseColumn(attribute: Boolean): String { + return if (attribute) "Y" else "N" + } + + override fun convertToEntityAttribute(dbData: String): Boolean { + return dbData == "Y" + } +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplication.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplication.kt new file mode 100644 index 0000000..3c13fd0 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplication.kt @@ -0,0 +1,45 @@ +package me.daniel.springjpacollectionandfetures + +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.CommandLineRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.data.domain.PageRequest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.transaction.annotation.Transactional + +@SpringBootApplication +class SpringJpaCollectionAndFeturesApplication: CommandLineRunner { + + private val logger = LoggerFactory.getLogger(SpringJpaCollectionAndFeturesApplication::class.java) + + data class BoardDTO( + var id: Long = 0L, + var title: String = "", + var content: String = "" + ) + + + @Autowired + lateinit var boardRepository: BoardRepository + + @Transactional(readOnly = false) + override fun run(vararg args: String?) { + val board = Board.create(title = "제목", content = "내용") + .addComment("첫 코멘트") + .addComment("두 번째 코멘트") + .addComment("세 번째 코멘트") + .addTag("일반") + .run(boardRepository::save) + logger.info("board = {}", board) + val test = boardRepository.findByIdOrNull(board.id)!! + val comments = boardRepository.findComments(board.id, PageRequest.of(0, 10)) + logger.info("comments = {}", comments) + } + +} + +fun main(args: Array) { + runApplication(*args) +} diff --git a/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/TagConverter.kt b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/TagConverter.kt new file mode 100644 index 0000000..4d11e98 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/kotlin/me/daniel/springjpacollectionandfetures/TagConverter.kt @@ -0,0 +1,19 @@ +package me.daniel.springjpacollectionandfetures + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import javax.persistence.AttributeConverter +import javax.persistence.Converter + +@Converter(autoApply = false) +class TagConverter: AttributeConverter, String> { + override fun convertToDatabaseColumn(attribute: MutableList?): String { + return jacksonObjectMapper().writeValueAsString(attribute) + } + + override fun convertToEntityAttribute(dbData: String?): MutableList { + return dbData?.let { + jacksonObjectMapper().readValue>(dbData, object : TypeReference>(){}) + } ?: mutableListOf() + } +} \ No newline at end of file diff --git a/spring-jpa-collection-and-fetures/src/main/resources/application.properties b/spring-jpa-collection-and-fetures/src/main/resources/application.properties new file mode 100644 index 0000000..c590c2a --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.jpa.show-sql=true +spring.jpa.generate-ddl=true +spring.jpa.hibernate.ddl-auto=create-drop + +spring.h2.console.enabled=true +spring.h2.console.path=/h2 + + diff --git a/spring-jpa-collection-and-fetures/src/test/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplicationTests.kt b/spring-jpa-collection-and-fetures/src/test/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplicationTests.kt new file mode 100644 index 0000000..d65da44 --- /dev/null +++ b/spring-jpa-collection-and-fetures/src/test/kotlin/me/daniel/springjpacollectionandfetures/SpringJpaCollectionAndFeturesApplicationTests.kt @@ -0,0 +1,16 @@ +package me.daniel.springjpacollectionandfetures + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringBootTest +class SpringJpaCollectionAndFeturesApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/spring-mvc-jacksonview/.gradle/5.4.1/fileHashes/fileHashes.lock b/spring-mvc-jacksonview/.gradle/5.4.1/fileHashes/fileHashes.lock index f4acf63..075c3ae 100644 Binary files a/spring-mvc-jacksonview/.gradle/5.4.1/fileHashes/fileHashes.lock and b/spring-mvc-jacksonview/.gradle/5.4.1/fileHashes/fileHashes.lock differ diff --git a/spring-mvc-jacksonview/.gradle/6.3/fileChanges/last-build.bin b/spring-mvc-jacksonview/.gradle/6.3/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/spring-mvc-jacksonview/.gradle/6.3/fileChanges/last-build.bin differ diff --git a/spring-mvc-jacksonview/.gradle/6.3/fileHashes/fileHashes.lock b/spring-mvc-jacksonview/.gradle/6.3/fileHashes/fileHashes.lock new file mode 100644 index 0000000..54f98ac Binary files /dev/null and b/spring-mvc-jacksonview/.gradle/6.3/fileHashes/fileHashes.lock differ diff --git a/spring-mvc-jacksonview/.gradle/6.3/gc.properties b/spring-mvc-jacksonview/.gradle/6.3/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/spring-mvc-jacksonview/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 16d2c3b..e056dce 100644 Binary files a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/spring-mvc-jacksonview/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/cache.properties b/spring-mvc-jacksonview/.gradle/buildOutputCleanup/cache.properties index c407bc3..71806f6 100644 --- a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/cache.properties +++ b/spring-mvc-jacksonview/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Thu May 23 13:08:42 KST 2019 -gradle.version=5.4.1 +#Sun May 24 17:50:32 KST 2020 +gradle.version=6.3 diff --git a/spring-mvc-jacksonview/.gradle/checksums/checksums.lock b/spring-mvc-jacksonview/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..e11f43f Binary files /dev/null and b/spring-mvc-jacksonview/.gradle/checksums/checksums.lock differ diff --git a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/outputFiles.bin b/spring-mvc-jacksonview/.gradle/checksums/md5-checksums.bin similarity index 84% rename from spring-mvc-jacksonview/.gradle/buildOutputCleanup/outputFiles.bin rename to spring-mvc-jacksonview/.gradle/checksums/md5-checksums.bin index 55585eb..ebd1b87 100644 Binary files a/spring-mvc-jacksonview/.gradle/buildOutputCleanup/outputFiles.bin and b/spring-mvc-jacksonview/.gradle/checksums/md5-checksums.bin differ diff --git a/spring-mvc-jacksonview/.gradle/checksums/sha1-checksums.bin b/spring-mvc-jacksonview/.gradle/checksums/sha1-checksums.bin new file mode 100644 index 0000000..9af3375 Binary files /dev/null and b/spring-mvc-jacksonview/.gradle/checksums/sha1-checksums.bin differ