Skip to content

Commit

Permalink
[Fix/#263] 아티클 목록 조회 API 개선: insert 쿼리 삭제 - 트랜잭션 간 불필요한 경합 문제 해결 (#292)
Browse files Browse the repository at this point in the history
* refactor: workbook record, dto non-nullable 필드로 수정

* fix: article main card의 workbook 컬럼 default 타입 변경 (JSON_OBJECT -> JSON_ARRAY)

* fix: article_main_card Insert(workbook제외), Update(workbook만) 쿼리 추가

* feat: add new funcation fromName in CategoryType

* feat: member 조회시 작가일 경우 name도 가져오도록 추가

* feat: 아티클 신규 저장 시, article_main_card(workbook 제외) 저장하도록 반영

* refactor: 필드명 수정

* feat: workbook-article 연결시 article_main_card의 workbook 컬럼 업데이트 하도록 변경

* fix: remove V1.00.0.16__alter_article_main_card_table.sql

* chore: 테스트용 임시 주석 처리

* refactor: 아티클 카테고리 조회 부분 리펙토링

* fix: article_main_card 테이블의 workbooks 컬럼의 디폴트 값({}) 대응

* chore: 미사용중인 주석 삭제

* feat: 기존 article_main_card 테이블에 존재하던 데이터도 지원되도록 반영

* refactor: ReadArticlesUseCase 리펙토링

* refactor: record class 명 수정(MemberIdAndNameRecord)
  • Loading branch information
hun-ca authored Aug 4, 2024
1 parent 2ea547e commit b273884
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.few.api.repo.dao.article

import jooq.jooq_dsl.tables.ArticleMst.ARTICLE_MST
import jooq.jooq_dsl.tables.Member.MEMBER
import com.few.api.repo.dao.article.command.ArticleMainCardExcludeWorkbookCommand
import com.few.api.repo.dao.article.command.UpdateArticleMainCardWorkbookCommand
import com.few.api.repo.dao.article.record.ArticleMainCardRecord
import com.few.api.repo.dao.article.support.CommonJsonMapper
import com.few.api.repo.dao.article.support.ArticleMainCardMapper
import jooq.jooq_dsl.tables.ArticleMainCard.ARTICLE_MAIN_CARD
import jooq.jooq_dsl.tables.MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE
import jooq.jooq_dsl.tables.Workbook.WORKBOOK
import org.jooq.*
import org.jooq.impl.DSL.*
import org.springframework.stereotype.Repository
Expand Down Expand Up @@ -43,47 +41,14 @@ class ArticleMainCardDao(
.where(ARTICLE_MAIN_CARD.ID.`in`(articleIds))
.query

fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbook(articleIds: Set<Long>): Set<ArticleMainCardRecord> {
return selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds)
.fetch(articleMainCardMapper)
.toSet()
}

private fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds: Set<Long>) =
dslContext.select(
ARTICLE_MST.ID.`as`(ArticleMainCardRecord::articleId.name),
ARTICLE_MST.TITLE.`as`(ArticleMainCardRecord::articleTitle.name),
ARTICLE_MST.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name),
ARTICLE_MST.CATEGORY_CD.`as`(ArticleMainCardRecord::categoryCd.name),
ARTICLE_MST.CREATED_AT.`as`(ArticleMainCardRecord::createdAt.name),
MEMBER.ID.`as`(ArticleMainCardRecord::writerId.name),
MEMBER.EMAIL.`as`(ArticleMainCardRecord::writerEmail.name),
jsonGetAttributeAsText(MEMBER.DESCRIPTION, "name").`as`(ArticleMainCardRecord::writerName.name),
jsonGetAttribute(MEMBER.DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerImgUrl.name),
jsonArrayAgg(
jsonObject(
key("id").value(WORKBOOK.ID),
key("title").value(WORKBOOK.TITLE)
)
).`as`(ArticleMainCardRecord::workbooks.name)
)
.from(ARTICLE_MST)
.join(MEMBER).on(ARTICLE_MST.MEMBER_ID.eq(MEMBER.ID)).and(ARTICLE_MST.DELETED_AT.isNull)
.and(MEMBER.DELETED_AT.isNull)
.leftJoin(MAPPING_WORKBOOK_ARTICLE).on(ARTICLE_MST.ID.eq(MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID)).and(MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull)
.leftJoin(WORKBOOK).on(MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(WORKBOOK.ID)).and(WORKBOOK.DELETED_AT.isNull)
.where(ARTICLE_MST.ID.`in`(articleIds))
.groupBy(ARTICLE_MST.ID)
.query
/**
* NOTE - The query performed in this function do not save the workbook.
*/
fun insertArticleMainCard(command: ArticleMainCardExcludeWorkbookCommand) =
insertArticleMainCardQuery(command).execute()

