Skip to content

Commit

Permalink
[Test/#248] 실행 계획 문서 생성 테스트 (#254)
Browse files Browse the repository at this point in the history
* refactor: 조회 쿼리 메서드 분리

* test: 실행 계획 생성 테스트

* feat: generateExplainDocs 테스크 구현

* feat: sql-explain-hook 구현

* test: insert/update 실행 계획 생성 테스트

* test: InsertUpdateExplainGenerator 생성 및 적용

* refactor: 캐싱 고려한 쿼리 병합 과정에서의 수정
  • Loading branch information
belljun3395 authored Jul 28, 2024
1 parent 8472cf2 commit ffaf871
Show file tree
Hide file tree
Showing 19 changed files with 974 additions and 148 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/sql-explain-hook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Sql Explain Hook

on:
pull_request:
branches: ["main"]
workflow_dispatch:

env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}


jobs:
sql-explain-hook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "temurin"

- name: Jooq Code Generation
run: |
./gradlew --info jooqCodegenAll
- name: Generate Explain Docs
run: |
./gradlew --info api-repo:generateExplainDocs
- name: Zip Explain Docs
run: |
mv ./api-repo/src/test/resources/explain ./explain
zip explain-docs.zip ./explain/*
- name: Upload Explain Docs
run: |
curl \
-F 'payload_json={"username": "GitHubAction", "content": "Check the PR here: [PR #${{ github.event.pull_request.number }}](https://github.com/YAPP-Github/24th-Web-Team-1-BE/pull/${{ github.event.pull_request.number }})"}' \
-F "[email protected]" \
$DISCORD_WEBHOOK
12 changes: 12 additions & 0 deletions api-repo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ dependencies {
/** Local Cache **/
implementation("org.ehcache:ehcache:${DependencyVersion.EHCACHE}")
implementation("org.springframework.boot:spring-boot-starter-cache")
}

tasks {
test {
useJUnitPlatform()
}

register<Test>("generateExplainDocs") {
useJUnitPlatform {
includeTags("explain")
}
}
}
122 changes: 63 additions & 59 deletions api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.few.api.repo.dao.article.record.SelectWorkBookMappedArticleRecord
import jooq.jooq_dsl.tables.ArticleIfo
import jooq.jooq_dsl.tables.ArticleMst
import jooq.jooq_dsl.tables.MappingWorkbookArticle
import org.jooq.DSLContext
import org.jooq.*
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Repository

Expand All @@ -23,56 +23,32 @@ class ArticleDao(

@Cacheable(key = "#query.articleId", cacheManager = LOCAL_CM, cacheNames = [SELECT_ARTICLE_RECORD_CACHE])
fun selectArticleRecord(query: SelectArticleRecordQuery): SelectArticleRecord? {
val articleId = query.articleId

return dslContext.select(
ArticleMst.ARTICLE_MST.ID.`as`(SelectArticleRecord::articleId.name),
ArticleMst.ARTICLE_MST.MEMBER_ID.`as`(SelectArticleRecord::writerId.name),
ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL.`as`(SelectArticleRecord::mainImageURL.name),
ArticleMst.ARTICLE_MST.TITLE.`as`(SelectArticleRecord::title.name),
ArticleMst.ARTICLE_MST.CATEGORY_CD.`as`(SelectArticleRecord::category.name),
ArticleIfo.ARTICLE_IFO.CONTENT.`as`(SelectArticleRecord::content.name),
ArticleMst.ARTICLE_MST.CREATED_AT.`as`(SelectArticleRecord::createdAt.name)
).from(ArticleMst.ARTICLE_MST)
.join(ArticleIfo.ARTICLE_IFO)
.on(ArticleMst.ARTICLE_MST.ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID))
.where(ArticleMst.ARTICLE_MST.ID.eq(articleId))
.and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull)
return selectArticleRecordQuery(query)
.fetchOneInto(SelectArticleRecord::class.java)
}

fun selectWorkBookArticleRecord(query: SelectWorkBookArticleRecordQuery): SelectWorkBookArticleRecord? {
val articleMst = ArticleMst.ARTICLE_MST
val articleIfo = ArticleIfo.ARTICLE_IFO
val mappingWorkbookArticle = MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE

val articleId = query.articleId
val workbookId = query.workbookId
fun selectArticleRecordQuery(query: SelectArticleRecordQuery) = dslContext.select(
ArticleMst.ARTICLE_MST.ID.`as`(SelectArticleRecord::articleId.name),
ArticleMst.ARTICLE_MST.MEMBER_ID.`as`(SelectArticleRecord::writerId.name),
ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL.`as`(SelectArticleRecord::mainImageURL.name),
ArticleMst.ARTICLE_MST.TITLE.`as`(SelectArticleRecord::title.name),
ArticleMst.ARTICLE_MST.CATEGORY_CD.`as`(SelectArticleRecord::category.name),
ArticleIfo.ARTICLE_IFO.CONTENT.`as`(SelectArticleRecord::content.name),
ArticleMst.ARTICLE_MST.CREATED_AT.`as`(SelectArticleRecord::createdAt.name)
).from(ArticleMst.ARTICLE_MST)
.join(ArticleIfo.ARTICLE_IFO)
.on(ArticleMst.ARTICLE_MST.ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID))
.where(ArticleMst.ARTICLE_MST.ID.eq(query.articleId))
.and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull)
.query

