Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] JOOQ 기반 카테고리 가게 커서 조회 #28

Merged
merged 11 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Check warning on line 12 in src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt

View check run for this annotation

Codecov / codecov/patch

src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt#L11-L12

Added lines #L11 - L12 were not covered by tests

ctx.exception(translator.translate("jOOQ", ctx.sql(), ctx.sqlException()!!))
}

Check warning on line 15 in src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt

View check run for this annotation

Codecov / codecov/patch

src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt#L14-L15

Added lines #L14 - L15 were not covered by tests
}
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