├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── rest-api ├── Makefile ├── bin │ └── version.sh ├── build.gradle ├── docker │ ├── app │ │ ├── Dockerfile │ │ └── check-health.sh │ ├── docker-compose.yml │ └── postgres │ │ ├── Dockerfile │ │ ├── db-dumps │ │ └── dump.sql │ │ └── docker-entrypoint-initdb.d │ │ ├── 001-init.sh │ │ └── 002-import_dump.sh └── src │ └── main │ ├── kotlin │ └── com │ │ └── example │ │ ├── RestApiApplication.kt │ │ ├── api │ │ ├── ApiConfig.kt │ │ ├── bookstore │ │ │ ├── BookStoreApiController.kt │ │ │ ├── apiModel.kt │ │ │ └── domain │ │ │ │ ├── db │ │ │ │ ├── QTables.kt │ │ │ │ ├── authorTable.kt │ │ │ │ └── bookTable.kt │ │ │ │ ├── es │ │ │ │ ├── AuthorSearch.kt │ │ │ │ └── BuildSearchIndex.kt │ │ │ │ └── repo │ │ │ │ ├── authorRepo.kt │ │ │ │ └── bookRepo.kt │ │ ├── common │ │ │ ├── EntityAlreadyExistException.kt │ │ │ └── EntityNotFoundException.kt │ │ └── tweeter │ │ │ ├── TweeterApiController.kt │ │ │ └── domain │ │ │ ├── TweeterRepo.kt │ │ │ └── entities │ │ │ └── jpaEntities.kt │ │ ├── config │ │ ├── Jackson.kt │ │ ├── QueryDsl.kt │ │ └── Swagger.kt │ │ └── util │ │ ├── jpa │ │ └── JpaTypes.kt │ │ └── queryDsl │ │ ├── QueryDslOperation.kt │ │ └── queryDslExtensions.kt │ └── resources │ ├── application-local.yml │ ├── application.yml │ └── logback.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | ### osx 2 | .DS_STORE 3 | 4 | ### idea, gradle 5 | .idea/ 6 | *.iml 7 | 8 | .gradle/ 9 | **/build/** 10 | **/out/** 11 | 12 | 13 | ### custom 14 | **/version.txt 15 | **/src/main/generated/** 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-kotlin-querydsl 2 | playground for spring-boot, kotlin , querydsl (with jpa, hibernate) 3 | 4 | ## build 5 | 6 | ``` 7 | $ make -C rest-api app.build 8 | 9 | ``` 10 | 11 | ## run local db (docker) 12 | 13 | ``` 14 | $ make -C rest-api db.local.up 15 | 16 | ``` 17 | ## the magic behind the query-dsl-kapt-codegenerator 18 | - https://github.com/querydsl/querydsl/tree/master/querydsl-jpa 19 | 20 | 21 | ## This example project is based on ... 22 | - https://github.com/making/spring-boot-db-samples 23 | 24 | ## Note: QueryDSL works fine with hibernate, but does not require it. 25 | 26 | - http://querydsl.com 27 | - https://github.com/querydsl/querydsl/blob/master/querydsl-examples/querydsl-example-sql-spring/src/main/java/com/querydsl/example/config/JdbcConfiguration.java 28 | 29 | 30 | ## Whats wrong with orm, jpa, hibernate and in-memory h2-db these days ? 31 | 32 | There is no silver bullet. 33 | It's born in a world of single-instance big fat application servers. 34 | It hardly fits into a modern world of: 35 | 36 | - functional programming: e.g. immutable threadsafe pojos / data classes 37 | - CQRS and eventsourcing 38 | - horizontal scaling of polyglot microservices 39 | 40 | Make up your mind ... 41 | 42 | - How hibernate ruined Monica's career: https://www.toptal.com/java/how-hibernate-ruined-my-career 43 | - Why do I hate hibernate: https://de.slideshare.net/alimenkou/why-do-i-hate-hibernate-12998784 44 | - ORM is an antipattern: http://seldo.com/weblog/2011/08/11/orm_is_an_antipattern 45 | - Opinionated JPA: https://leanpub.com/opinionatedjpa/read 46 | - Lightweight ORM, do it yourself: https://blog.philipphauer.de/do-it-yourself-orm-alternative-hibernate-drawbacks/ 47 | - Don't use H2 db for testing, use docker: https://blog.philipphauer.de/dont-use-in-memory-databases-tests-h2/ 48 | 49 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | task wrapper(type: Wrapper) { 2 | gradleVersion = '4.2' 3 | } 4 | 5 | 6 | subprojects { 7 | buildscript { 8 | repositories { 9 | mavenCentral() 10 | jcenter() 11 | maven { 12 | url "https://plugins.gradle.org/m2/" 13 | } 14 | } 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | jcenter() 20 | } 21 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastman/spring-kotlin-querydsl/a73827462ffa4b5552e617ea57aab0039f4e9cbd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /rest-api/Makefile: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=spring-kotlin-querydsl-rest-api 2 | SERVICE_VERSION ?= $(shell test -n "$$(cat version.txt)" && cat version.txt || ./bin/version.sh | tee version.txt) 3 | 4 | 5 | DOCKER_FILE_DB=docker/postgres/Dockerfile 6 | DOCKER_FILE_APP=docker/app/Dockerfile 7 | 8 | DOCKER_TAG_LOCAL_DB=local/spring-kotlin-querydsl-db:latest 9 | DOCKER_TAG_LOCAL_APP=local/$(SERVICE_NAME):$(SERVICE_VERSION) 10 | 11 | DOCKER_COMPOSE_FILE="docker/docker-compose.yml" 12 | DOCKER_COMPOSE_SVC_DB_LOCAL="spring-kotlin-querydsl-db-local" 13 | DOCKER_COMPOSE_SVC_DB_CI="spring-kotlin-querydsl-db-ci" 14 | 15 | 16 | GRADLE_CMD=../gradlew 17 | 18 | print-%: ; @echo $*=$($*) 19 | guard-%: 20 | @test ${${*}} || (echo "FAILED! Environment variable $* not set " && exit 1) 21 | @echo "-> use env var $* = ${${*}}"; 22 | 23 | .PHONY : help 24 | help : Makefile 25 | @sed -n 's/^##//p' $< 26 | 27 | version: 28 | @echo $(SERVICE_VERSION) 29 | 30 | ## db.build : build db docker image 31 | db.build: 32 | docker build -t $(DOCKER_TAG_LOCAL_DB) -f $(DOCKER_FILE_DB) . 33 | ## db.local.up : build and start db-local 34 | db.local.up: db.build 35 | docker-compose -f $(DOCKER_COMPOSE_FILE) up $(DOCKER_COMPOSE_SVC_DB_LOCAL) 36 | ## db.ci.up : build and start db-ci 37 | db.ci.up: db.build 38 | docker-compose -f $(DOCKER_COMPOSE_FILE) up $(DOCKER_COMPOSE_SVC_DB_CI) 39 | ## db.down : stop db's 40 | db.down: 41 | docker-compose -f $(DOCKER_COMPOSE_FILE) down 42 | ## db.down : stop db's and delete volumes 43 | db.down.v: 44 | docker-compose -f $(DOCKER_COMPOSE_FILE) down -v 45 | ## app.clean : clean 46 | app.clean: 47 | $(GRADLE_CMD) clean 48 | rm version.txt || true 49 | 50 | ## app.version : show version 51 | app.version: 52 | @echo "$(SERVICE_VERSION)" 53 | 54 | ## app.build : clean and build (jar, docker) 55 | app.build: app.clean app.version guard-SERVICE_VERSION db.build 56 | 57 | @echo "building service $(SERVICE_NAME) version $(SERVICE_VERSION)" 58 | mkdir -p src/main/resources/public/ && cp -rf version.txt src/main/resources/public/version.txt 59 | $(GRADLE_CMD) clean build test 60 | docker build -t $(DOCKER_TAG_LOCAL_APP) -f $(DOCKER_FILE_APP) . 61 | export SERVICE_VERSION=$(SERVICE_VERSION) && docker-compose -f $(DOCKER_COMPOSE_FILE) down -v 62 | @echo "build complete: service $(SERVICE_NAME) version $(SERVICE_VERSION)" 63 | 64 | 65 | app.compose.up: guard-SERVICE_VERSION 66 | docker ps 67 | export SERVICE_VERSION=$(SERVICE_VERSION) && export SERVICE_NAME=$(SERVICE_NAME) && docker-compose -f $(DOCKER_COMPOSE_FILE) up 68 | 69 | app.compose.down: guard-SERVICE_VERSION 70 | docker ps 71 | export SERVICE_VERSION=$(SERVICE_VERSION) && export SERVICE_NAME=$(SERVICE_NAME) && docker-compose -f $(DOCKER_COMPOSE_FILE) down -v 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /rest-api/bin/version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | date -u +"%Y-%m-%dT%H.%M.%SZ" > version.txt -------------------------------------------------------------------------------- /rest-api/build.gradle: -------------------------------------------------------------------------------- 1 | version '0.0.1' 2 | 3 | 4 | 5 | buildscript { 6 | ext { 7 | kotlin_version = '1.1.61' 8 | kotlinLoggingVersion = '1.4.6' 9 | logstashLogbackEncoderVersion='4.8' 10 | springBootVersion ='1.5.7.RELEASE' 11 | jacksonVersion = '2.9.2' 12 | swaggerVersion = '2.7.0' 13 | mockitoVersion = '2.8.47' 14 | postgresDriverVersion = '42.1.4' 15 | queryDslVersion = '4.1.4' 16 | hibernateVersion = '5.2.7.Final' 17 | } 18 | 19 | dependencies { 20 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 21 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}") 22 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}") 23 | classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlin_version}") 24 | 25 | classpath "org.postgresql:postgresql:${postgresDriverVersion}" 26 | } 27 | } 28 | 29 | 30 | 31 | apply plugin: "kotlin" 32 | apply plugin: "kotlin-spring" 33 | apply plugin: "kotlin-noarg" 34 | apply plugin: "org.springframework.boot" 35 | 36 | 37 | apply plugin: 'kotlin-jpa' 38 | apply plugin: 'kotlin-kapt' 39 | apply plugin: 'idea' 40 | 41 | idea { 42 | module { 43 | def kaptMain = file('build/generated/source/kapt/main') 44 | sourceDirs += kaptMain 45 | generatedSourceDirs += kaptMain 46 | } 47 | } 48 | 49 | compileKotlin { 50 | kotlinOptions.jvmTarget = "1.8" 51 | } 52 | compileTestKotlin { 53 | kotlinOptions.jvmTarget = "1.8" 54 | } 55 | 56 | dependencies { 57 | // kotlin 58 | compile("org.jetbrains.kotlin:kotlin-stdlib-jre8") 59 | compile("org.jetbrains.kotlin:kotlin-reflect") 60 | // logging 61 | compile "net.logstash.logback:logstash-logback-encoder:$logstashLogbackEncoderVersion" 62 | compile "io.github.microutils:kotlin-logging:$kotlinLoggingVersion" 63 | // spring 64 | 65 | compile("org.springframework.boot:spring-boot-starter-web") 66 | compile("org.springframework.boot:spring-boot-starter-actuator") 67 | compile "org.springframework.boot:spring-boot-starter-data-jpa" 68 | // jackson json 69 | compile "com.fasterxml.jackson.module:jackson-modules-java8:$jacksonVersion" 70 | compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" 71 | compile "com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonVersion" 72 | compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion" 73 | compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" 74 | // swagger 75 | compile "io.springfox:springfox-swagger2:$swaggerVersion" 76 | compile "io.springfox:springfox-swagger-ui:$swaggerVersion" 77 | 78 | // jsr305 - does it ???? NO :( 79 | compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' 80 | 81 | 82 | // postgres 83 | compile "org.postgresql:postgresql:${postgresDriverVersion}" 84 | 85 | // db: hibernate, flyway, postgres 86 | compile "org.hibernate:hibernate-core:$hibernateVersion" 87 | compile "org.hibernate:hibernate-entitymanager:$hibernateVersion" 88 | compile "org.hibernate:hibernate-java8:$hibernateVersion" 89 | //compile "org.flywaydb:flyway-core:4.1.1" 90 | 91 | //querydsl 92 | compile "com.querydsl:querydsl-jpa:${queryDslVersion}" 93 | kapt "com.querydsl:querydsl-apt:${queryDslVersion}:jpa" 94 | // JSR-330 javax.inject annotations - required by querydsl 95 | compile group: 'javax.inject', name: 'javax.inject', version: '1' 96 | // https://github.com/querydsl/querydsl/blob/master/querydsl-examples/querydsl-example-sql-spring/pom.xml 97 | //compile "com.querydsl:querydsl-sql:${queryDslVersion}" 98 | //compile "com.querydsl:querydsl-sql-spring:${queryDslVersion}" 99 | 100 | // hibernate lucene search 101 | // https://mvnrepository.com/artifact/org.hibernate/hibernate-search-orm 102 | compile group: 'org.hibernate', name: 'hibernate-search-orm', version: '5.8.2.Final' 103 | 104 | 105 | } 106 | 107 | test { 108 | testLogging { 109 | events "failed" 110 | exceptionFormat "full" 111 | } 112 | } 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /rest-api/docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre-alpine 2 | 3 | EXPOSE 8080 8081 4 | 5 | COPY build/libs/rest-api-0.0.1.jar /opt/app/app.jar 6 | COPY docker/app/check-health.sh /usr/local/bin/ 7 | 8 | WORKDIR /opt/app/ 9 | CMD java -jar app.jar 10 | -------------------------------------------------------------------------------- /rest-api/docker/app/check-health.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | curl --fail "http://$host/health" || exit 1 6 | -------------------------------------------------------------------------------- /rest-api/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | spring-kotlin-querydsl-db-local: 4 | image: local/spring-kotlin-querydsl-db:latest 5 | ports: 6 | - "5432:5432" 7 | tmpfs: 8 | - /tmp 9 | - /var/run/postgresql 10 | - /var/lib/postgresql/data 11 | spring-kotlin-querydsl-db-ci: 12 | image: local/spring-kotlin-querydsl-db:latest 13 | ports: 14 | - "5435:5432" 15 | restart: always 16 | tmpfs: 17 | - /tmp 18 | - /var/run/postgresql 19 | - /var/lib/postgresql/data -------------------------------------------------------------------------------- /rest-api/docker/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM library/postgres:9.6.3-alpine 2 | 3 | EXPOSE 5432 4 | 5 | COPY docker/postgres/docker-entrypoint-initdb.d/** /docker-entrypoint-initdb.d/ 6 | COPY docker/postgres/db-dumps/** /db-dumps/ 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /rest-api/docker/postgres/db-dumps/dump.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.6.3 6 | -- Dumped by pg_dump version 9.6.5 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SET check_function_bodies = false; 14 | SET client_min_messages = warning; 15 | SET row_security = off; 16 | 17 | -- 18 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: 19 | -- 20 | 21 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 22 | 23 | 24 | -- 25 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: 26 | -- 27 | 28 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 29 | 30 | 31 | SET search_path = public, pg_catalog; 32 | 33 | SET default_tablespace = ''; 34 | 35 | SET default_with_oids = false; 36 | 37 | 38 | CREATE TABLE tweet ( 39 | id uuid NOT NULL, 40 | version integer NOT NULL, 41 | created_at timestamp without time zone NOT NULL, 42 | updated_at timestamp without time zone NOT NULL, 43 | message text NOT NULL, 44 | comment text NULL 45 | ); 46 | ALTER TABLE tweet OWNER TO app_rw; 47 | ALTER TABLE ONLY tweet 48 | ADD CONSTRAINT tweet_pkey PRIMARY KEY (id); 49 | 50 | 51 | CREATE TABLE author ( 52 | id uuid NOT NULL, 53 | version integer NOT NULL, 54 | created_at timestamp without time zone NOT NULL, 55 | updated_at timestamp without time zone NOT NULL, 56 | name text NOT NULL 57 | ); 58 | ALTER TABLE author OWNER TO app_rw; 59 | ALTER TABLE ONLY author 60 | ADD CONSTRAINT author_pkey PRIMARY KEY (id); 61 | 62 | CREATE TABLE book ( 63 | id uuid NOT NULL, 64 | author_id uuid NOT NULL, 65 | version integer NOT NULL, 66 | created_at timestamp without time zone NOT NULL, 67 | updated_at timestamp without time zone NOT NULL, 68 | title character varying(255) NOT NULL, 69 | status character varying(255) NOT NULL, 70 | price numeric(15,2) NOT NULL 71 | ); 72 | ALTER TABLE book OWNER TO app_rw; 73 | ALTER TABLE ONLY book 74 | ADD CONSTRAINT book_pkey PRIMARY KEY (id); 75 | ALTER TABLE ONLY book 76 | ADD CONSTRAINT book_author_id_fkey FOREIGN KEY (author_id) REFERENCES author(id); 77 | -- 78 | -- PostgreSQL database dump complete 79 | -- 80 | 81 | -------------------------------------------------------------------------------- /rest-api/docker/postgres/docker-entrypoint-initdb.d/001-init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "======= create db and (user) roles =======" 4 | 5 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 6 | CREATE ROLE app_rw WITH LOGIN PASSWORD 'app_rw'; 7 | 8 | CREATE DATABASE app OWNER app_rw; 9 | CREATE DATABASE app_test OWNER app_rw; 10 | 11 | GRANT ALL ON DATABASE app TO app_rw; 12 | GRANT ALL ON DATABASE app_test TO app_rw; 13 | 14 | EOSQL 15 | 16 | 17 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 18 | 19 | CREATE ROLE app_ro WITH LOGIN ENCRYPTED PASSWORD 'app_ro' NOSUPERUSER NOCREATEROLE NOCREATEDB ; 20 | GRANT CONNECT ON DATABASE app TO app_ro; 21 | GRANT CONNECT ON DATABASE app_test TO app_ro; 22 | 23 | \c app; 24 | ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO app_ro; --- this grants privileges on new tables generated in new database "foo" 25 | GRANT USAGE ON SCHEMA public TO app_ro; 26 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro; 27 | GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro; 28 | 29 | \c app_test; 30 | ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO app_ro; --- this grants privileges on new tables generated in new database "foo" 31 | GRANT USAGE ON SCHEMA public TO app_ro; 32 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro; 33 | GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro; 34 | 35 | EOSQL 36 | 37 | -------------------------------------------------------------------------------- /rest-api/docker/postgres/docker-entrypoint-initdb.d/002-import_dump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "======= import sql dump =======" 4 | 5 | psql app -f /db-dumps/dump.sql 6 | -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/RestApiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories 7 | import org.springframework.transaction.annotation.EnableTransactionManagement 8 | 9 | @SpringBootApplication 10 | @EnableTransactionManagement 11 | @EnableJpaRepositories 12 | @EnableJpaAuditing 13 | class RestApiApplication 14 | 15 | fun main(args: Array) { 16 | SpringApplication.run(RestApiApplication::class.java, *args) 17 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/ApiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.api 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | data class ApiConfig( 8 | @Value(value = "\${app.appName}") val appName: String 9 | ) { 10 | val title: String 11 | get() = "API $appName" 12 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/BookStoreApiController.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore 2 | 3 | import com.example.api.bookstore.domain.es.AuthorSearch 4 | import com.example.api.bookstore.domain.repo.AuthorRepoService 5 | import com.example.api.bookstore.domain.repo.BookRepoService 6 | import mu.KLogging 7 | import org.springframework.web.bind.annotation.* 8 | import java.time.Instant 9 | import java.util.* 10 | 11 | @RestController 12 | class BookStoreApiController( 13 | private val authorRepo: AuthorRepoService, 14 | private val bookRepo: BookRepoService, 15 | private val authorSearch: AuthorSearch 16 | ) { 17 | 18 | @GetMapping("/api/bookstore/authors") 19 | fun authorsFindAll() = 20 | authorRepo.findAll() 21 | .results 22 | .toList() 23 | .map { it.toAuthorDto() } 24 | 25 | @GetMapping("/api/bookstore/authors/{id}") 26 | fun authorsGetOne(@PathVariable id: UUID) = 27 | authorRepo.requireOneById(id) 28 | .toAuthorDto() 29 | 30 | @PutMapping("/api/bookstore/authors") 31 | fun authorsCreateOne(@RequestBody req: AuthorCreateRequest) = 32 | req.toAuthorRecord() 33 | .let { authorRepo.insert(it) } 34 | .also { logger.info { "Updated Record: $it" } } 35 | .toAuthorDto() 36 | 37 | @PostMapping("/api/bookstore/authors/{id}") 38 | fun authorsUpdateOne(@PathVariable id: UUID, @RequestBody req: AuthorUpdateRequest): AuthorDto 39 | = authorRepo.requireOneById(id) 40 | .copy(modifiedAt = Instant.now(), name = req.name) 41 | .let { authorRepo.update(it) } 42 | .also { logger.info { "Updated Record: $it" } } 43 | .toAuthorDto() 44 | 45 | 46 | @GetMapping("/api/bookstore/books/{id}") 47 | fun booksGetOne(@PathVariable id: UUID) = 48 | bookRepo.requireOneById(id) 49 | 50 | @PutMapping("/api/bookstore/books") 51 | fun booksCreateOne(@RequestBody req: BookCreateRequest) = 52 | req.toBookRecord() 53 | .let { bookRepo.insert(it) } 54 | .also { logger.info { "Updated Record: $it" } } 55 | 56 | 57 | @PostMapping("/api/bookstore/books/{id}") 58 | fun booksUpdateOne(@PathVariable id: UUID, @RequestBody req: BookUpdateRequest) 59 | = bookRepo.requireOneById(id) 60 | .copy(modifiedAt = Instant.now(), title = req.title, status = req.status, price = req.price) 61 | .let { bookRepo.update(it) } 62 | .also { logger.info { "Updated Record: $it" } } 63 | 64 | @GetMapping("/api/bookstore/books") 65 | fun booksFindAll() = bookRepo.findAllBooksJoinAuthor().map { 66 | it.toBookDto() 67 | }.also { 68 | 69 | logger.info { it } 70 | } 71 | 72 | @GetMapping("/api/bookstore/books/summary") 73 | fun booksFindAllAsSummary() = bookRepo.findAllBooksJoinAuthorAsSummary() 74 | 75 | 76 | 77 | 78 | @GetMapping("/api/bookstore/authors/search") 79 | fun authorsSearch(@RequestParam q:String) = 80 | authorSearch.search(q).toList().map { it.toAuthorDto() } 81 | 82 | companion object : KLogging() 83 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/apiModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore 2 | 3 | import com.example.api.bookstore.domain.db.Author 4 | import com.example.api.bookstore.domain.db.Book 5 | import com.example.api.bookstore.domain.db.BookStatus 6 | import com.example.api.bookstore.domain.repo.BookRecordJoinAuthorRecord 7 | import java.math.BigDecimal 8 | import java.time.Instant 9 | import java.util.* 10 | 11 | data class AuthorCreateRequest(val name: String) 12 | data class AuthorUpdateRequest(val name: String) 13 | data class BookCreateRequest(val authorId: UUID, val title: String, val status: BookStatus, val price: BigDecimal) 14 | data class BookUpdateRequest(val title: String, val status: BookStatus, val price: BigDecimal) 15 | 16 | data class AuthorDto(val id: UUID, val createdAt: Instant, val modifiedAt: Instant, val name: String) 17 | data class BookDto( 18 | val id: UUID, 19 | val createdAt: Instant, 20 | val modifiedAt: Instant, 21 | val title: String, 22 | val status: BookStatus, 23 | val price: BigDecimal, 24 | val author: AuthorDto 25 | ) 26 | 27 | fun Author.toAuthorDto() = AuthorDto(id = id, createdAt = createdAt, modifiedAt = modifiedAt, name = name) 28 | 29 | fun AuthorCreateRequest.toAuthorRecord(): Author { 30 | val now = Instant.now() 31 | return Author( 32 | id = UUID.randomUUID(), 33 | createdAt = now, 34 | modifiedAt = now, 35 | version = 0, 36 | name = name 37 | ) 38 | } 39 | 40 | fun BookCreateRequest.toBookRecord(): Book { 41 | val now = Instant.now() 42 | return Book( 43 | id = UUID.randomUUID(), 44 | createdAt = now, 45 | modifiedAt = now, 46 | version = 0, 47 | authorId = authorId, 48 | title = title, 49 | status = status, 50 | price = price 51 | ) 52 | } 53 | 54 | fun BookRecordJoinAuthorRecord.toBookDto() = 55 | BookDto( 56 | id = bookRecord.id, 57 | createdAt = bookRecord.createdAt, 58 | modifiedAt = bookRecord.modifiedAt, 59 | title = bookRecord.title, 60 | status = bookRecord.status, 61 | price = bookRecord.price, 62 | author = authorRecord.toAuthorDto() 63 | ) 64 | 65 | -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/db/QTables.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.db 2 | 3 | object QTables { 4 | val BOOK = QBook.book 5 | val AUTHOR = QAuthor.author 6 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/db/authorTable.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.db 2 | 3 | import com.example.util.jpa.JpaTypes 4 | import mu.KLogging 5 | import org.hibernate.annotations.Type 6 | import org.hibernate.search.annotations.Analyze 7 | import org.hibernate.search.annotations.Field 8 | import org.hibernate.search.annotations.Index 9 | import org.hibernate.search.annotations.Indexed 10 | import org.hibernate.search.annotations.Store 11 | import java.time.Instant 12 | import java.util.* 13 | import javax.persistence.* 14 | 15 | @Entity 16 | @Table(name = "author") 17 | @Indexed 18 | data class Author( 19 | @Id 20 | @Type(type = JpaTypes.UUID) 21 | val id: UUID, 22 | @Version 23 | val version: Int = -1, 24 | @Column(name = "created_at", nullable = false) 25 | var createdAt: Instant, 26 | @Column(name = "updated_at", nullable = false) 27 | var modifiedAt: Instant, 28 | 29 | @Column(name = "name", nullable = false) 30 | @Field(index=Index.YES, analyze=Analyze.YES, store= Store.NO) 31 | val name: String 32 | ) { 33 | companion object : KLogging() 34 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/db/bookTable.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.db 2 | 3 | import com.example.util.jpa.JpaTypes 4 | import mu.KLogging 5 | import org.hibernate.annotations.Type 6 | import java.math.BigDecimal 7 | import java.time.Instant 8 | import java.util.* 9 | import javax.persistence.* 10 | 11 | @Entity 12 | @Table(name = "book") 13 | data class Book( 14 | @Id 15 | @Type(type = JpaTypes.UUID) 16 | val id: UUID, 17 | @Version 18 | val version: Int = -1, 19 | @Column(name = "created_at", nullable = false) 20 | var createdAt: Instant, 21 | @Column(name = "updated_at", nullable = false) 22 | var modifiedAt: Instant, 23 | 24 | @Column(name = "author_id", nullable = false) 25 | val authorId: UUID, 26 | @Column(name = "title", nullable = false) 27 | val title: String, 28 | @Column(name = "status", nullable = false) 29 | val status: BookStatus, 30 | @Column(name = "price", nullable = false) 31 | val price: BigDecimal 32 | ) { 33 | companion object : KLogging() 34 | } 35 | 36 | enum class BookStatus { NEW, PUBLISHED; } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/es/AuthorSearch.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.es 2 | 3 | 4 | import com.example.api.bookstore.domain.db.Author 5 | import com.example.api.bookstore.domain.db.QTables 6 | import org.hibernate.search.jpa.FullTextQuery 7 | import javax.persistence.EntityManager 8 | import javax.persistence.PersistenceContext 9 | import javax.transaction.Transactional 10 | 11 | 12 | import org.springframework.stereotype.Repository 13 | 14 | /** 15 | * Search methods for the entity User using Hibernate search. 16 | * The Transactional annotation ensure that transactions will be opened and 17 | * closed at the beginning and at the end of each method. 18 | * 19 | * @author netgloo 20 | */ 21 | @Repository 22 | @Transactional 23 | class AuthorSearch( 24 | @PersistenceContext 25 | private val entityManager: EntityManager 26 | ) { 27 | 28 | 29 | /** 30 | * A basic search for the entity User. The search is done by exact match per 31 | * keywords on fields name, city and email. 32 | * 33 | * @param text The query text. 34 | */ 35 | //@Transactional 36 | fun search(text: String): List { 37 | 38 | // get the full text entity manager 39 | val fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(entityManager) 40 | 41 | // create the query using Hibernate Search query DSL 42 | val queryBuilder = fullTextEntityManager.searchFactory 43 | .buildQueryBuilder().forEntity(Author::class.java).get() 44 | 45 | // a very basic query by keywords 46 | val query = queryBuilder 47 | .keyword() 48 | //Query fullTextQuery = tweetQb.keyword().onField(Tweet_.message.getName()).matching(“Message updated”).createQuery(); 49 | .onFields(Author::name.name) 50 | .matching(text) 51 | .createQuery() 52 | 53 | // wrap Lucene query in an Hibernate Query object 54 | val jpaQuery:FullTextQuery = fullTextEntityManager.createFullTextQuery(query, Author::class.java) 55 | 56 | // execute search and return results (sorted by relevance as default) 57 | //@SuppressWarnings("unchecked") 58 | return jpaQuery.resultList.toList().map { it as Author } 59 | } 60 | 61 | 62 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/es/BuildSearchIndex.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.es 2 | 3 | import mu.KLogging 4 | import org.hibernate.search.jpa.Search.getFullTextEntityManager 5 | import org.hibernate.search.jpa.FullTextEntityManager 6 | import org.hibernate.search.jpa.Search 7 | import org.springframework.boot.context.event.ApplicationReadyEvent 8 | import org.springframework.context.ApplicationListener 9 | import org.springframework.stereotype.Component 10 | import javax.persistence.EntityManager 11 | 12 | import javax.persistence.PersistenceContext 13 | import javax.transaction.Transactional 14 | 15 | @Component 16 | class BuildSearchIndex( 17 | @PersistenceContext 18 | private val entityManager:EntityManager 19 | ) : ApplicationListener { 20 | @Transactional 21 | override fun onApplicationEvent(event: ApplicationReadyEvent?) { 22 | try { 23 | val fullTextEntityManager = Search.getFullTextEntityManager(entityManager) 24 | logger.info { "ES: Indexer Start" } 25 | fullTextEntityManager.createIndexer().startAndWait() 26 | logger.info { "ES: Indexer Done" } 27 | } catch (e: Exception) { 28 | logger.error { 29 | "ES: Indexer Failed! reason=$e" 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Create an initial Lucene index for the data already present in the 36 | * database. 37 | * This method is called when Spring's startup. 38 | */ 39 | /* 40 | fun onApplicationEvent(event: ApplicationReadyEvent) { 41 | try { 42 | val fullTextEntityManager = Search.getFullTextEntityManager(entityManager) 43 | fullTextEntityManager.createIndexer().startAndWait() 44 | } catch (e: InterruptedException) { 45 | println( 46 | "An error occurred trying to build the serach index: " + e.toString()) 47 | } 48 | 49 | return 50 | } 51 | */ 52 | 53 | companion object: KLogging() 54 | 55 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/repo/authorRepo.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.repo 2 | 3 | import com.example.api.bookstore.domain.db.Author 4 | import com.example.api.bookstore.domain.db.QTables 5 | import com.example.api.common.EntityAlreadyExistException 6 | import com.example.api.common.EntityNotFoundException 7 | import org.springframework.data.jpa.repository.JpaRepository 8 | import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport 9 | import org.springframework.stereotype.Component 10 | import org.springframework.stereotype.Repository 11 | import java.util.* 12 | import javax.transaction.Transactional 13 | 14 | @Repository 15 | interface AuthorJpaRepository : JpaRepository {} 16 | 17 | @Component 18 | @Transactional // should be moved to RequestHandler 19 | class AuthorRepoService( 20 | private val jpaRepo: AuthorJpaRepository 21 | ) : QueryDslRepositorySupport(Author::class.java) { 22 | fun getOneById(id: UUID): Author? = jpaRepo.findOne(id) 23 | 24 | fun requireOneById(id: UUID): Author 25 | = getOneById(id) ?: throw EntityNotFoundException("$CRUD_RECORD_NAME NOT FOUND ! (id=$id)") 26 | 27 | fun existsById(id: UUID) = jpaRepo.exists(id) 28 | 29 | fun requireExistsById(id: UUID) { 30 | if (!existsById(id)) throw EntityNotFoundException("$CRUD_RECORD_NAME NOT FOUND ! (id=$id)") 31 | } 32 | 33 | fun requireDoesNotExistById(id: UUID) { 34 | if (existsById(id)) throw EntityAlreadyExistException("$CRUD_RECORD_NAME ALREADY EXIST ! (id=$id)") 35 | } 36 | 37 | 38 | fun insert(author: Author): Author = 39 | author.also { requireDoesNotExistById(it.id) } 40 | .let { jpaRepo.save(it) } 41 | 42 | fun update(author: Author): Author = 43 | author.also { requireExistsById(it.id) } 44 | .let { jpaRepo.save(it) } 45 | 46 | fun findAll() = 47 | from(Q_CRUD_TABLE) 48 | .fetchAll().fetchResults() 49 | 50 | companion object { 51 | val CRUD_RECORD_NAME = "AuthorRecord" 52 | val Q_CRUD_TABLE = QTables.AUTHOR 53 | } 54 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/bookstore/domain/repo/bookRepo.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.bookstore.domain.repo 2 | 3 | import com.example.api.bookstore.domain.db.Author 4 | import com.example.api.bookstore.domain.db.Book 5 | import com.example.api.bookstore.domain.db.QTables 6 | import com.example.api.common.EntityAlreadyExistException 7 | import com.example.api.common.EntityNotFoundException 8 | import com.querydsl.core.types.Projections 9 | import com.querydsl.jpa.impl.JPAQueryFactory 10 | import org.springframework.data.jpa.repository.JpaRepository 11 | import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport 12 | import org.springframework.stereotype.Component 13 | import org.springframework.stereotype.Repository 14 | import java.util.* 15 | import javax.transaction.Transactional 16 | 17 | @Repository 18 | interface BookJpaRepository : JpaRepository {} 19 | 20 | @Component 21 | @Transactional // should be moved to RequestHandler 22 | class BookRepoService( 23 | private val jpaRepo: BookJpaRepository, 24 | private val authorRepoService: AuthorRepoService, 25 | private val jpaQueryFactory: JPAQueryFactory 26 | ) : QueryDslRepositorySupport(Book::class.java) { 27 | fun getOneById(id: UUID): Book? = jpaRepo.findOne(id) 28 | 29 | fun requireOneById(id: UUID): Book 30 | = getOneById(id) ?: throw EntityNotFoundException("$CRUD_RECORD_NAME NOT FOUND ! (id=$id)") 31 | 32 | fun existsById(id: UUID) = jpaRepo.exists(id) 33 | 34 | fun requireExistsById(id: UUID) { 35 | if (!existsById(id)) throw EntityNotFoundException("$CRUD_RECORD_NAME NOT FOUND ! (id=$id)") 36 | } 37 | 38 | fun requireDoesNotExistById(id: UUID) { 39 | if (existsById(id)) throw EntityAlreadyExistException("$CRUD_RECORD_NAME ALREADY EXIST ! (id=$id)") 40 | } 41 | 42 | 43 | fun insert(book: Book): Book = 44 | book.also { requireDoesNotExistById(it.id) } 45 | .also { authorRepoService.requireExistsById(it.authorId) } 46 | .let { jpaRepo.save(it) } 47 | 48 | fun update(book: Book): Book = 49 | book.also { requireExistsById(it.id) } 50 | .also { authorRepoService.requireExistsById(it.authorId) } 51 | .let { jpaRepo.save(it) } 52 | 53 | fun findAll() = 54 | from(Q_CRUD_TABLE) 55 | .fetchAll().fetchResults() 56 | 57 | fun findAllBooksJoinAuthor(): List { 58 | val authorTable = QTables.AUTHOR 59 | val bookTable = QTables.BOOK 60 | val proj = Projections.constructor( 61 | BookRecordJoinAuthorRecord::class.java, 62 | bookTable, 63 | authorTable 64 | ) 65 | 66 | return jpaQueryFactory.select(proj) 67 | .from(authorTable) 68 | .innerJoin(bookTable) 69 | .on(bookTable.authorId.eq(authorTable.id)) 70 | .fetchResults() 71 | .results.toList() 72 | } 73 | 74 | fun findAllBooksJoinAuthorAsSummary(): List { 75 | val authorTable = QTables.AUTHOR 76 | val bookTable = QTables.BOOK 77 | val proj = Projections.constructor( 78 | BookWithAuthorSummary::class.java, 79 | authorTable.id, authorTable.name, 80 | bookTable.id, bookTable.title 81 | ) 82 | 83 | return jpaQueryFactory.select(proj) 84 | .from(authorTable) 85 | .innerJoin(bookTable) 86 | .on(bookTable.authorId.eq(authorTable.id)) 87 | .fetchResults() 88 | .results.toList() 89 | } 90 | 91 | companion object { 92 | val CRUD_RECORD_NAME = "BookRecord" 93 | val Q_CRUD_TABLE = QTables.AUTHOR 94 | } 95 | } 96 | 97 | data class BookWithAuthorSummary(val authorId: UUID, val authorName: String, val bookId: UUID, val bookTitle: String) 98 | data class BookRecordJoinAuthorRecord(val bookRecord: Book, val authorRecord: Author) -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/common/EntityAlreadyExistException.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.common 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.ResponseStatus 5 | 6 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) 7 | class EntityAlreadyExistException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/common/EntityNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.common 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.ResponseStatus 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 7 | class EntityNotFoundException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/tweeter/TweeterApiController.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.tweeter 2 | 3 | import com.example.api.tweeter.domain.TweetRepoService 4 | import com.example.api.tweeter.domain.entities.Tweet 5 | import org.springframework.web.bind.annotation.* 6 | import java.time.Instant 7 | import java.util.* 8 | 9 | @RestController 10 | class TweeterApiController( 11 | private val repo: TweetRepoService 12 | ) { 13 | 14 | @GetMapping("/api/tweeter") 15 | fun findAll() = repo.findAll().results.map { it.toDto() } 16 | 17 | @GetMapping("/api/tweeter/{id}") 18 | fun getOne(@PathVariable id: UUID) = repo.requireOneById(id).toDto() 19 | 20 | 21 | @PutMapping("/api/tweeter") 22 | fun createOne(@RequestBody req: CreateTweetRequest): TweetDto { 23 | val tweet = req.toTweet() 24 | 25 | return repo 26 | .insert(tweet) 27 | .toDto() 28 | } 29 | 30 | @PostMapping("/api/tweeter/{id}") 31 | fun updateOne(@PathVariable id: UUID, @RequestBody req: CreateTweetRequest): TweetDto { 32 | val tweet = repo 33 | .requireOneById(id) 34 | .copy( 35 | modifiedAt = Instant.now(), 36 | message = req.message, 37 | comment = req.comment 38 | ) 39 | 40 | return repo.update(tweet).toDto() 41 | } 42 | } 43 | 44 | data class CreateTweetRequest(val message: String, val comment: String?) 45 | 46 | data class TweetDto( 47 | val id: UUID, 48 | val version: Int, 49 | val createdAt: Instant, 50 | val modifiedAt: Instant, 51 | val message: String, 52 | val comment: String? 53 | ) 54 | 55 | private fun Tweet.toDto() = TweetDto( 56 | id = id, 57 | version = version, 58 | createdAt = createdAt, 59 | modifiedAt = modifiedAt, 60 | message = message, 61 | comment = comment 62 | ) 63 | 64 | private fun CreateTweetRequest.toTweet(): Tweet { 65 | val now = Instant.now() 66 | 67 | return Tweet(id = UUID.randomUUID(), version = 0, createdAt = now, modifiedAt = now, message = message, comment = comment) 68 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/tweeter/domain/TweeterRepo.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.tweeter.domain 2 | 3 | import com.example.api.common.EntityNotFoundException 4 | import com.example.api.tweeter.domain.entities.QTweet 5 | import com.example.api.tweeter.domain.entities.Tweet 6 | import com.querydsl.jpa.impl.JPAQueryFactory 7 | import org.springframework.data.jpa.repository.JpaRepository 8 | import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport 9 | import org.springframework.stereotype.Component 10 | import org.springframework.stereotype.Repository 11 | import java.util.* 12 | import javax.transaction.Transactional 13 | 14 | 15 | @Repository 16 | interface TweetJpaRepository : JpaRepository {} 17 | 18 | @Component 19 | class TweetRepoService( 20 | private val jpaQueryFactory: JPAQueryFactory, 21 | private val jpaRepo: TweetJpaRepository 22 | ) : QueryDslRepositorySupport(Tweet::class.java) { 23 | 24 | fun getOneById(id: UUID): Tweet? = jpaRepo.findOne(id) 25 | 26 | fun requireOneById(id: UUID): Tweet 27 | = getOneById(id) ?: throw EntityNotFoundException("TweetRecord NOT FOUND ! (id=$id)") 28 | 29 | @Transactional 30 | fun findAll() = 31 | from(QTweet.tweet) 32 | .fetchAll().fetchResults() 33 | 34 | fun insert(tweet: Tweet): Tweet { 35 | return jpaRepo.save(tweet) 36 | } 37 | 38 | fun update(tweet: Tweet): Tweet { 39 | return jpaRepo.save(tweet) 40 | } 41 | 42 | 43 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/api/tweeter/domain/entities/jpaEntities.kt: -------------------------------------------------------------------------------- 1 | package com.example.api.tweeter.domain.entities 2 | 3 | import com.example.util.jpa.JpaTypes 4 | import mu.KLogging 5 | import org.hibernate.annotations.Type 6 | import java.time.Instant 7 | import java.util.* 8 | import javax.persistence.Column 9 | import javax.persistence.Entity 10 | import javax.persistence.Id 11 | import javax.persistence.Version 12 | 13 | @Entity 14 | //@EntityListeners(JpaAuthorListener::class) 15 | data class Tweet( 16 | @Id 17 | @Type(type = JpaTypes.UUID) 18 | val id: UUID, 19 | @Version 20 | val version: Int = -1, 21 | @Column(name = "created_at", nullable = false) 22 | var createdAt: Instant, 23 | @Column(name = "updated_at", nullable = false) 24 | var modifiedAt: Instant, 25 | 26 | @Column(name = "message", nullable = false) 27 | val message: String, 28 | @Column(name = "comment", nullable = true) 29 | val comment: String? 30 | ) { 31 | 32 | 33 | companion object : KLogging() 34 | } 35 | -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/config/Jackson.kt: -------------------------------------------------------------------------------- 1 | package com.example.config 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.databind.SerializationFeature 6 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | 10 | @Configuration 11 | class Jackson { 12 | 13 | @Bean 14 | fun objectMapper(): ObjectMapper = defaultMapper() 15 | 16 | companion object { 17 | fun defaultMapper(): ObjectMapper 18 | = jacksonObjectMapper() 19 | .findAndRegisterModules() 20 | 21 | // toJson() 22 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 23 | .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) 24 | 25 | // fromJson() 26 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 27 | .disable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) 28 | .disable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) 29 | .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) 30 | .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) 31 | .enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) 32 | } 33 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/config/QueryDsl.kt: -------------------------------------------------------------------------------- 1 | package com.example.config 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import javax.inject.Provider 7 | import javax.persistence.EntityManager 8 | import javax.persistence.PersistenceContext 9 | 10 | 11 | @Configuration 12 | class QueryDsl(@PersistenceContext private val em: EntityManager) { 13 | 14 | val jpaQueryFactory: JPAQueryFactory 15 | @Bean 16 | get() { 17 | val provider = Provider { em } 18 | return JPAQueryFactory(provider) 19 | } 20 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/config/Swagger.kt: -------------------------------------------------------------------------------- 1 | package com.example.config 2 | 3 | import com.example.api.ApiConfig 4 | import com.google.common.base.Predicates 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import springfox.documentation.builders.RequestHandlerSelectors 8 | import springfox.documentation.spring.web.plugins.Docket 9 | import springfox.documentation.swagger2.annotations.EnableSwagger2 10 | 11 | @Configuration 12 | @EnableSwagger2 13 | class Swagger(private val apiConfig: ApiConfig) { 14 | 15 | @Bean 16 | fun mainApi(): Docket 17 | = apiConfig.toDocket() 18 | .groupName("Main") 19 | .select() 20 | .apis(RequestHandlerSelectors.basePackage(apiConfig.getBasePackageName())) 21 | .build() 22 | 23 | @Bean 24 | fun monitoringApi(): Docket 25 | = apiConfig.toDocket() 26 | .groupName("Monitoring") 27 | .useDefaultResponseMessages(true) 28 | .select() 29 | .apis(Predicates.not(RequestHandlerSelectors.basePackage(apiConfig.getBasePackageName()))) 30 | .build() 31 | } 32 | 33 | private fun ApiConfig.getBasePackageName() = this::class.java.`package`.name 34 | private fun ApiConfig.toApiInfo() = springfox.documentation.builders.ApiInfoBuilder().title(this.title).build() 35 | private fun ApiConfig.toDocket() = springfox.documentation.spring.web.plugins.Docket(springfox.documentation.spi.DocumentationType.SWAGGER_2).apiInfo(this.toApiInfo()) -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/util/jpa/JpaTypes.kt: -------------------------------------------------------------------------------- 1 | package com.example.util.jpa 2 | 3 | object JpaTypes { 4 | const val UUID: String = "pg-uuid" 5 | } -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/util/queryDsl/QueryDslOperation.kt: -------------------------------------------------------------------------------- 1 | package com.example.util.queryDsl 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue 4 | 5 | object QueryDslOperation { 6 | const val ASC = "asc" 7 | const val DESC = "desc" 8 | const val LIKE = "like" 9 | const val EQ = "eq" 10 | const val GOE = "goe" 11 | const val LOE = "loe" 12 | } 13 | 14 | enum class QueryDslPredicateCombiner(val text: String) { 15 | ALL_OF("allOf"), 16 | ANY_OF("anyOf") 17 | ; 18 | 19 | @JsonValue 20 | fun jsonValue(): String = text 21 | } 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /rest-api/src/main/kotlin/com/example/util/queryDsl/queryDslExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.util.queryDsl 2 | 3 | import com.querydsl.core.types.ExpressionUtils 4 | import com.querydsl.core.types.OrderSpecifier 5 | import com.querydsl.core.types.Predicate 6 | import com.querydsl.core.types.dsl.BooleanExpression 7 | import com.querydsl.core.types.dsl.DateTimePath 8 | import com.querydsl.core.types.dsl.NumberPath 9 | import com.querydsl.core.types.dsl.StringExpression 10 | import com.querydsl.jpa.impl.JPAQuery 11 | import java.time.Instant 12 | 13 | fun queryDslAllOf(predicates: List): Predicate? { 14 | val t = predicates.toTypedArray() 15 | return ExpressionUtils.allOf(*t) 16 | } 17 | 18 | fun queryDslAnyOf(predicates: List): Predicate? { 19 | val t = predicates.toTypedArray() 20 | return ExpressionUtils.anyOf(*t) 21 | } 22 | 23 | fun StringExpression.likeGoogle(value: String): BooleanExpression { 24 | val query = "%${value.trim()}%" 25 | return this.likeIgnoreCase(query) 26 | } 27 | 28 | fun BooleanExpression.andAllOf(predicates: List): BooleanExpression { 29 | val t = predicates.toTypedArray() 30 | return this.and(ExpressionUtils.allOf(*t)) 31 | } 32 | 33 | fun BooleanExpression.orAllOf(predicates: List): BooleanExpression { 34 | val t = predicates.toTypedArray() 35 | return this.or(ExpressionUtils.allOf(*t)) 36 | } 37 | 38 | fun BooleanExpression.andAnyOf(predicates: List): BooleanExpression { 39 | val t = predicates.toTypedArray() 40 | return this.and(ExpressionUtils.anyOf(*t)) 41 | } 42 | 43 | fun BooleanExpression.orAnyOf(predicates: List): BooleanExpression { 44 | val t = predicates.toTypedArray() 45 | return this.or(ExpressionUtils.anyOf(*t)) 46 | } 47 | 48 | fun JPAQuery.orderBy(orderSpecifier: List>): JPAQuery { 49 | if (orderSpecifier.isEmpty()) { 50 | return this 51 | } 52 | val t = orderSpecifier.toTypedArray() 53 | return this.orderBy(*t) 54 | } 55 | 56 | fun DateTimePath.eq(value: String): BooleanExpression = this.eq(Instant.parse(value)) 57 | fun DateTimePath.gt(value: String): BooleanExpression = this.gt(Instant.parse(value)) 58 | fun DateTimePath.lt(value: String): BooleanExpression = this.lt(Instant.parse(value)) 59 | fun DateTimePath.goe(value: String): BooleanExpression = this.goe(Instant.parse(value)) 60 | fun DateTimePath.loe(value: String): BooleanExpression = this.loe(Instant.parse(value)) 61 | 62 | fun NumberPath.eq(value: String): BooleanExpression = this.eq(value.toInt()) 63 | fun NumberPath.gt(value: String): BooleanExpression = this.gt(value.toInt()) 64 | fun NumberPath.lt(value: String): BooleanExpression = this.lt(value.toInt()) 65 | fun NumberPath.goe(value: String): BooleanExpression = this.goe(value.toInt()) 66 | fun NumberPath.loe(value: String): BooleanExpression = this.loe(value.toInt()) -------------------------------------------------------------------------------- /rest-api/src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: local 3 | datasource: 4 | #url: jdbc:postgresql://localhost:5432/app 5 | url: jdbc:postgresql://localhost:5432/app 6 | username: app_rw 7 | password: app_rw 8 | driver-class-name: org.postgresql.Driver 9 | 10 | 11 | -------------------------------------------------------------------------------- /rest-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring: 3 | profiles: 4 | active: local 5 | datasource: 6 | driver-class-name: org.postgresql.Driver 7 | 8 | jpa: 9 | show-sql: true 10 | properties: 11 | hibernate: 12 | show_sql: true 13 | format_sql: true 14 | use_sql_comments: true 15 | generate_statistics: false 16 | naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy 17 | search: 18 | default.directory_provider: filesystem 19 | # make sure app as permissions !!! 20 | default.indexBase: /tmp/spring-kotlin-querydsl/lucene/indexes/ 21 | 22 | logging: 23 | level: 24 | org: 25 | hibernate: 26 | type: info 27 | app: 28 | appName: "spring-kotlin-querydsl" 29 | --- 30 | -------------------------------------------------------------------------------- /rest-api/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'rest-api' 2 | 3 | --------------------------------------------------------------------------------