return dslContext.select(
articleMst.ID.`as`(SelectWorkBookArticleRecord::articleId.name),
articleMst.MEMBER_ID.`as`(SelectWorkBookArticleRecord::writerId.name),
articleMst.MAIN_IMAGE_URL.`as`(SelectWorkBookArticleRecord::mainImageURL.name),
articleMst.TITLE.`as`(SelectWorkBookArticleRecord::title.name),
articleMst.CATEGORY_CD.`as`(SelectWorkBookArticleRecord::category.name),
articleIfo.CONTENT.`as`(SelectWorkBookArticleRecord::content.name),
articleMst.CREATED_AT.`as`(SelectWorkBookArticleRecord::createdAt.name),
mappingWorkbookArticle.DAY_COL.`as`(SelectWorkBookArticleRecord::day.name)
).from(articleMst)
.join(articleIfo)
.on(articleMst.ID.eq(articleIfo.ARTICLE_MST_ID))
.join(mappingWorkbookArticle)
.on(mappingWorkbookArticle.WORKBOOK_ID.eq(workbookId))
.and(mappingWorkbookArticle.ARTICLE_ID.eq(articleMst.ID))
.where(articleMst.ID.eq(articleId))
.and(articleMst.DELETED_AT.isNull)
fun selectWorkBookArticleRecord(query: SelectWorkBookArticleRecordQuery): SelectWorkBookArticleRecord? {
return selectWorkBookArticleRecordQuery(query)
.fetchOneInto(SelectWorkBookArticleRecord::class.java)
}

fun selectWorkbookMappedArticleRecords(query: SelectWorkbookMappedArticleRecordsQuery): List<SelectWorkBookMappedArticleRecord> {
val workbookId = query.workbookId

return dslContext.select(
fun selectWorkBookArticleRecordQuery(query: SelectWorkBookArticleRecordQuery) =
dslContext.select(
ArticleMst.ARTICLE_MST.ID.`as`(SelectWorkBookArticleRecord::articleId.name),
ArticleMst.ARTICLE_MST.MEMBER_ID.`as`(SelectWorkBookArticleRecord::writerId.name),
ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL.`as`(SelectWorkBookArticleRecord::mainImageURL.name),
Expand All @@ -81,31 +57,59 @@ class ArticleDao(
ArticleIfo.ARTICLE_IFO.CONTENT.`as`(SelectWorkBookArticleRecord::content.name),
ArticleMst.ARTICLE_MST.CREATED_AT.`as`(SelectWorkBookArticleRecord::createdAt.name),
MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.`as`(SelectWorkBookArticleRecord::day.name)
)
.from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE)
.leftJoin(ArticleMst.ARTICLE_MST)
.on(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleMst.ARTICLE_MST.ID))
).from(ArticleMst.ARTICLE_MST)
.join(ArticleIfo.ARTICLE_IFO)
.on(ArticleMst.ARTICLE_MST.ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID))
.where(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(workbookId))
.join(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE)
.on(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(query.workbookId))
.and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleMst.ARTICLE_MST.ID))
.where(ArticleMst.ARTICLE_MST.ID.eq(query.articleId))
.and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull)
.query

fun selectWorkbookMappedArticleRecords(query: SelectWorkbookMappedArticleRecordsQuery): List<SelectWorkBookMappedArticleRecord> {
return selectWorkbookMappedArticleRecordsQuery(query)
.fetchInto(SelectWorkBookMappedArticleRecord::class.java)
}

