Skip to content

Commit

Permalink
[feature] JOOQ 기반 카테고리 가게 커서 조회 (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
dojinyou authored Oct 14, 2023
1 parent 2c0eb29 commit 1c72804
Show file tree
Hide file tree
Showing 18 changed files with 581 additions and 36 deletions.
45 changes: 44 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@

import com.epages.restdocs.apispec.gradle.OpenApi3Task
import org.gradle.internal.impldep.org.junit.experimental.categories.Categories.CategoryFilter.exclude
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jooq.meta.jaxb.Logging

plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
id("org.jlleitschuh.gradle.ktlint")
id("com.epages.restdocs-api-spec")
id("nu.studer.jooq")
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
Expand All @@ -22,6 +25,7 @@ val restdocsApiVersion = "${property("restdocsApiVersion")}"
val springMockkVersion = "${property("springMockkVersion")}"
val autoParamsVersion = "${property("autoParamsVersion")}"
val jacocoVersion = "${property("jacocoVersion")}"
val jooqVersion = "${property("jooqVersion")}"

java {
sourceCompatibility = JavaVersion.valueOf("VERSION_$javaVersion")
Expand Down Expand Up @@ -50,6 +54,8 @@ dependencies {
// database
runtimeOnly("org.postgresql:postgresql")
implementation("org.liquibase:liquibase-core")
jooqGenerator("org.jooq:jooq-meta-extensions-liquibase")
jooqGenerator("org.liquibase:liquibase-core")

// test
testImplementation("org.testcontainers:postgresql")
Expand All @@ -73,6 +79,42 @@ tasks.withType<KotlinCompile> {
}
}

jooq {
version.set(dependencyManagement.importedProperties["jooq.version"])

configurations {
create("main") {
jooqConfiguration.apply {
logging = Logging.WARN
generator.apply {
name = "org.jooq.codegen.DefaultGenerator"
database.apply {
name = "org.jooq.meta.extensions.liquibase.LiquibaseDatabase"
properties.add(
org.jooq.meta.jaxb.Property().withKey("rootPath")
.withValue("${project.projectDir}/src/main/resources")
)
properties.add(
org.jooq.meta.jaxb.Property().withKey("scripts")
.withValue("/db/changelog-master.yml")
)
}
generate.apply {
isDeprecated = false
isRecords = true
isImmutablePojos = true
isFluentSetters = true
}
target.apply {
packageName = "com.mjucow.eatda.jooq"
}
strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy"
}
}
}
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
Expand Down Expand Up @@ -160,7 +202,8 @@ tasks.jacocoTestCoverageVerification {
excludes = listOf(
"com.mjucow.eatda.EatdaApplicationKt",
"*.common.*",
"*.dto.*"
"*.dto.*",
"com.mjucow.eatda.jooq.*"
)
}
}
Expand Down
6 changes: 5 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
applicationVersion=0.0.1-SNAPSHOT

### Project configs ###
projectGroup="com.mju-cow"
projectGroup="com.mjucow"

### Project depdency versions ###
kotlinVersion=1.9.10
Expand All @@ -17,6 +17,10 @@ jacocoVersion=0.8.9
springBootVersion=3.1.2
springDependencyManagementVersion=1.1.3

### DB depedency versions ###
jooqPluginVersion=8.2
jooqVersion="3.18.4"

### Test dependency versions ###
testContainerVersion=1.19.0
restdocsApiVersion=0.18.2
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ pluginManagement {
val springDependencyManagementVersion: String by settings
val ktlintVersion: String by settings
val restdocsApiVersion: String by settings
val jooqPluginVersion: String by settings

plugins {
id("org.springframework.boot") version springBootVersion
id("io.spring.dependency-management") version springDependencyManagementVersion
id("org.jlleitschuh.gradle.ktlint") version ktlintVersion
id("com.epages.restdocs-api-spec") version restdocsApiVersion
id("nu.studer.jooq") version jooqPluginVersion
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.mjucow.eatda.common.config

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.jsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

Expand All @@ -15,9 +17,12 @@ class JacksonConfiguration {
*/
@Bean
fun objectMapper(): ObjectMapper {
return jacksonObjectMapper()
.registerModule(JavaTimeModule())
.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return jsonMapper {
addModule(kotlinModule())
addModule(JavaTimeModule())
configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
propertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.mjucow.eatda.common.config

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.persistence.Entity
import org.jooq.Record
import org.jooq.RecordMapper
import org.jooq.RecordMapperProvider
import org.jooq.RecordType
import org.jooq.SQLDialect
import org.jooq.conf.RenderNameCase
import org.jooq.impl.DataSourceConnectionProvider
import org.jooq.impl.DefaultConfiguration
import org.jooq.impl.DefaultDSLContext
import org.jooq.impl.DefaultExecuteListenerProvider
import org.jooq.impl.DefaultRecordMapper
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.sql.DataSource

@Configuration
@EnableTransactionManagement
@ImportAutoConfiguration(JooqAutoConfiguration::class)
class JooqContextConfiguration(
private val dataSource: DataSource,
private val objectMapper: ObjectMapper,
) {

@Bean
fun connectionProvider() = DataSourceConnectionProvider(TransactionAwareDataSourceProxy(dataSource))

@Bean
fun dsl() = DefaultDSLContext(configuration())

fun configuration(): DefaultConfiguration {
val jooqConfiguration = DefaultConfiguration()

val settings = jooqConfiguration.settings()
.withExecuteWithOptimisticLocking(true)
.withExecuteLogging(true)
.withMapConstructorParameterNamesInKotlin(true)
.withRenderNameCase(RenderNameCase.LOWER)

jooqConfiguration.set(settings)
jooqConfiguration.set(connectionProvider())
jooqConfiguration.set(DefaultExecuteListenerProvider(jooqToSpringExceptionTransformer()))
jooqConfiguration.setSQLDialect(SQLDialect.POSTGRES)
jooqConfiguration.setRecordMapperProvider(object : RecordMapperProvider {
override fun <R : Record?, E : Any?> provide(
recordType: RecordType<R>?,
type: Class<out E>?,
): RecordMapper<R, E> {
if (type?.annotations?.any { it.annotationClass == Entity::class } == true) {
return getEntityRecordMapper(type)
}

return DefaultRecordMapper(recordType, type)
}
})

return jooqConfiguration
}

@Bean
fun jooqToSpringExceptionTransformer() = JooqToSpringExceptionTransformer()

/**
* JPA Entity의 Secondary Constrcutor로 DefaultRecordMapper가 Mapping되지 않아
* CustomerRecordMapper로 mapping 해야함. 클래스별로 따로 만들지 않고 map으로 변환 후 objectMapper로 mapping 처리
*/
private fun <R : Record?, E : Any?> getEntityRecordMapper(type: Class<out E>): RecordMapper<R, E> {
return RecordMapper<R, E> { objectMapper.convertValue(it!!.intoMap(), type) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mjucow.eatda.common.config

import org.jooq.ExecuteContext
import org.jooq.ExecuteListener
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator

class JooqToSpringExceptionTransformer : ExecuteListener {
override fun exception(ctx: ExecuteContext) {
if (ctx.sqlException() == null) return

val dialect = ctx.configuration().dialect()
val translator = SQLErrorCodeSQLExceptionTranslator(dialect.name)

ctx.exception(translator.translate("jOOQ", ctx.sql(), ctx.sqlException()!!))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.mjucow.eatda.domain.store.service.query.dto.StoreDto
import com.mjucow.eatda.persistence.store.StoreRepository
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.domain.SliceImpl
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -14,11 +15,14 @@ import org.springframework.transaction.annotation.Transactional
class StoreQueryService(
val repository: StoreRepository,
) {
fun findAllByCursor(id: Long? = null, page: Pageable): Slice<StoreDto> {
return if (id == null) {
repository.findAllByOrderByIdDesc(page).map(StoreDto::from)
fun findAllByCategoryAndCursor(id: Long? = null, categoryId: Long? = null, page: Pageable): Slice<StoreDto> {
return if (categoryId == null) {
repository.findAllByIdLessThanOrderByIdDesc(page, id).map(StoreDto::from)
} else {
repository.findByIdLessThanOrderByIdDesc(id, page).map(StoreDto::from)
// FIXME(cache): store 캐시 처리 이후 store 조회 개선하기
val storeIds = repository.findIdsByCategoryIdOrderByIdDesc(categoryId, page, id)
val stores = repository.findAllByIdInOrderByIdDesc(storeIds.content).map(StoreDto::from)
SliceImpl(stores, page, storeIds.hasNext())
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mjucow.eatda.persistence.store

import com.mjucow.eatda.domain.store.entity.Store
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice

interface StoreCustomRepository {
fun findIdsByCategoryIdOrderByIdDesc(categoryId: Long, page: Pageable, id: Long? = null): Slice<Long>
fun findAllByIdLessThanOrderByIdDesc(page: Pageable, id: Long? = null): Slice<Store>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.mjucow.eatda.persistence.store

import com.mjucow.eatda.domain.store.entity.Store
import com.mjucow.eatda.jooq.Tables.STORE
import com.mjucow.eatda.jooq.Tables.STORE_CATEGORY
import org.jooq.DSLContext
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.domain.SliceImpl
import org.springframework.stereotype.Repository
import kotlin.math.min

@Repository
class StoreCustomRepositoryImpl(
private val db: DSLContext,
) : StoreCustomRepository {
override fun findIdsByCategoryIdOrderByIdDesc(categoryId: Long, page: Pageable, id: Long?): Slice<Long> {
val query = db.select(STORE_CATEGORY.STORE_ID)
.from(STORE_CATEGORY)
.where(STORE_CATEGORY.CATEGORY_ID.eq(categoryId))

if (id != null) {
query.and(STORE_CATEGORY.STORE_ID.lessThan(id))
}

val result = query.orderBy(STORE_CATEGORY.STORE_ID.desc())
.limit(page.pageSize + 1)
.fetch()
.into(Long::class.java)

val content = result.subList(0, min(result.size, page.pageSize))
val hasNext = result.size > page.pageSize

return SliceImpl(content, page, hasNext)
}

override fun findAllByIdLessThanOrderByIdDesc(page: Pageable, id: Long?): Slice<Store> {
val query = db.select()
.from(STORE)

if (id != null) {
query.where(STORE.ID.lessThan(id))
}

val result = query.orderBy(STORE.ID.desc())
.limit(page.pageSize + 1)
.fetch()
.into(Store::class.java)

val content = result.subList(0, min(result.size, page.pageSize))
val hasNext = result.size > page.pageSize

return SliceImpl(content, page, hasNext)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.mjucow.eatda.persistence.store

import com.mjucow.eatda.domain.store.entity.Store
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.jpa.repository.JpaRepository

interface StoreRepository : JpaRepository<Store, Long> {
fun findAllByOrderByIdDesc(page: Pageable): Slice<Store>
fun findByIdLessThanOrderByIdDesc(id: Long, page: Pageable): Slice<Store>
interface StoreRepository : JpaRepository<Store, Long>, StoreCustomRepository {
fun existsByName(name: String): Boolean
fun findAllByIdInOrderByIdDesc(id: List<Long>): List<Store>
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ class StoreController(

@GetMapping
@ResponseStatus(HttpStatus.OK)
fun findAllByCursor(
@RequestParam("id", required = false) id: Long,
fun findAllByCategoryIdAndCursor(
@RequestParam("storeId", required = false) id: Long?,
@RequestParam("categoryId", required = false) categoryId: Long?,
@PageableDefault(size = 20) page: Pageable,
): ApiResponse<Slice<StoreDto>> {
val storeDtos = storeQueryService.findAllByCursor(id, page)
val storeDtos = storeQueryService.findAllByCategoryAndCursor(id, categoryId, page)
return ApiResponse.success(storeDtos)
}

Expand Down
10 changes: 6 additions & 4 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ spring:
show_sql: true

datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:15432/eatda
username: local
password: local
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://localhost:15432/eatda
username: local
password: local
maximum-pool-size: 5
connection-timeout: 1100
keepalive-time: 30000
validation-timeout: 1000
max-lifetime: 600000

jooq:
sql-dialect: postgres
---

spring:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE UNIQUE INDEX idx_store_category_category_id_store_id ON store_category(category_id, store_id);
Loading

0 comments on commit 1c72804

Please sign in to comment.