fun insertArticleMainCardsBulk(commands: Set<ArticleMainCardRecord>) {
dslContext.batch(
commands.map { command -> insertArticleMainCardsBulkQuery(command) }
).execute()
}

fun insertArticleMainCardsBulkQuery(command: ArticleMainCardRecord) =
dslContext.insertInto(
fun insertArticleMainCardQuery(command: ArticleMainCardExcludeWorkbookCommand) = dslContext
.insertInto(
ARTICLE_MAIN_CARD,
ARTICLE_MAIN_CARD.ID,
ARTICLE_MAIN_CARD.TITLE,
Expand All @@ -92,8 +57,7 @@ class ArticleMainCardDao(
ARTICLE_MAIN_CARD.CREATED_AT,
ARTICLE_MAIN_CARD.WRITER_ID,
ARTICLE_MAIN_CARD.WRITER_EMAIL,
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION,
ARTICLE_MAIN_CARD.WORKBOOKS
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION
).values(
command.articleId,
command.articleTitle,
Expand All @@ -109,7 +73,14 @@ class ArticleMainCardDao(
"url" to command.writerImgUrl
)
)
),
JSON.valueOf(articleMainCardMapper.toJsonStr(command.workbooks))
)
)

fun updateArticleMainCardSetWorkbook(command: UpdateArticleMainCardWorkbookCommand) =
updateArticleMainCardSetWorkbookQuery(command).execute()

fun updateArticleMainCardSetWorkbookQuery(command: UpdateArticleMainCardWorkbookCommand) = dslContext
.update(ARTICLE_MAIN_CARD)
.set(ARTICLE_MAIN_CARD.WORKBOOKS, JSON.valueOf(articleMainCardMapper.toJsonStr(command.workbooks)))
.where(ARTICLE_MAIN_CARD.ID.eq(command.articleId))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.few.api.repo.dao.article.command

import java.net.URL
import java.time.LocalDateTime