fun selectWorkbookMappedArticleRecordsQuery(query: SelectWorkbookMappedArticleRecordsQuery) = dslContext.select(
ArticleMst.ARTICLE_MST.ID.`as`(SelectWorkBookArticleRecord::articleId.name),
ArticleMst.ARTICLE_MST.MEMBER_ID.`as`(SelectWorkBookArticleRecord::writerId.name),
ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL.`as`(SelectWorkBookArticleRecord::mainImageURL.name),
ArticleMst.ARTICLE_MST.TITLE.`as`(SelectWorkBookArticleRecord::title.name),
ArticleMst.ARTICLE_MST.CATEGORY_CD.`as`(SelectWorkBookArticleRecord::category.name),
ArticleIfo.ARTICLE_IFO.CONTENT.`as`(SelectWorkBookArticleRecord::content.name),
ArticleMst.ARTICLE_MST.CREATED_AT.`as`(SelectWorkBookArticleRecord::createdAt.name),
MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.`as`(SelectWorkBookArticleRecord::day.name)
)
.from(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE)
.leftJoin(ArticleMst.ARTICLE_MST)
.on(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleMst.ARTICLE_MST.ID))
.join(ArticleIfo.ARTICLE_IFO)
.on(ArticleMst.ARTICLE_MST.ID.eq(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID))
.where(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(query.workbookId))
.and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull)
.query

fun insertFullArticleRecord(command: InsertFullArticleRecordCommand): Long {
val mstId = dslContext.insertInto(ArticleMst.ARTICLE_MST)
val mstId = insertArticleMstCommand(command)
.returning(ArticleMst.ARTICLE_MST.ID)
.fetchOne()
insertArticleIfoCommand(mstId!!.getValue(ArticleMst.ARTICLE_MST.ID), command).execute()
return mstId.getValue(ArticleMst.ARTICLE_MST.ID)
}

fun insertArticleMstCommand(command: InsertFullArticleRecordCommand) =
dslContext.insertInto(ArticleMst.ARTICLE_MST)
.set(ArticleMst.ARTICLE_MST.MEMBER_ID, command.writerId)
.set(ArticleMst.ARTICLE_MST.MAIN_IMAGE_URL, command.mainImageURL.toString())
.set(ArticleMst.ARTICLE_MST.TITLE, command.title)
.set(ArticleMst.ARTICLE_MST.CATEGORY_CD, command.category)
.returning(ArticleMst.ARTICLE_MST.ID)
.fetchOne()

dslContext.insertInto(ArticleIfo.ARTICLE_IFO)
.set(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID, mstId!!.getValue(ArticleMst.ARTICLE_MST.ID))
.set(ArticleIfo.ARTICLE_IFO.CONTENT, command.content)
.execute()

return mstId.getValue(ArticleMst.ARTICLE_MST.ID)
}
fun insertArticleIfoCommand(
mstId: Long,
command: InsertFullArticleRecordCommand,
) = dslContext.insertInto(ArticleIfo.ARTICLE_IFO)
.set(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID, mstId)
.set(ArticleIfo.ARTICLE_IFO.CONTENT, command.content)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,26 @@ class ArticleViewCountDao(
) {

fun upsertArticleViewCount(query: ArticleViewCountQuery) {
upsertArticleViewCountQuery(query)
.execute()
}

fun upsertArticleViewCountQuery(query: ArticleViewCountQuery) =
dslContext.insertInto(ARTICLE_VIEW_COUNT)
.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId)
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 1)
.set(ARTICLE_VIEW_COUNT.CATEGORY_CD, query.categoryType.code)
.onDuplicateKeyUpdate()
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, ARTICLE_VIEW_COUNT.VIEW_COUNT.plus(1))
.execute()
}

fun selectArticleViewCount(command: ArticleViewCountCommand): Long? {
return dslContext.select(
ARTICLE_VIEW_COUNT.VIEW_COUNT
).from(ARTICLE_VIEW_COUNT)
.where(ARTICLE_VIEW_COUNT.ARTICLE_ID.eq(command.articleId))
.and(ARTICLE_VIEW_COUNT.DELETED_AT.isNull)
.fetchOneInto(Long::class.java)
return selectArticleViewCountQuery(command).fetchOneInto(Long::class.java)
}

fun selectArticleViewCountQuery(command: ArticleViewCountCommand) = dslContext.select(
ARTICLE_VIEW_COUNT.VIEW_COUNT
).from(ARTICLE_VIEW_COUNT)
.where(ARTICLE_VIEW_COUNT.ARTICLE_ID.eq(command.articleId))
.and(ARTICLE_VIEW_COUNT.DELETED_AT.isNull)
.query
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ class ArticleViewHisDao(
) {

fun insertArticleViewHis(command: ArticleViewHisCommand) {
insertArticleViewHisCommand(command).execute()
}

fun insertArticleViewHisCommand(command: ArticleViewHisCommand) =
dslContext.insertInto(
ArticleViewHis.ARTICLE_VIEW_HIS,
ArticleViewHis.ARTICLE_VIEW_HIS.ARTICLE_MST_ID,
ArticleViewHis.ARTICLE_VIEW_HIS.MEMBER_ID
).values(
command.articleId,
command.memberId
).execute()
}
)

