-
Notifications
You must be signed in to change notification settings - Fork 1
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
[Feat/#228] 아티클 목록 조회(무한스크롤) API 추가 #259
Changes from 27 commits
cadc248
02ea8a7
451d7f3
48657e6
c591f8a
a4ca18c
2bfb33e
e64b341
d5b51bc
74a9829
053007d
54d8186
2510219
d9027cd
0213ead
c2d5fde
29743a0
394620b
3329b64
2c10688
04d5f6e
787c6b6
7e29ca9
6546c36
8539a26
c7c1d79
a0ec2a1
d22c0fa
50c8c48
5c9e316
e546788
5e05b09
a657727
0ddb026
d97fe44
0bc1aa2
7ffc0f7
fd3e7b7
d4279e2
b1255ee
58265dc
af42b1a
c657c0a
365c02d
e3dd2b9
3749da9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package com.few.api.repo.dao.article | ||
|
||
import jooq.jooq_dsl.tables.ArticleMst.ARTICLE_MST | ||
import jooq.jooq_dsl.tables.MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE | ||
import jooq.jooq_dsl.tables.Member.MEMBER | ||
import jooq.jooq_dsl.tables.Workbook.WORKBOOK | ||
import com.few.api.repo.dao.article.record.ArticleMainCardRecord | ||
import com.few.api.repo.dao.article.support.CommonJsonMapper | ||
import jooq.jooq_dsl.tables.ArticleMainCard.ARTICLE_MAIN_CARD | ||
import org.jooq.* | ||
import org.jooq.impl.DSL.* | ||
import org.springframework.stereotype.Repository | ||
import java.time.LocalDateTime | ||
|
||
@Repository | ||
class ArticleMainCardDao( | ||
private val dslContext: DSLContext, | ||
private val commonJsonMapper: CommonJsonMapper, | ||
) { | ||
|
||
fun selectArticleMainCardsRecord(articleIds: Set<Long>): Set<ArticleMainCardRecord> { | ||
return selectArticleMainCardsRecordQuery(articleIds) | ||
.fetchInto(ArticleMainCardRecord::class.java) | ||
.toSet() | ||
} | ||
|
||
private fun selectArticleMainCardsRecordQuery(articleIds: Set<Long>) = dslContext.select( | ||
ARTICLE_MAIN_CARD.ID.`as`(ArticleMainCardRecord::articleId.name), | ||
ARTICLE_MAIN_CARD.TITLE.`as`(ArticleMainCardRecord::articleTitle.name), | ||
ARTICLE_MAIN_CARD.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name), | ||
ARTICLE_MAIN_CARD.CATEGORY_CD.`as`(ArticleMainCardRecord::categoryCd.name), | ||
ARTICLE_MAIN_CARD.CREATED_AT.`as`(ArticleMainCardRecord::createdAt.name), | ||
ARTICLE_MAIN_CARD.WRITER_ID.`as`(ArticleMainCardRecord::writerId.name), | ||
ARTICLE_MAIN_CARD.WRITER_EMAIL.`as`(ArticleMainCardRecord::writerEmail.name), | ||
jsonGetAttributeAsText(ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "name").`as`(ArticleMainCardRecord::writerName.name), | ||
jsonGetAttribute(ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerImgUrl.name) | ||
).from(ARTICLE_MAIN_CARD) | ||
.where(ARTICLE_MAIN_CARD.ID.`in`(articleIds)) | ||
.query | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존에 조회가 되어서 |
||
|
||
fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbook(articleIds: Set<Long>): Set<ArticleMainCardRecord> { | ||
return selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds) | ||
.fetchInto(ArticleMainCardRecord::class.java) | ||
.toSet() | ||
} | ||
|
||
private fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds: Set<Long>): | ||
SelectQuery<Record9<Long, String, String, Byte, LocalDateTime, Long, String, String, JSON>> { | ||
val a = ARTICLE_MST.`as`("a") | ||
val m = MEMBER.`as`("m") | ||
val mwa = MAPPING_WORKBOOK_ARTICLE.`as`("mwa") | ||
val w = WORKBOOK.`as`("w") | ||
|
||
return dslContext.select( | ||
a.ID.`as`(ArticleMainCardRecord::articleId.name), | ||
a.TITLE.`as`(ArticleMainCardRecord::articleTitle.name), | ||
a.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name), | ||
a.CATEGORY_CD.`as`(ArticleMainCardRecord::categoryCd.name), | ||
a.CREATED_AT.`as`(ArticleMainCardRecord::createdAt.name), | ||
m.ID.`as`(ArticleMainCardRecord::writerId.name), | ||
m.EMAIL.`as`(ArticleMainCardRecord::writerEmail.name), | ||
jsonGetAttributeAsText(m.DESCRIPTION, "name").`as`(ArticleMainCardRecord::writerName.name), | ||
jsonGetAttribute(m.DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerImgUrl.name) | ||
) | ||
.from(a) | ||
.join(m).on(a.MEMBER_ID.eq(m.ID)).and(a.DELETED_AT.isNull).and(m.DELETED_AT.isNull) | ||
.leftJoin(mwa).on(a.ID.eq(mwa.ARTICLE_ID)).and(mwa.DELETED_AT.isNull) | ||
.leftJoin(w).on(mwa.WORKBOOK_ID.eq(w.ID)).and(w.DELETED_AT.isNull) | ||
.where(a.ID.`in`(articleIds)) | ||
.groupBy(a.ID) | ||
.query | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 조인해서 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RAW SQLSELECT a.id AS article_id,
a.title AS article_title,
a.main_image_url,
a.category_cd,
a.created_at AS article_created_at,
m.id AS writer_id,
m.email AS writer_email,
m.description AS writer_description,
json_extract(m.description, '$.url') AS writer_description_url,
json_extract(m.description, '$.name') AS writer_dname,
JSON_ARRAYAGG(
JSON_OBJECT(
'title', IFNULL(w.title, NULL),
'id', IFNULL(w.id, NULL)
)
) AS workbooks
FROM ARTICLE_MST a
JOIN MEMBER m ON a.member_id = m.id
AND a.deleted_at IS NULL
AND m.deleted_at IS NULL
LEFT JOIN MAPPING_WORKBOOK_ARTICLE mwa
ON a.id = mwa.article_id
AND mwa.deleted_at IS NULL
LEFT JOIN WORKBOOK w
ON mwa.workbook_id = w.id
AND w.deleted_at IS NULL
where a.id in (1, 2, 3)
GROUP BY a.id; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 쿼리에서 해당 아티클이 속한 워크북 리스트를 하나의 컬럼으로 가져올라다 보니 json으로 만들어서 가져오려고 JSON_ARRAYAGG(
JSON_OBJECT(
'title', IFNULL(w.title, NULL),
'id', IFNULL(w.id, NULL)
)
) AS workbooks 이게 들어간건데 jooq에서 어떤 노력을 해도 계속 에러나서 빠지게 됨... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 결국 최종 쿼리는 아래와 같이 workbook 관련 2개 테이블이 빠지게 됨 SELECT a.id AS article_id,
a.title AS article_title,
a.main_image_url,
a.category_cd,
a.created_at AS article_created_at,
m.id AS writer_id,
m.email AS writer_email,
m.description AS writer_description,
json_extract(m.description, '$.url') AS writer_description_url,
json_extract(m.description, '$.name') AS writer_dname
FROM ARTICLE_MST a
JOIN MEMBER m ON a.member_id = m.id
AND a.deleted_at IS NULL
AND m.deleted_at IS NULL
where a.id in (1, 2, 3)
GROUP BY a.id; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요 사진의 로그로 allowMultiQueries 문제라는거 감잡고 https://blog.jooq.org/mysqls-allowmultiqueries-flag-with-jdbc-and-jooq/ jdbc url 끝에 &allowMultiQueries=true 추가하고 jsonArrayAgg(
jsonObject(
key("id").value(Workbook.WORKBOOK.ID),
key("title").value(Workbook.WORKBOOK.TITLE),
)
).`as`(ArticleMainCardRecord::temp.name), // 우선 temp로 실험함 위와 같이 수정하니까 본래 쿼리와 같이 동작합니다. jsonArrayAgg 문서: https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/json-functions/json-object-function/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 본래 쿼리가 생각하신 개발 방향에 맞으면 수정하면 될 것 가타요!!!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 종준님.. ㄹㅇ 쵝오다 이걸 바로 캐치해버리시네 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀해주신대로 해보고 말씀드릴게요 |
||
} | ||
|
||
fun insertArticleMainCardsBulk(commands: Set<ArticleMainCardRecord>) { | ||
dslContext.batch( | ||
insertArticleMainCardsBulkQuery(commands) | ||
).execute() | ||
} | ||
|
||
fun insertArticleMainCardsBulkQuery(commands: Set<ArticleMainCardRecord>): | ||
InsertValuesStep8<jooq.jooq_dsl.tables.records.ArticleMainCardRecord, Long, String, String, Byte, LocalDateTime, Long, String, JSON> { | ||
val insertStep = dslContext.insertInto( | ||
ARTICLE_MAIN_CARD, | ||
ARTICLE_MAIN_CARD.ID, | ||
ARTICLE_MAIN_CARD.TITLE, | ||
ARTICLE_MAIN_CARD.MAIN_IMAGE_URL, | ||
ARTICLE_MAIN_CARD.CATEGORY_CD, | ||
ARTICLE_MAIN_CARD.CREATED_AT, | ||
ARTICLE_MAIN_CARD.WRITER_ID, | ||
ARTICLE_MAIN_CARD.WRITER_EMAIL, | ||
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION | ||
) | ||
|
||
for (command in commands) { | ||
insertStep.values( | ||
command.articleId, | ||
command.articleTitle, | ||
command.mainImageUrl.toString(), | ||
command.categoryCd, | ||
command.createdAt, | ||
command.writerId, | ||
command.writerEmail, | ||
JSON.valueOf( | ||
commonJsonMapper.toJsonStr( | ||
mapOf( | ||
"name" to command.writerName, | ||
"url" to command.writerImgUrl | ||
) | ||
) | ||
) | ||
) | ||
} | ||
|
||
return insertStep | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,14 @@ package com.few.api.repo.dao.article | |
|
||
import com.few.api.repo.dao.article.command.ArticleViewCountCommand | ||
import com.few.api.repo.dao.article.query.ArticleViewCountQuery | ||
import com.few.api.repo.dao.article.query.SelectArticlesOrderByViewsQuery | ||
import com.few.api.repo.dao.article.query.SelectRankByViewsQuery | ||
import com.few.api.repo.dao.article.record.SelectArticleViewsRecord | ||
import jooq.jooq_dsl.tables.ArticleViewCount.ARTICLE_VIEW_COUNT | ||
import org.jooq.DSLContext | ||
import org.jooq.Record2 | ||
import org.jooq.SelectQuery | ||
import org.jooq.impl.DSL.* | ||
import org.springframework.stereotype.Repository | ||
|
||
@Repository | ||
|
@@ -21,6 +27,13 @@ class ArticleViewCountDao( | |
.execute() | ||
} | ||
|
||
fun insertArticleViewCountToZero(query: ArticleViewCountQuery) = insertArticleViewCountToZeroQuery(query).execute() | ||
|
||
fun insertArticleViewCountToZeroQuery(query: ArticleViewCountQuery) = dslContext.insertInto(ARTICLE_VIEW_COUNT) | ||
.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId) | ||
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 0) | ||
.set(ARTICLE_VIEW_COUNT.CATEGORY_CD, query.categoryType.code) | ||
|
||
fun selectArticleViewCount(command: ArticleViewCountCommand): Long? { | ||
return dslContext.select( | ||
ARTICLE_VIEW_COUNT.VIEW_COUNT | ||
|
@@ -29,4 +42,47 @@ class ArticleViewCountDao( | |
.and(ARTICLE_VIEW_COUNT.DELETED_AT.isNull) | ||
.fetchOneInto(Long::class.java) | ||
} | ||
|
||
fun selectRankByViews(query: SelectRankByViewsQuery): Long? { | ||
return selectRankByViewsQuery(query) | ||
.fetchOneInto(Long::class.java) | ||
} | ||
|
||
fun selectRankByViewsQuery(query: SelectRankByViewsQuery) = dslContext | ||
.select(field("offset")) | ||
.from( | ||
dslContext.select( | ||
ARTICLE_VIEW_COUNT.ARTICLE_ID, | ||
rowNumber().over(orderBy(ARTICLE_VIEW_COUNT.VIEW_COUNT.desc())).`as`("offset") | ||
).from(ARTICLE_VIEW_COUNT.`as`("RankedRows")) | ||
) | ||
.where(field("RankedRows.article_id").eq(query.articleId)) | ||
.query | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a657727 에서 쿼리 문법 수정 |
||
|
||
fun selectArticlesOrderByViews(query: SelectArticlesOrderByViewsQuery): Set<SelectArticleViewsRecord> { | ||
return selectArticlesOrderByViewsQuery(query) | ||
.fetchInto(SelectArticleViewsRecord::class.java) | ||
.toSet() | ||
} | ||
|
||
fun selectArticlesOrderByViewsQuery(query: SelectArticlesOrderByViewsQuery): SelectQuery<Record2<Long, Long>> { | ||
val articleViewCountOffsetTb = select() | ||
.from(ARTICLE_VIEW_COUNT) | ||
.where(ARTICLE_VIEW_COUNT.DELETED_AT.isNull) | ||
.orderBy(ARTICLE_VIEW_COUNT.VIEW_COUNT.desc()) | ||
.limit(query.offset, Long.MAX_VALUE) | ||
.asTable("article_view_count_offset_tb") | ||
|
||
val sql = dslContext.select( | ||
articleViewCountOffsetTb.field(ARTICLE_VIEW_COUNT.ARTICLE_ID.`as`(SelectArticleViewsRecord::articleId.name)), | ||
articleViewCountOffsetTb.field(ARTICLE_VIEW_COUNT.VIEW_COUNT.`as`(SelectArticleViewsRecord::views.name)) | ||
) | ||
.from(articleViewCountOffsetTb) | ||
|
||
if (query.category != null) { | ||
sql.where(field("article_view_count_offset_tb.category_cd").eq(query.category.code)) | ||
} | ||
|
||
return sql.limit(10).query | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
select *
from ARTICLE_VIEW_COUNT
order by view_count desc
limit 20, 10; 위 처럼 쿼리가 작성되면 offset이 20이고 맨 앞20개 row를 스킵한 뒤 10개를 읽는 쿼리가 됩니다. (참고로 테이블 끝까지 읽기 위해 최종 쿼리 예시select *
from (select *
from ARTICLE_VIEW_COUNT
order by view_count desc
limit 20, 9999999) as article_view_count_offset_tb
where article_view_count_offset_tb.category_cd = 10
limit 10;
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.few.api.repo.dao.article.query | ||
|
||
import com.few.data.common.code.CategoryType | ||
|
||
data class SelectArticlesOrderByViewsQuery( | ||
val offset: Long, | ||
val category: CategoryType?, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.few.api.repo.dao.article.query | ||
|
||
data class SelectRankByViewsQuery( | ||
val articleId: Long, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.few.api.repo.dao.article.record | ||
|
||
import java.net.URL | ||
import java.time.LocalDateTime | ||
|
||
data class ArticleMainCardRecord( | ||
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, | ||
) { | ||
var content: String = "" | ||
set(value) { | ||
field = value | ||
} | ||
|
||
var views: Long = 0L | ||
set(value) { | ||
field = value | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.few.api.repo.dao.article.record | ||
|
||
data class SelectArticleContentsRecord( | ||
val articleId: Long, | ||
val content: String, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.few.api.repo.dao.article.record | ||
|
||
data class SelectArticleViewsRecord( | ||
val articleId: Long, | ||
val views: Long, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.few.api.repo.dao.article.support | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import org.springframework.stereotype.Component | ||
|
||
@Component | ||
class CommonJsonMapper( // TODO: common 성 패키지 위치로 이동 | ||
private val objectMapper: ObjectMapper, | ||
) { | ||
fun toJsonStr(map: Map<String, Any>): String { | ||
return objectMapper.writeValueAsString(map) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아티클 컨텐츠만 뽑아오는건데 생각해보니까 이건 매번 스크롤마다 10개 아티클에 대해서 모두 수행되기 때문에 disk I/O 부하가 다소 있을거 같습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그런데 어딘가에서는 저장되고 io로 불러와야하니까 선택의 문제가 아닐까요?
우선 db에서 불러오고 쫌 더 좋은 방법 고민해봐요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
글고 생각해보니까 요거는 정책으로 풀 수 있는 문제가 아닌가 합니다.
썸넬에서는 최대 5줄 보여준다 하면 우리가 미리 잘라서 넣어둘 수 있으니까요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞아요 일단 메모리에 저장하면DB에서 꺼내는것보단 빠르긴 함. 말씀대로 5줄로 쭐이면 좋긴할듯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
잴 많이 호출되는 api에서 잴 많이 disk I/O 부하를 주는 셈. 그렇다고 이걸 로컬 캐시에 넣는게 맞을까요?