data class ArticleMainCardExcludeWorkbookCommand(
val articleId: Long,
val articleTitle: String,
val mainImageUrl: URL,
val categoryCd: Byte,
val createdAt: LocalDateTime,
val writerId: Long,
val writerEmail: String,
val writerName: String,
val writerImgUrl: URL,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.few.api.repo.dao.article.command

data class UpdateArticleMainCardWorkbookCommand(
val articleId: Long,
val workbooks: List<WorkbookCommand>,
)

data class WorkbookCommand(
val id: Long,
val title: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.few.api.repo.dao.article.support

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.few.api.repo.dao.article.command.WorkbookCommand
import com.few.api.repo.dao.article.record.ArticleMainCardRecord
import com.few.api.repo.dao.article.record.WorkbookRecord
import org.jooq.JSON
Expand All @@ -15,22 +16,28 @@ import java.time.LocalDateTime
class ArticleMainCardMapper(
private val objectMapper: ObjectMapper,
) : RecordMapper<Record, ArticleMainCardRecord> {
override fun map(record: Record): ArticleMainCardRecord {
val workbooksJsonArrayStr: String = record.get(ArticleMainCardRecord::workbooks.name, JSON::class.java).data()
override fun map(record: Record) = ArticleMainCardRecord(
articleId = record.get(ArticleMainCardRecord::articleId.name, Long::class.java),
articleTitle = record.get(ArticleMainCardRecord::articleTitle.name, String::class.java),
mainImageUrl = record.get(ArticleMainCardRecord::mainImageUrl.name, URL::class.java),
categoryCd = record.get(ArticleMainCardRecord::categoryCd.name, Byte::class.java),
createdAt = record.get(ArticleMainCardRecord::createdAt.name, LocalDateTime::class.java),
writerId = record.get(ArticleMainCardRecord::writerId.name, Long::class.java),
writerEmail = record.get(ArticleMainCardRecord::writerEmail.name, String::class.java),
writerName = record.get(ArticleMainCardRecord::writerName.name, String::class.java),
writerImgUrl = record.get(ArticleMainCardRecord::writerImgUrl.name, URL::class.java),
workbooks = record.get(ArticleMainCardRecord::workbooks.name, JSON::class.java)?.data()?.let {
if ("{}".equals(it)) {
emptyList()
} else {
val workbookRecords = objectMapper.readValue<List<WorkbookRecord>>(it)
workbookRecords.filter { w -> w.id != null && w.title != null }
.toList()
}
} ?: run {
emptyList()
}
)

return ArticleMainCardRecord(
articleId = record.get(ArticleMainCardRecord::articleId.name, Long::class.java),
articleTitle = record.get(ArticleMainCardRecord::articleTitle.name, String::class.java),
mainImageUrl = record.get(ArticleMainCardRecord::mainImageUrl.name, URL::class.java),
categoryCd = record.get(ArticleMainCardRecord::categoryCd.name, Byte::class.java),
createdAt = record.get(ArticleMainCardRecord::createdAt.name, LocalDateTime::class.java),
writerId = record.get(ArticleMainCardRecord::writerId.name, Long::class.java),
writerEmail = record.get(ArticleMainCardRecord::writerEmail.name, String::class.java),
writerName = record.get(ArticleMainCardRecord::writerName.name, String::class.java),
writerImgUrl = record.get(ArticleMainCardRecord::writerImgUrl.name, URL::class.java),
workbooks = objectMapper.readValue<List<WorkbookRecord>>(workbooksJsonArrayStr)
)
}

fun toJsonStr(workbooks: List<WorkbookRecord>) = objectMapper.writeValueAsString(workbooks)
fun toJsonStr(workbooks: List<WorkbookCommand>) = objectMapper.writeValueAsString(workbooks)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery
import com.few.api.repo.dao.member.query.SelectWriterQuery
import com.few.api.repo.dao.member.query.SelectWritersQuery
import com.few.api.repo.dao.member.record.MemberIdAndIsDeletedRecord
import com.few.api.repo.dao.member.record.MemberIdRecord
import com.few.api.repo.dao.member.record.MemberIdAndNameRecord
import com.few.api.repo.dao.member.record.MemberEmailAndTypeRecord
import com.few.api.repo.dao.member.record.WriterRecord
import com.few.api.repo.dao.member.record.WriterRecordMappedWorkbook
Expand All @@ -22,6 +22,7 @@ import jooq.jooq_dsl.tables.MappingWorkbookArticle
import jooq.jooq_dsl.tables.Member
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.jooq.impl.DSL.jsonGetAttributeAsText
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
Expand Down Expand Up @@ -124,13 +125,14 @@ class MemberDao(
.where(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code))
.and(Member.MEMBER.DELETED_AT.isNull)

fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdRecord? {
fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdAndNameRecord? {
return selectMemberByEmailQuery(query)
.fetchOneInto(MemberIdRecord::class.java)
.fetchOneInto(MemberIdAndNameRecord::class.java)
}

fun selectMemberByEmailQuery(query: SelectMemberByEmailQuery) = dslContext.select(
Member.MEMBER.ID.`as`(MemberIdRecord::memberId.name)
Member.MEMBER.ID.`as`(MemberIdAndNameRecord::memberId.name),
jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(MemberIdAndNameRecord::writerName.name) // writer only(nullable)
)
.from(Member.MEMBER)
.where(Member.MEMBER.EMAIL.eq(query.email))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.few.api.repo.dao.member.record

data class MemberIdRecord(
data class MemberIdAndNameRecord(
val memberId: Long,
val writerName: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.few.api.domain.admin.document.service

import com.few.api.domain.admin.document.service.dto.AppendWorkbookToArticleMainCardInDto
import com.few.api.domain.admin.document.service.dto.InitializeArticleMainCardInDto
import com.few.api.exception.common.NotFoundException
import com.few.api.repo.dao.article.ArticleMainCardDao
import com.few.api.repo.dao.article.command.ArticleMainCardExcludeWorkbookCommand
import com.few.api.repo.dao.article.command.UpdateArticleMainCardWorkbookCommand
import com.few.api.repo.dao.article.command.WorkbookCommand
import com.few.api.repo.dao.article.record.ArticleMainCardRecord
import com.few.api.repo.dao.workbook.WorkbookDao
import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery
import org.springframework.stereotype.Service

@Service
class ArticleMainCardService(
val articleMainCardDao: ArticleMainCardDao,
val workbookDao: WorkbookDao,
) {
fun initialize(inDto: InitializeArticleMainCardInDto) {
articleMainCardDao.insertArticleMainCard(
ArticleMainCardExcludeWorkbookCommand(
articleId = inDto.articleId,
articleTitle = inDto.articleTitle,
mainImageUrl = inDto.mainImageUrl,
categoryCd = inDto.categoryCd,
createdAt = inDto.createdAt,
writerId = inDto.writerId,
writerEmail = inDto.writerEmail,
writerName = inDto.writerName,
writerImgUrl = inDto.writerImgUrl
)
)
}

fun appendWorkbook(inDto: AppendWorkbookToArticleMainCardInDto) {
val workbookRecord = workbookDao.selectWorkBook(SelectWorkBookRecordQuery(inDto.workbookId))
?: throw NotFoundException("workbook.notfound.id")

val toBeAddedWorkbook = WorkbookCommand(inDto.workbookId, workbookRecord.title)

val articleMainCardRecord: ArticleMainCardRecord =
articleMainCardDao.selectArticleMainCardsRecord(setOf(inDto.articleId))
.ifEmpty { throw NotFoundException("articlemaincard.notfound.id") }
.first()

val workbookCommands =
articleMainCardRecord.workbooks.map { WorkbookCommand(it.id!!, it.title!!) }.toMutableList()
workbookCommands.add(toBeAddedWorkbook)

articleMainCardDao.updateArticleMainCardSetWorkbook(
UpdateArticleMainCardWorkbookCommand(
articleId = inDto.articleId,
workbooks = workbookCommands
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.domain.admin.document.service.dto

data class AppendWorkbookToArticleMainCardInDto(
val articleId: Long,
val workbookId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.few.api.domain.admin.document.service.dto

import java.net.URL
import java.time.LocalDateTime

data class InitializeArticleMainCardInDto(
val articleId: Long,
val articleTitle: String,
val mainImageUrl: URL,
val categoryCd: Byte,
val createdAt: LocalDateTime,
val writerId: Long,
val writerEmail: String,
val writerName: String,
val writerImgUrl: URL,
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.few.api.domain.admin.document.usecase

import com.few.api.domain.admin.document.service.ArticleMainCardService
import com.few.api.domain.admin.document.usecase.dto.AddArticleUseCaseIn
import com.few.api.domain.admin.document.usecase.dto.AddArticleUseCaseOut
import com.few.api.domain.admin.document.service.GetUrlService
import com.few.api.domain.admin.document.service.dto.GetUrlInDto
import com.few.api.domain.admin.document.service.dto.InitializeArticleMainCardInDto
import com.few.api.domain.admin.document.utils.ObjectPathGenerator
import com.few.api.exception.common.ExternalIntegrationException
import com.few.api.exception.common.NotFoundException
Expand All @@ -25,6 +27,8 @@ import com.few.storage.document.service.PutDocumentService
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.io.File
import java.net.URL
import java.time.LocalDateTime
import java.util.*

@Component
Expand All @@ -37,11 +41,12 @@ class AddArticleUseCase(
private val convertDocumentService: ConvertDocumentService,
private val putDocumentService: PutDocumentService,
private val getUrlService: GetUrlService,
private val articleMainCardService: ArticleMainCardService,
) {
@Transactional
fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut {
/** select writerId */
val writerId = SelectMemberByEmailQuery(useCaseIn.writerEmail).let {
val writerIdRecord = SelectMemberByEmailQuery(useCaseIn.writerEmail).let {
memberDao.selectMemberByEmail(it)
} ?: throw NotFoundException("member.notfound.id")

Expand Down Expand Up @@ -91,12 +96,15 @@ class AddArticleUseCase(
}
}

val category = CategoryType.fromName(useCaseIn.category)
?: throw NotFoundException("article.invalid.category")

/** insert article */
val articleMstId = InsertFullArticleRecordCommand(
writerId = writerId.memberId,
writerId = writerIdRecord.memberId,
mainImageURL = useCaseIn.articleImageUrl,
title = useCaseIn.title,
category = CategoryType.convertToCode(useCaseIn.category),
category = category.code,
content = htmlSource
).let { articleDao.insertFullArticleRecord(it) }

Expand All @@ -123,10 +131,23 @@ class AddArticleUseCase(

ArticleViewCountQuery(
articleMstId,
CategoryType.fromCode(CategoryType.convertToCode(useCaseIn.category))
?: throw NotFoundException("article.invalid.category")
category
).let { articleViewCountDao.insertArticleViewCountToZero(it) }

articleMainCardService.initialize(
InitializeArticleMainCardInDto(
articleId = articleMstId,
articleTitle = useCaseIn.title,
mainImageUrl = useCaseIn.articleImageUrl,
categoryCd = category.code,
createdAt = LocalDateTime.now(), // TODO: DB insert 시점으로 변경
writerId = writerIdRecord.memberId,
writerEmail = useCaseIn.writerEmail,
writerName = writerIdRecord.writerName ?: throw NotFoundException("article.writer.name"),
writerImgUrl = URL("https://github.com/user-attachments/assets/528a6531-2cba-4efc-b8df-64a083d38be8") //TODO: 작가 이미지로 변환
)
)

return AddArticleUseCaseOut(articleMstId)
}
}
Loading

0 comments on commit b273884

Please sign in to comment.