fun countArticleViews(query: ArticleViewHisCountQuery): Long? {
return dslContext.selectCount()
.from(ArticleViewHis.ARTICLE_VIEW_HIS)
.where(ArticleViewHis.ARTICLE_VIEW_HIS.ARTICLE_MST_ID.eq(query.articleId))
return countArticleViewsQuery(query)
.fetchOne(0, Long::class.java)
}

fun countArticleViewsQuery(query: ArticleViewHisCountQuery) =
dslContext.selectCount()
.from(ArticleViewHis.ARTICLE_VIEW_HIS)
.where(ArticleViewHis.ARTICLE_VIEW_HIS.ARTICLE_MST_ID.eq(query.articleId)).query
}
71 changes: 38 additions & 33 deletions api-repo/src/main/kotlin/com/few/api/repo/dao/member/MemberDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ class MemberDao(

@Cacheable(key = "#query.writerId", cacheManager = LOCAL_CM, cacheNames = [SELECT_WRITER_CACHE])
fun selectWriter(query: SelectWriterQuery): WriterRecord? {
val writerId = query.writerId

return dslContext.select(
Member.MEMBER.ID.`as`(WriterRecord::writerId.name),
DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(WriterRecord::name.name),
DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.ID.eq(writerId))
.and(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code))
.and(Member.MEMBER.DELETED_AT.isNull)
return selectWriterQuery(query)
.fetchOneInto(WriterRecord::class.java)
}

fun selectWriterQuery(query: SelectWriterQuery) = dslContext.select(
Member.MEMBER.ID.`as`(WriterRecord::writerId.name),
DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(WriterRecord::name.name),
DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.ID.eq(query.writerId))
.and(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code))
.and(Member.MEMBER.DELETED_AT.isNull)

/**
* 작가 목록 조회 쿼리
* query의 writerIds에 해당하는 작가 목록을 조회한다.
Expand All @@ -48,17 +48,7 @@ class MemberDao(
val cachedIdS = cachedValues.map { it.writerId }
val notCachedIds = query.writerIds.filter { it !in cachedIdS }

val fetchedValue = dslContext.select(
Member.MEMBER.ID.`as`(WriterRecord::writerId.name),
DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name")
.`as`(WriterRecord::name.name),
DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.ID.`in`(notCachedIds))
.and(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code))
.and(Member.MEMBER.DELETED_AT.isNull)
.orderBy(Member.MEMBER.ID.asc())
val fetchedValue = selectWritersQuery(notCachedIds)
.fetchInto(WriterRecord::class.java).let {
cacheManager.addSelectWorkBookCache(it)
return@let it
Expand All @@ -67,25 +57,40 @@ class MemberDao(
return cachedValues + fetchedValue
}

fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdRecord? {
val email = query.email
fun selectWritersQuery(notCachedIds: List<Long>) = dslContext.select(
Member.MEMBER.ID.`as`(WriterRecord::writerId.name),
DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name")
.`as`(WriterRecord::name.name),
DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.ID.`in`(notCachedIds))
.and(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code))
.and(Member.MEMBER.DELETED_AT.isNull)
.orderBy(Member.MEMBER.ID.asc())

return dslContext.select(
Member.MEMBER.ID.`as`(MemberIdRecord::memberId.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.EMAIL.eq(email))
.and(Member.MEMBER.DELETED_AT.isNull)
fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdRecord? {
return selectMemberByEmailQuery(query)
.fetchOneInto(MemberIdRecord::class.java)
}

fun selectMemberByEmailQuery(query: SelectMemberByEmailQuery) = dslContext.select(
Member.MEMBER.ID.`as`(MemberIdRecord::memberId.name)
)
.from(Member.MEMBER)
.where(Member.MEMBER.EMAIL.eq(query.email))
.and(Member.MEMBER.DELETED_AT.isNull)

fun insertMember(command: InsertMemberCommand): Long? {
val result = dslContext.insertInto(Member.MEMBER)
.set(Member.MEMBER.EMAIL, command.email)
.set(Member.MEMBER.TYPE_CD, command.memberType.code)
val result = insertMemberCommand(command)
.returning(Member.MEMBER.ID)
.fetchOne()

return result?.getValue(Member.MEMBER.ID)
}

fun insertMemberCommand(command: InsertMemberCommand) =
dslContext.insertInto(Member.MEMBER)
.set(Member.MEMBER.EMAIL, command.email)
.set(Member.MEMBER.TYPE_CD, command.memberType.code)
}
Loading

0 comments on commit ffaf871

Please sign in to comment.