From a74a92e675a91b0bc56bc7194d3fabb305f20e93 Mon Sep 17 00:00:00 2001 From: doji Date: Fri, 6 Oct 2023 23:18:20 +0900 Subject: [PATCH 01/11] jooq configuration settings.gradle.kts --- build.gradle.kts | 43 +++++++++++++++++ gradle.properties | 6 ++- settings.gradle.kts | 2 + .../common/config/JooqContextConfiguration.kt | 48 +++++++++++++++++++ .../JooqToSpringExceptionTransformer.kt | 16 +++++++ src/main/resources/application.yml | 10 ++-- .../231006-add_store_cateogry_index.sql | 1 + 7 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt create mode 100644 src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt create mode 100644 src/main/resources/db/changelog/231006-add_store_cateogry_index.sql diff --git a/build.gradle.kts b/build.gradle.kts index 3c3a319..b0d96a2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,16 @@ import com.epages.restdocs.apispec.gradle.OpenApi3Task import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jooq.meta.jaxb.Logging +import java.io.FileInputStream +import java.util.* 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") @@ -22,6 +26,10 @@ val restdocsApiVersion = "${property("restdocsApiVersion")}" val springMockkVersion = "${property("springMockkVersion")}" val autoParamsVersion = "${property("autoParamsVersion")}" val jacocoVersion = "${property("jacocoVersion")}" +val jooqVersion = "${property("jooqVersion")}" +val resourceProperties = Properties().apply { + load(FileInputStream(File("${rootProject.rootDir}/src/main/resources/application.yml"))) +} java { sourceCompatibility = JavaVersion.valueOf("VERSION_$javaVersion") @@ -50,6 +58,7 @@ dependencies { // database runtimeOnly("org.postgresql:postgresql") implementation("org.liquibase:liquibase-core") + jooqGenerator("org.postgresql:postgresql") // test testImplementation("org.testcontainers:postgresql") @@ -73,6 +82,40 @@ tasks.withType { } } +jooq { + version.set(dependencyManagement.importedProperties["jooq.version"]) + + configurations { + create("main") { + jooqConfiguration.apply { + logging = Logging.WARN + jdbc.apply { + url = resourceProperties["url"] as String + user = resourceProperties["username"] as String + password = resourceProperties["password"] as String + } + generator.apply { + name = "org.jooq.codegen.DefaultGenerator" + database.apply { + name = "org.jooq.meta.postgres.PostgresDatabase" + inputSchema = "public" + } + generate.apply { + isDeprecated = false + isRecords = true + isImmutablePojos = true + isFluentSetters = true + } + target.apply { + packageName = "${group}.${rootProject.name}" + } + strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy" + } + } + } + } +} + tasks.withType { useJUnitPlatform() } diff --git a/gradle.properties b/gradle.properties index 5db63a1..e9ca100 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index e29da3a..cf1c169 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt new file mode 100644 index 0000000..ce8ccc6 --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt @@ -0,0 +1,48 @@ +package com.mjucow.eatda.common.config + +import org.jooq.SQLDialect +import org.jooq.impl.DataSourceConnectionProvider +import org.jooq.impl.DefaultConfiguration +import org.jooq.impl.DefaultDSLContext +import org.jooq.impl.DefaultExecuteListenerProvider +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, +) { + + @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) + + jooqConfiguration.set(settings) + jooqConfiguration.set(connectionProvider()) + jooqConfiguration.set(DefaultExecuteListenerProvider(jooqToSpringExceptionTransformer())) + jooqConfiguration.setSQLDialect(SQLDialect.POSTGRES) + + return jooqConfiguration + } + + @Bean + fun jooqToSpringExceptionTransformer() = JooqToSpringExceptionTransformer() + +} + diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt new file mode 100644 index 0000000..32a2ec8 --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqToSpringExceptionTransformer.kt @@ -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()!!)) + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8022b65..ade777c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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: diff --git a/src/main/resources/db/changelog/231006-add_store_cateogry_index.sql b/src/main/resources/db/changelog/231006-add_store_cateogry_index.sql new file mode 100644 index 0000000..08e2932 --- /dev/null +++ b/src/main/resources/db/changelog/231006-add_store_cateogry_index.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX idx_store_category_category_id_store_id ON store_category(category_id, store_id); From 5aefdfc2317a25767f854e7afafd4a97e326c823 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 7 Oct 2023 20:18:58 +0900 Subject: [PATCH 02/11] =?UTF-8?q?jooq=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=BB=A4=EC=84=9C=20=EA=B0=80=EA=B2=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 6 +- .../common/config/JooqContextConfiguration.kt | 25 ++ .../store/service/query/StoreQueryService.kt | 12 +- .../store/StoreCustomRepository.kt | 10 + .../store/StoreCustomRepositoryImpl.kt | 55 +++++ .../persistence/store/StoreRepository.kt | 7 +- .../presentation/store/StoreController.kt | 7 +- .../mjucow/eatda/domain/AbstractDataTest.kt | 4 + .../query/StoreQueryServiceDataTest.kt | 14 +- .../store/StoreCustomRepositoryImplTest.kt | 222 ++++++++++++++++++ .../store/StoreControllerMvcTest.kt | 14 +- src/test/resources/logback-test.xml | 5 + 12 files changed, 354 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepository.kt create mode 100644 src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt create mode 100644 src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index b0d96a2..58b9b14 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -99,6 +99,7 @@ jooq { database.apply { name = "org.jooq.meta.postgres.PostgresDatabase" inputSchema = "public" + excludes = "databasechangelog|databasechangeloglock" } generate.apply { isDeprecated = false @@ -107,7 +108,7 @@ jooq { isFluentSetters = true } target.apply { - packageName = "${group}.${rootProject.name}" + packageName = "com.mjucow.eatda" } strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy" } @@ -203,7 +204,8 @@ tasks.jacocoTestCoverageVerification { excludes = listOf( "com.mjucow.eatda.EatdaApplicationKt", "*.common.*", - "*.dto.*" + "*.dto.*", + "*.tables.*", ) } } diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt index ce8ccc6..a08ca8f 100644 --- a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt @@ -1,10 +1,17 @@ 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.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 @@ -18,6 +25,7 @@ import javax.sql.DataSource @ImportAutoConfiguration(JooqAutoConfiguration::class) class JooqContextConfiguration( private val dataSource: DataSource, + private val objectMapper: ObjectMapper, ) { @Bean @@ -32,11 +40,24 @@ class JooqContextConfiguration( val settings = jooqConfiguration.settings() .withExecuteWithOptimisticLocking(true) .withExecuteLogging(true) + .withMapConstructorParameterNamesInKotlin(true) jooqConfiguration.set(settings) jooqConfiguration.set(connectionProvider()) jooqConfiguration.set(DefaultExecuteListenerProvider(jooqToSpringExceptionTransformer())) jooqConfiguration.setSQLDialect(SQLDialect.POSTGRES) + jooqConfiguration.setRecordMapperProvider(object : RecordMapperProvider { + override fun provide( + recordType: RecordType?, + type: Class?, + ): RecordMapper { + if (type?.annotations?.any { it.annotationClass == Entity::class } == true) { + return getEntityRecordMapper(type) + } + + return DefaultRecordMapper(recordType, type) + } + }) return jooqConfiguration } @@ -44,5 +65,9 @@ class JooqContextConfiguration( @Bean fun jooqToSpringExceptionTransformer() = JooqToSpringExceptionTransformer() + private fun getEntityRecordMapper(type: Class): RecordMapper { + return RecordMapper { objectMapper.convertValue(it!!.intoMap(), type) } + } + } diff --git a/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt b/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt index 3a4958a..47c05ee 100644 --- a/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt +++ b/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt @@ -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 @@ -14,11 +15,14 @@ import org.springframework.transaction.annotation.Transactional class StoreQueryService( val repository: StoreRepository, ) { - fun findAllByCursor(id: Long? = null, page: Pageable): Slice { - return if (id == null) { - repository.findAllByOrderByIdDesc(page).map(StoreDto::from) + fun findAllByCategoryAndCursor(id: Long? = null, categoryId: Long? = null, page: Pageable): Slice { + 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()) } } diff --git a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepository.kt b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepository.kt new file mode 100644 index 0000000..134dc26 --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepository.kt @@ -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 + fun findAllByIdLessThanOrderByIdDesc(page: Pageable, id: Long? = null): Slice +} diff --git a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt new file mode 100644 index 0000000..af3e868 --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt @@ -0,0 +1,55 @@ +package com.mjucow.eatda.persistence.store + +import com.mjucow.eatda.Tables.STORE +import com.mjucow.eatda.Tables.STORE_CATEGORY +import com.mjucow.eatda.domain.store.entity.Store +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 { + 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 { + 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) + } +} diff --git a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreRepository.kt b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreRepository.kt index 340bcf3..c986fd5 100644 --- a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreRepository.kt +++ b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreRepository.kt @@ -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 { - fun findAllByOrderByIdDesc(page: Pageable): Slice - fun findByIdLessThanOrderByIdDesc(id: Long, page: Pageable): Slice +interface StoreRepository : JpaRepository, StoreCustomRepository { fun existsByName(name: String): Boolean + fun findAllByIdInOrderByIdDesc(id: List): List } diff --git a/src/main/kotlin/com/mjucow/eatda/presentation/store/StoreController.kt b/src/main/kotlin/com/mjucow/eatda/presentation/store/StoreController.kt index 201c835..f6f95dc 100644 --- a/src/main/kotlin/com/mjucow/eatda/presentation/store/StoreController.kt +++ b/src/main/kotlin/com/mjucow/eatda/presentation/store/StoreController.kt @@ -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> { - val storeDtos = storeQueryService.findAllByCursor(id, page) + val storeDtos = storeQueryService.findAllByCategoryAndCursor(id, categoryId, page) return ApiResponse.success(storeDtos) } diff --git a/src/test/kotlin/com/mjucow/eatda/domain/AbstractDataTest.kt b/src/test/kotlin/com/mjucow/eatda/domain/AbstractDataTest.kt index c9f488b..ab38c44 100644 --- a/src/test/kotlin/com/mjucow/eatda/domain/AbstractDataTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/domain/AbstractDataTest.kt @@ -1,13 +1,17 @@ package com.mjucow.eatda.domain +import com.mjucow.eatda.common.config.JacksonConfiguration +import com.mjucow.eatda.common.config.JooqContextConfiguration import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Import import org.springframework.test.context.ActiveProfiles import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers +@Import(value = [JooqContextConfiguration::class, JacksonConfiguration::class]) @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) diff --git a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt index b4da686..dcb9605 100644 --- a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt @@ -53,17 +53,17 @@ class StoreQueryServiceDataTest : AbstractDataTest() { @Test fun findAllByCursorWithNullId() { // given - val pageSize = Store.MAX_NAME_LENGTH + val pageSize = 2 val latestId = IntStream.range(0, pageSize * 2).mapToLong { repository.save(Store(name = "validName$it", address = StoreMother.ADDRESS)).id }.max().asLong val page = Pageable.ofSize(pageSize) // when - val result = storeQueryService.findAllByCursor(page = page) + val result = storeQueryService.findAllByCategoryAndCursor(page = page) // then - assertThat(result.content[0].id).isEqualTo(latestId) + assertThat(result).anyMatch { it.id == latestId } } @DisplayName("데이터가 페이지 크기보다 크다면 페이지 크기만큼만 조회된다") @@ -77,7 +77,7 @@ class StoreQueryServiceDataTest : AbstractDataTest() { val page = Pageable.ofSize(pageSize) // when - val result = storeQueryService.findAllByCursor(id = (pageSize * 2).toLong(), page = page) + val result = storeQueryService.findAllByCategoryAndCursor(id = (pageSize * 2).toLong(), page = page) // then assertThat(result.content.size).isEqualTo(pageSize) @@ -89,12 +89,12 @@ class StoreQueryServiceDataTest : AbstractDataTest() { fun findAllByCursor() { // given val store = StoreMother.create() - repository.save(store) + val storeId = repository.save(store).id val pageSize = 10 val page = Pageable.ofSize(pageSize) // when - val result = storeQueryService.findAllByCursor(id = store.id + 1, page = page) + val result = storeQueryService.findAllByCategoryAndCursor(id = storeId + 1, page = page) // then assertThat(result.content.size).isLessThan(pageSize) @@ -110,7 +110,7 @@ class StoreQueryServiceDataTest : AbstractDataTest() { val page = Pageable.ofSize(10) // when - val result = storeQueryService.findAllByCursor(id = store.id, page = page) + val result = storeQueryService.findAllByCategoryAndCursor(id = store.id, page = page) // then assertThat(result).isEmpty() diff --git a/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt b/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt new file mode 100644 index 0000000..ca89c6a --- /dev/null +++ b/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt @@ -0,0 +1,222 @@ +package com.mjucow.eatda.persistence.store + +import com.mjucow.eatda.common.config.JacksonConfiguration +import com.mjucow.eatda.domain.AbstractDataTest +import com.mjucow.eatda.domain.store.entity.Category +import com.mjucow.eatda.domain.store.entity.Store +import com.mjucow.eatda.domain.store.entity.objectmother.StoreMother +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import org.springframework.data.domain.Pageable +import java.util.stream.IntStream + +@Import(JacksonConfiguration::class) +class StoreCustomRepositoryImplTest : AbstractDataTest() { + @Autowired + lateinit var storeCustomRepositoryImpl: StoreCustomRepositoryImpl + @Autowired + lateinit var categoryRepository: CategoryRepository + @Autowired + lateinit var storeRepository: StoreRepository + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 X, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 많음") + @Test + fun test11() { + // given + val pageSize = 5 + val category = categoryRepository.saveAndFlush(Category("test")) + + val minStoreId = IntStream.range(0, pageSize + 1).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + val updated = storeRepository.saveAndFlush(store) + updated.addCategory(category) + storeRepository.saveAndFlush(updated).id + }.min().asLong + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize) + ) + + // then + assertThat(result).hasSize(pageSize).allMatch { it > minStoreId } + assertThat(result.hasNext()).isTrue() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 X, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 적음") + @Test + fun test12() { + // given + val pageSize = 5 + val dataSize = 3 + val category = categoryRepository.saveAndFlush(Category("test")) + + val minStoreId = IntStream.range(0, dataSize).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + val updated = storeRepository.saveAndFlush(store) + updated.addCategory(category) + storeRepository.saveAndFlush(updated).id + }.min().asLong + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize) + ) + + // then + assertThat(result).hasSize(dataSize).allMatch { it >= minStoreId } + assertThat(result.hasNext()).isFalse() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 X, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 없음") + @Test + fun test13() { + // given + val pageSize = 5 + val category = categoryRepository.saveAndFlush(Category("test")) + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize) + ) + + // then + assertThat(result).isEmpty() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 많음") + @Test + fun test21() { + // given + val pageSize = 5 + val category = categoryRepository.saveAndFlush(Category("test")) + + val maxStoreId = IntStream.range(0, pageSize * 2).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + val updated = storeRepository.saveAndFlush(store) + updated.addCategory(category) + storeRepository.saveAndFlush(updated).id + }.max().asLong + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize), + id = maxStoreId + ) + + // then + assertThat(result).hasSize(pageSize).allMatch { it < maxStoreId } + assertThat(result.hasNext()).isTrue() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 적음") + @Test + fun test22() { + // given + val pageSize = 5 + val dataSize = 2 + val category = categoryRepository.saveAndFlush(Category("test")) + + val minStoreId = IntStream.range(0, pageSize * 2).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + val updated = storeRepository.saveAndFlush(store) + updated.addCategory(category) + storeRepository.saveAndFlush(updated).id + }.min().asLong + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize), + id = minStoreId + dataSize + ) + + // then + assertThat(result).hasSize(dataSize).allMatch { it < minStoreId + dataSize } + assertThat(result.hasNext()).isFalse() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터 없음") + @Test + fun test23() { + // given + val pageSize = 5 + val category = categoryRepository.saveAndFlush(Category("test")) + + // when + val result = storeCustomRepositoryImpl.findIdsByCategoryIdOrderByIdDesc( + categoryId = category.id, + page = Pageable.ofSize(pageSize), + id = Long.MAX_VALUE + ) + + // then + assertThat(result).isEmpty() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 X, 페이지 크기 대비 실제 데이터 많음") + @Test + fun test31() { + // given + val pageSize = 2 + val maxStoreId = IntStream.range(0, pageSize * 2).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + storeRepository.saveAndFlush(store).id + }.max().asLong + + // when + val result = storeCustomRepositoryImpl.findAllByIdLessThanOrderByIdDesc( + page = Pageable.ofSize(pageSize), + id = maxStoreId + 1 + ) + + // then + assertThat(result.content.size).isEqualTo(pageSize) + assertThat(result.hasNext()).isTrue() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 X, 페이지 크기 대비 실제 데이터 적음") + @Test + fun test32() { + // given + val pageSize = 5 + val dataSize = 2 + + val maxStoreId = IntStream.range(0, dataSize).mapToLong { + val store = Store(name = "name$it", address = StoreMother.ADDRESS) + storeRepository.saveAndFlush(store).id + }.max().asLong + + // when + val result = storeCustomRepositoryImpl.findAllByIdLessThanOrderByIdDesc( + page = Pageable.ofSize(pageSize), + id = maxStoreId + 1 + ) + + // then + assertThat(result.content.size).isEqualTo(dataSize) + assertThat(result.hasNext()).isFalse() + } + + @DisplayName("커버 방식의 가게 조회: 마지막 조회한 가게 식별자 O, 카테고리 식별자 O, 페이지 크기 대비 실제 데이터") + @Test + fun test33() { + // given + val pageSize = 5 + + // when + val result = storeCustomRepositoryImpl.findAllByIdLessThanOrderByIdDesc( + page = Pageable.ofSize(pageSize) + ) + + // then + assertThat(result).isEmpty() + } +} diff --git a/src/test/kotlin/com/mjucow/eatda/presentation/store/StoreControllerMvcTest.kt b/src/test/kotlin/com/mjucow/eatda/presentation/store/StoreControllerMvcTest.kt index 7f28de0..9a677fa 100644 --- a/src/test/kotlin/com/mjucow/eatda/presentation/store/StoreControllerMvcTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/presentation/store/StoreControllerMvcTest.kt @@ -79,14 +79,15 @@ class StoreControllerMvcTest : AbstractMockMvcTest() { ) }.toList() - every { storeQueryService.findAllByCursor(any(), any()) } returns SliceImpl(storeDtos) + every { storeQueryService.findAllByCategoryAndCursor(any(), any(), any()) } returns SliceImpl(storeDtos) // when & then mockMvc.perform( RestDocumentationRequestBuilders.get( - "$BASE_URI?id={storeId}&size={pageSize}", + "$BASE_URI?storeId={storeId}&size={size}&categoryId={categoryId}", storeDtos.size + 1, - pageSize + pageSize, + null ) ) .andExpect(MockMvcResultMatchers.status().isOk) @@ -97,10 +98,11 @@ class StoreControllerMvcTest : AbstractMockMvcTest() { identifier = "store-findAllByCursor", resourceDetails = ResourceSnippetParametersBuilder() .tag("Store") - .description("커서 기반 가게 조회") + .description("커서 기반 카테고리 가게 조회") .queryParameters( - ResourceDocumentation.parameterWithName("storeId").description("조회한 마지막 가게 식별자"), - ResourceDocumentation.parameterWithName("pageSize").description("조회할 페이지 사이즈") + ResourceDocumentation.parameterWithName("storeId").description("조회한 마지막 가게 식별자").optional(), + ResourceDocumentation.parameterWithName("categoryId").description("조회하는 가게의 카테고리의 식별자").optional(), + ResourceDocumentation.parameterWithName("size").description("조회할 페이지 사이즈").optional() ) .responseFields( PayloadDocumentation.fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메세지"), diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index f471b6e..e7983ea 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -11,5 +11,10 @@ + + + + + From 4d1e846e1ab5e491b42b42b63f86345618b56c9d Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 7 Oct 2023 20:21:17 +0900 Subject: [PATCH 03/11] lint --- build.gradle.kts | 4 ++-- .../mjucow/eatda/common/config/JooqContextConfiguration.kt | 2 -- .../eatda/domain/store/service/query/StoreQueryService.kt | 2 +- .../eatda/persistence/store/StoreCustomRepositoryImplTest.kt | 2 ++ 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 58b9b14..b9c826b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import com.epages.restdocs.apispec.gradle.OpenApi3Task import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jooq.meta.jaxb.Logging import java.io.FileInputStream -import java.util.* +import java.util.Properties plugins { id("org.springframework.boot") @@ -205,7 +205,7 @@ tasks.jacocoTestCoverageVerification { "com.mjucow.eatda.EatdaApplicationKt", "*.common.*", "*.dto.*", - "*.tables.*", + "*.tables.*" ) } } diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt index a08ca8f..fc15f2e 100644 --- a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt @@ -68,6 +68,4 @@ class JooqContextConfiguration( private fun getEntityRecordMapper(type: Class): RecordMapper { return RecordMapper { objectMapper.convertValue(it!!.intoMap(), type) } } - } - diff --git a/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt b/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt index 47c05ee..53d87c3 100644 --- a/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt +++ b/src/main/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryService.kt @@ -19,7 +19,7 @@ class StoreQueryService( return if (categoryId == null) { repository.findAllByIdLessThanOrderByIdDesc(page, id).map(StoreDto::from) } else { - //FIXME(cache): store 캐시 처리 이후 store 조회 개선하기 + // 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()) diff --git a/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt b/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt index ca89c6a..923600b 100644 --- a/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImplTest.kt @@ -17,8 +17,10 @@ import java.util.stream.IntStream class StoreCustomRepositoryImplTest : AbstractDataTest() { @Autowired lateinit var storeCustomRepositoryImpl: StoreCustomRepositoryImpl + @Autowired lateinit var categoryRepository: CategoryRepository + @Autowired lateinit var storeRepository: StoreRepository From cddaed013770083986b6355c60ebf8dee9695a51 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 7 Oct 2023 20:28:07 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mjucow/eatda/common/config/JooqContextConfiguration.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt index fc15f2e..7096156 100644 --- a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt @@ -65,6 +65,10 @@ class JooqContextConfiguration( @Bean fun jooqToSpringExceptionTransformer() = JooqToSpringExceptionTransformer() + /** + * JPA Entity의 Secondary Constrcutor로 DefaultRecordMapper가 Mapping되지 않아 + * CustomerRecordMapper로 mapping 해야함. 클래스별로 따로 만들지 않고 map으로 변환 후 objectMapper로 mapping 처리 + */ private fun getEntityRecordMapper(type: Class): RecordMapper { return RecordMapper { objectMapper.convertValue(it!!.intoMap(), type) } } From b2b8fd8ae48c63619e5d6c58734036375a831c63 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 08:41:59 +0900 Subject: [PATCH 05/11] =?UTF-8?q?jooq=20liquibase=20=EA=B8=B0=EB=B0=98=20g?= =?UTF-8?q?enerate=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 29 +++++++++---------- .../store/StoreCustomRepositoryImpl.kt | 4 +-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b9c826b..4c7bb86 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,8 @@ 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 -import java.io.FileInputStream import java.util.Properties plugins { @@ -27,9 +27,6 @@ val springMockkVersion = "${property("springMockkVersion")}" val autoParamsVersion = "${property("autoParamsVersion")}" val jacocoVersion = "${property("jacocoVersion")}" val jooqVersion = "${property("jooqVersion")}" -val resourceProperties = Properties().apply { - load(FileInputStream(File("${rootProject.rootDir}/src/main/resources/application.yml"))) -} java { sourceCompatibility = JavaVersion.valueOf("VERSION_$javaVersion") @@ -58,7 +55,8 @@ dependencies { // database runtimeOnly("org.postgresql:postgresql") implementation("org.liquibase:liquibase-core") - jooqGenerator("org.postgresql:postgresql") + jooqGenerator("org.jooq:jooq-meta-extensions-liquibase") + jooqGenerator("org.liquibase:liquibase-core") // test testImplementation("org.testcontainers:postgresql") @@ -89,17 +87,18 @@ jooq { create("main") { jooqConfiguration.apply { logging = Logging.WARN - jdbc.apply { - url = resourceProperties["url"] as String - user = resourceProperties["username"] as String - password = resourceProperties["password"] as String - } generator.apply { name = "org.jooq.codegen.DefaultGenerator" database.apply { - name = "org.jooq.meta.postgres.PostgresDatabase" - inputSchema = "public" - excludes = "databasechangelog|databasechangeloglock" + 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 @@ -108,7 +107,7 @@ jooq { isFluentSetters = true } target.apply { - packageName = "com.mjucow.eatda" + packageName = "com.mjucow.eatda.jooq" } strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy" } @@ -205,7 +204,7 @@ tasks.jacocoTestCoverageVerification { "com.mjucow.eatda.EatdaApplicationKt", "*.common.*", "*.dto.*", - "*.tables.*" + "com.mjucow.eatda.jooq.*", ) } } diff --git a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt index af3e868..fdb0278 100644 --- a/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt +++ b/src/main/kotlin/com/mjucow/eatda/persistence/store/StoreCustomRepositoryImpl.kt @@ -1,8 +1,8 @@ package com.mjucow.eatda.persistence.store -import com.mjucow.eatda.Tables.STORE -import com.mjucow.eatda.Tables.STORE_CATEGORY 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 From 181f71fc37e167599fdd405ec9287eab2bb7ba04 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 08:42:38 +0900 Subject: [PATCH 06/11] lint --- build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4c7bb86..26e0399 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,6 @@ 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 -import java.util.Properties plugins { id("org.springframework.boot") @@ -204,7 +203,7 @@ tasks.jacocoTestCoverageVerification { "com.mjucow.eatda.EatdaApplicationKt", "*.common.*", "*.dto.*", - "com.mjucow.eatda.jooq.*", + "com.mjucow.eatda.jooq.*" ) } } From 554a53d9867592cd62ea991f2fe06913f7fcb1ff Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 09:09:43 +0900 Subject: [PATCH 07/11] jooq renden config --- .../com/mjucow/eatda/common/config/JooqContextConfiguration.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt index 7096156..1081937 100644 --- a/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JooqContextConfiguration.kt @@ -7,6 +7,7 @@ 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 @@ -41,6 +42,7 @@ class JooqContextConfiguration( .withExecuteWithOptimisticLocking(true) .withExecuteLogging(true) .withMapConstructorParameterNamesInKotlin(true) + .withRenderNameCase(RenderNameCase.LOWER) jooqConfiguration.set(settings) jooqConfiguration.set(connectionProvider()) From db1474d50162abb004ed47f2d54d09abef2fdaac Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 09:11:15 +0900 Subject: [PATCH 08/11] case ignore deserialize --- .../eatda/common/config/JacksonConfiguration.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/mjucow/eatda/common/config/JacksonConfiguration.kt b/src/main/kotlin/com/mjucow/eatda/common/config/JacksonConfiguration.kt index 30d816f..4e9d9f6 100644 --- a/src/main/kotlin/com/mjucow/eatda/common/config/JacksonConfiguration.kt +++ b/src/main/kotlin/com/mjucow/eatda/common/config/JacksonConfiguration.kt @@ -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 @@ -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) + } } } From b611450648a5529d781841d9221e82d77bcaf411 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 09:23:24 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=EC=8B=A4=ED=8C=A8=ED=95=B4=EB=8F=84=20re?= =?UTF-8?q?port=20=EC=83=9D=EC=84=B1=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebd0cb4..8cc3235 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: # Report upload - name: Upload coverage reports to Codecov + if: failure() uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 1e4bed7b81bc056b85d432c9dfc33b133d37e8f3 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 10:17:54 +0900 Subject: [PATCH 10/11] =?UTF-8?q?category=20=EA=B8=B0=EB=B0=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 - .../query/StoreQueryServiceDataTest.kt | 93 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cc3235..ebd0cb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,6 @@ jobs: # Report upload - name: Upload coverage reports to Codecov - if: failure() uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt index dcb9605..9ba2b4e 100644 --- a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt @@ -2,8 +2,10 @@ package com.mjucow.eatda.domain.store.service.query import autoparams.kotlin.AutoKotlinSource import com.mjucow.eatda.domain.AbstractDataTest +import com.mjucow.eatda.domain.store.entity.Category import com.mjucow.eatda.domain.store.entity.Store import com.mjucow.eatda.domain.store.entity.objectmother.StoreMother +import com.mjucow.eatda.persistence.store.CategoryRepository import com.mjucow.eatda.persistence.store.StoreRepository import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName @@ -22,6 +24,9 @@ class StoreQueryServiceDataTest : AbstractDataTest() { @Autowired lateinit var repository: StoreRepository + @Autowired + lateinit var categoryRepository: CategoryRepository + @DisplayName("id에 해당하는 store가 없다면 null을 반환한다") @ParameterizedTest @AutoKotlinSource @@ -115,4 +120,92 @@ class StoreQueryServiceDataTest : AbstractDataTest() { // then assertThat(result).isEmpty() } + + @DisplayName("id값이 없다면 최신 데이터를 조회한다: 특정 카테고리") + @Test + fun categoryTest1() { + // given + val category = categoryRepository.save(Category("test")) + val pageSize = 2 + val latestId = IntStream.range(0, pageSize * 2).mapToLong { + val store = Store(name = "validName$it", address = StoreMother.ADDRESS) + store.addCategory(category) + repository.saveAndFlush(store).id + }.max().asLong + val page = Pageable.ofSize(pageSize) + + // when + val result = storeQueryService.findAllByCategoryAndCursor(categoryId = category.id, page = page) + + // then + assertThat(result).anyMatch { it.id == latestId } + } + + @DisplayName("데이터가 페이지 크기보다 크다면 페이지 크기만큼만 조회된다: 특정 카테고리") + @Test + fun categoryTest2() { + // given + val category = categoryRepository.save(Category("test")) + val pageSize = Store.MAX_NAME_LENGTH - 1 + repeat(Store.MAX_NAME_LENGTH) { + val store = Store(name = "x".repeat(it + 1), address = StoreMother.ADDRESS) + store.addCategory(category) + repository.saveAndFlush(store) + } + val page = Pageable.ofSize(pageSize) + + // when + val result = storeQueryService.findAllByCategoryAndCursor( + categoryId = category.id, + page = page, + ) + + // then + assertThat(result.content.size).isEqualTo(pageSize) + assertThat(result.hasNext()).isTrue() + } + + @DisplayName("조회할 결과가 일부라면 일부만 조회된다: 특정 카테고리") + @Test + fun categoryTest3() { + // given + val category = categoryRepository.save(Category("test")) + val store = StoreMother.create() + store.addCategory(category) + val storeId = repository.save(store).id + val pageSize = 10 + val page = Pageable.ofSize(pageSize) + + // when + val result = storeQueryService.findAllByCategoryAndCursor( + categoryId = category.id, + id = storeId + 1, + page = page, + ) + + // then + assertThat(result.content.size).isLessThan(pageSize) + assertThat(result.hasNext()).isFalse() + } + + @DisplayName("조회할 데이터가 없다면 empty를 반환한다: 특정 카테고리") + @Test + fun categoryTest4() { + // given + val category = categoryRepository.save(Category("test")) + val store = StoreMother.create() + store.addCategory(category) + repository.save(store) + val page = Pageable.ofSize(10) + + // when + val result = storeQueryService.findAllByCategoryAndCursor( + categoryId = category.id, + id = store.id, + page = page, + ) + + // then + assertThat(result).isEmpty() + } } From 8d23c5b963e1519bf6c534e8b31bd289d02366c9 Mon Sep 17 00:00:00 2001 From: doji Date: Sat, 14 Oct 2023 10:19:43 +0900 Subject: [PATCH 11/11] lint --- .../domain/store/service/query/StoreQueryServiceDataTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt index 9ba2b4e..4687a68 100644 --- a/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt +++ b/src/test/kotlin/com/mjucow/eatda/domain/store/service/query/StoreQueryServiceDataTest.kt @@ -157,7 +157,7 @@ class StoreQueryServiceDataTest : AbstractDataTest() { // when val result = storeQueryService.findAllByCategoryAndCursor( categoryId = category.id, - page = page, + page = page ) // then @@ -180,7 +180,7 @@ class StoreQueryServiceDataTest : AbstractDataTest() { val result = storeQueryService.findAllByCategoryAndCursor( categoryId = category.id, id = storeId + 1, - page = page, + page = page ) // then @@ -202,7 +202,7 @@ class StoreQueryServiceDataTest : AbstractDataTest() { val result = storeQueryService.findAllByCategoryAndCursor( categoryId = category.id, id = store.id, - page = page, + page = page ) // then