From 8c999a9e7c0bfa950206d5e96f5244bd805425e4 Mon Sep 17 00:00:00 2001 From: Sambo Chea Date: Mon, 11 Apr 2022 12:35:47 +0700 Subject: [PATCH] Add basic module and redis data with jpa and example --- README.md | 154 ++++++++++++++++++ api/build.gradle.kts | 9 + .../cubetiqs/web/modules/redis/RedisConfig.kt | 20 +++ .../web/modules/redis/RedisController.kt | 31 ++++ .../web/modules/redis/RedisKVModel.kt | 14 ++ .../cubetiqs/web/modules/redis/RedisModule.kt | 6 + .../web/modules/user/UserController.kt | 60 +++++++ .../cubetiqs/web/modules/user/UserEntity.kt | 30 ++++ .../cubetiqs/web/modules/user/UserModule.kt | 6 + .../web/modules/user/UserRepository.kt | 9 + api/src/main/resources/application.yml | 16 +- api/src/main/resources/banner.txt | 8 + build.gradle.kts | 2 +- k8s/03-secret.yaml | 5 + 14 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisConfig.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisController.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisKVModel.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisModule.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/user/UserController.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/user/UserEntity.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/user/UserModule.kt create mode 100644 api/src/main/kotlin/com/cubetiqs/web/modules/user/UserRepository.kt create mode 100644 api/src/main/resources/banner.txt diff --git a/README.md b/README.md index 6678b8e..ca622cd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,170 @@ # CUBETIQ Web Modules (Template) + - Setup and Default Web Configuration - Swagger UI and API's Documentation (SpringFox) - General Purpose for External and Internal use-cases - Dockerfile and Docker profile build support # Modules + - API (Default Module) +### Spring Data + +- Add `spring-data-jpa` dependency in `build.gradle.kts` + +```kotlin +implementation("org.springframework.boot:spring-boot-starter-data-jpa") +``` + +- Add `spring-data-redis` dependency in `build.gradle.kts` (Redis + Driver) + +```kotlin +implementation("org.springframework.boot:spring-boot-starter-data-redis") +``` + +### Spring Boot Properties + +- Recommend + +```yaml +spring: + application: + name: ${APP_NAME:spring-web-api} +app: + data-dir: ${APP_DATA_DIR:${user.home}/${spring.application.name}} +``` + +- Upload File Properties + +```yaml +server: + tomcat: + max-http-form-post-size: ${SERVER_MAX_HTTP_FORM_POST_SIZE:50MB} + +spring: + servlet: + multipart: + max-file-size: 256MB + max-request-size: 256MB + enabled: true +``` + +- Logging + +```yaml +logging: + file: + path: ${LOGGING_FILE_PATH:${app.data-dir}/logs/} + name: ${logging.file.path}/app.log +``` + +### Spring Data Redis + +- Redis Properties + +```yaml +spring: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:your_password} +``` + +### Spring Data JPA Properties + +- Datasource Enhancement (Default: PostgresSQL) + +```yaml +spring: + datasource: + driverClassName: ${DATA_SOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver} + url: ${DATA_SOURCE_URL:jdbc:postgresql://localhost:5432/demo} + username: ${POSTGRES_USERNAME:postgres} + password: ${POSTGRES_PASSWORD:postgres} + hikari: + max-lifetime: ${DATA_SOURCE_MAX_LIFETIME:1800000} + connection-timeout: ${DATA_SOURCE_CONNECTION_TIMEOUT:30000} + idle-timeout: ${DATA_SOURCE_IDLE_TIMEOUT:600000} + maximum-pool-size: ${DATA_SOURCE_MAXIMUM_POOL_SIZE:10} + allow-pool-suspension: ${DATA_SOURCE_ALLOW_POOL_SUSPENSION:true} + tomcat: + max_active: ${DATA_SOURCE_TOMCAT_MAX_ACTIVE:100} + max_idle: ${DATA_SOURCE_TOMCAT_MAX_IDLE:10} + min-idle: ${DATA_SOURCE_TOMCAT_MIN_IDLE:10} + initial_size: ${DATA_SOURCE_TOMCAT_INITIAL_SIZE:10} + remove_abandoned: ${DATA_SOURCE_TOMCAT_REMOVE_ABANDONED:true} + jpa: + database-platform: ${JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect} + show-sql: ${JPA_SHOW_SQL:false} + hibernate: + ddl-auto: ${JPA_HIBERNATE_DDL_AUTO:update} + properties: + hibernate: + dialect: ${JPA_HIBERNATE_DIALECT:org.hibernate.dialect.PostgreSQLDialect} + open-in-view: ${JPA_OPEN_IN_VIEW:false} +``` + +- PostgreSQL + +```yaml +spring: + datasource: + driverClassName: ${DATA_SOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver} + url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:demo} + username: ${POSTGRES_USERNAME:postgres} + password: ${POSTGRES_PASSWORD:postgres} +``` + +- MySQL + +```yaml +spring: + datasource: + driverClassName: ${DATA_SOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DB:demo}?createDatabaseIfNotExist=true&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC + username: ${MYSQL_USERNAME:demo} + password: ${MYSQL_PASSWORD:demo} + jpa: + database-platform: ${JPA_DATABASE_PLATFORM:org.hibernate.dialect.MySQLDialect} + properties: + hibernate: + dialect: ${JPA_HIBERNATE_DIALECT:org.hibernate.dialect.MySQLDialect} +``` + +- H2 (Embedded) +- + +```yaml +spring: + datasource: + driverClassName: ${DATA_SOURCE_DRIVER_CLASS_NAME:org.h2.Driver} + url: jdbc:h2:file:${H2_DB_PATH:./data/db};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: ${H2_USERNAME:sa} + password: ${H2_PASSWORD:password} + jpa: + database-platform: ${JPA_DATABASE_PLATFORM:org.hibernate.dialect.H2Dialect} + h2: + console: + enabled: ${H2_CONSOLE_ENABLED:true} +``` + +- Avoid the Lazy Initialization Problem + +```yaml +spring: + jpa: + properties: + hibernate: + enable_lazy_load_no_trans: ${HIBERNATE_LAZY_NO_TRANS:true} +``` + # Contributors + - Sambo Chea ### Language and Framework + - Spring Boot: 2.6.6 - Kotlin: 1.6.20 - Gradle: 7.4.1 diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 792b047..605dfa7 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("io.spring.dependency-management") kotlin("jvm") kotlin("plugin.spring") + kotlin("plugin.jpa") } val kotlinVersion = "1.6.20" @@ -32,6 +33,10 @@ springBoot { } dependencies { + // Spring Data JPA (Required for Database Layer) + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + // Migrating from SpringFox implementation("org.springdoc:springdoc-openapi-ui:1.6.7") @@ -44,6 +49,10 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + runtimeOnly("com.h2database:h2") + runtimeOnly("org.postgresql:postgresql") + testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisConfig.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisConfig.kt new file mode 100644 index 0000000..383f5f0 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisConfig.kt @@ -0,0 +1,20 @@ +package com.cubetiqs.web.modules.redis + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate + +@RedisModule +@Configuration +class RedisConfig @Autowired constructor( + private val connectionFactory: RedisConnectionFactory, +) { + @Bean + fun redisTemplate(): RedisTemplate { + val template = RedisTemplate() + template.setConnectionFactory(connectionFactory) + return template + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisController.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisController.kt new file mode 100644 index 0000000..1622f2b --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisController.kt @@ -0,0 +1,31 @@ +package com.cubetiqs.web.modules.redis + +import com.cubetiqs.web.util.RouteConstants +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.web.bind.annotation.* + +@RedisModule +@Tag(name = "Redis Controller") +@RestController +@RequestMapping(RouteConstants.INDEX + "redis") +class RedisController @Autowired constructor( + private val redisTemplate: RedisTemplate, +) { + @GetMapping("/{key}") + fun getAll( + @PathVariable("key") key: String, + ): Collection { + return redisTemplate.opsForValue().multiGet(listOf(key)) ?: listOf() + } + + @PostMapping("/{key}") + fun set( + @PathVariable("key") key: String, + @RequestBody body: RedisKVModel + ): RedisKVModel { + redisTemplate.opsForValue().set(key, body) + return body + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisKVModel.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisKVModel.kt new file mode 100644 index 0000000..d701728 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisKVModel.kt @@ -0,0 +1,14 @@ +package com.cubetiqs.web.modules.redis + +import java.io.Serializable + +data class RedisKVModel( + var key: String? = null, + var value: Any? = null, +) : Serializable { + companion object { + fun create(key: String, value: Any): RedisKVModel { + return RedisKVModel(key, value) + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisModule.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisModule.kt new file mode 100644 index 0000000..3fc97d6 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/redis/RedisModule.kt @@ -0,0 +1,6 @@ +package com.cubetiqs.web.modules.redis + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty + +@ConditionalOnProperty(name = ["spring.redis.enabled"], havingValue = "true") +annotation class RedisModule \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserController.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserController.kt new file mode 100644 index 0000000..63fd5d3 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserController.kt @@ -0,0 +1,60 @@ +package com.cubetiqs.web.modules.user + +import com.cubetiqs.web.util.RouteConstants +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import org.springdoc.core.converters.models.PageableAsQueryParam +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.web.bind.annotation.* +import java.util.* + +@UserModule +@Tag(name = "User Controller") +@RestController +@RequestMapping(RouteConstants.INDEX + "user") +class UserController @Autowired constructor( + private val userRepository: UserRepository, +) { + @GetMapping + @PageableAsQueryParam + fun getAll( + @Parameter(hidden = true) + pageable: Pageable?, + ): Page { + return userRepository.findAll(pageable ?: Pageable.unpaged()) + } + + @ResponseStatus(value = org.springframework.http.HttpStatus.CREATED) + @PostMapping + fun create( + @RequestBody body: UserEntity + ): UserEntity { + return userRepository.save(body) + } + + @ResponseStatus(value = org.springframework.http.HttpStatus.OK) + @PutMapping("/{id}") + fun update( + @PathVariable id: String, + @RequestBody body: UserEntity + ): UserEntity { + val user = userRepository.findById(UUID.fromString(id)).orElseThrow { + throw IllegalArgumentException("User not found") + } + body.id = user.id + return userRepository.save(body) + } + + @ResponseStatus(value = org.springframework.http.HttpStatus.NO_CONTENT) + @DeleteMapping("/{id}") + fun delete( + @PathVariable id: String, + ) { + val user = userRepository.findById(UUID.fromString(id)).orElseThrow { + throw IllegalArgumentException("User not found") + } + userRepository.delete(user) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserEntity.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserEntity.kt new file mode 100644 index 0000000..3cbcec2 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserEntity.kt @@ -0,0 +1,30 @@ +package com.cubetiqs.web.modules.user + +import org.hibernate.Hibernate +import java.io.Serializable +import java.util.* +import javax.persistence.* + +@Entity +@Table(name = "user") +open class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: UUID? = null, + + @Column(name = "name", length = 50) + open var name: String? = null, + + @Column(name = "username", length = 50, unique = true) + open var username: String? = null, +) : Serializable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as UserEntity + + return id != null && id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserModule.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserModule.kt new file mode 100644 index 0000000..3ed8336 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserModule.kt @@ -0,0 +1,6 @@ +package com.cubetiqs.web.modules.user + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty + +@ConditionalOnProperty(name = ["module.user.enabled", "spring.datasource.enabled"], havingValue = "true") +annotation class UserModule \ No newline at end of file diff --git a/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserRepository.kt b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserRepository.kt new file mode 100644 index 0000000..c84add5 --- /dev/null +++ b/api/src/main/kotlin/com/cubetiqs/web/modules/user/UserRepository.kt @@ -0,0 +1,9 @@ +package com.cubetiqs.web.modules.user + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.util.* + +@UserModule +@Repository +interface UserRepository : JpaRepository \ No newline at end of file diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index bea3b1f..41aaec0 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -1,8 +1,22 @@ spring: profiles: - active: ${APP_PROFILE:dev} + active: ${APP_PROFILE:demo} application: name: cubetiq-api-service + redis: + enabled: ${REDIS_ENABLED:false} + host: ${REDIS_HOST:localhost} + password: ${REDIS_PASSWORD:null} + datasource: + enabled: ${DATASOURCE_ENABLED:false} + driverClassName: ${DATA_SOURCE_DRIVER_CLASS_NAME:org.h2.Driver} + url: jdbc:h2:file:${H2_DB_PATH:${cubetiq.app.data-dir}/data/db};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: ${H2_USERNAME:sa} + password: ${H2_PASSWORD:password} + +module: + user: + enabled: ${MODULE_USER_ENABLED:false} cubetiq: app: diff --git a/api/src/main/resources/banner.txt b/api/src/main/resources/banner.txt new file mode 100644 index 0000000..609d38b --- /dev/null +++ b/api/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +----------------------- CUBEIQ @sombochea --------------------------- +________ _____ ___ __ ______ +__ ___/________ ___________(_)_______ _______ _ __ | / /_____ ___ /_ +_____ \ ___ __ \__ ___/__ / __ __ \__ __ `/ __ | /| / / _ _ \__ __ \ +____/ / __ /_/ /_ / _ / _ / / /_ /_/ / __ |/ |/ / / __/_ /_/ / +/____/ _ .___/ /_/ /_/ /_/ /_/ _\__, / ____/|__/ \___/ /_.___/ + /_/ /____/ +Spring Boot Version: ${spring-boot.formatted-version} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 416fbe7..3e6ffa5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false kotlin("jvm") version "1.6.20" apply false kotlin("plugin.spring") version "1.6.20" apply false - // kotlin("plugin.jpa") version "1.6.10" apply false + kotlin("plugin.jpa") version "1.6.20" apply false } allprojects { diff --git a/k8s/03-secret.yaml b/k8s/03-secret.yaml index e767ec6..35f09ff 100644 --- a/k8s/03-secret.yaml +++ b/k8s/03-secret.yaml @@ -15,8 +15,13 @@ metadata: namespace: spring-web-dev name: spring-web-dev-secret stringData: + REDIS_HOST: redis-service REDIS_PASSWORD: demo POSTGRES_USER: demo POSTGRES_DB: demo POSTGRES_PASSWORD: demo + APP_DATA_DIR: /opt/cubetiq/data + MODULE_USER_ENABLED: true + DATASOURCE_ENABLED: true + REDIS_ENABLED: true type: Opaque \ No newline at end of file