feat: ページングが動くように

This commit is contained in:
usbharu 2024-08-17 18:21:32 +09:00
parent d53cdca9e5
commit 11e2eb1e10
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
10 changed files with 111 additions and 16 deletions

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Document
@ -61,6 +62,22 @@ class MongoInternalTimelineObjectRepository(
springDataMongoTimelineObjectRepository.deleteByTimelineId(timelineId.value) springDataMongoTimelineObjectRepository.deleteByTimelineId(timelineId.value)
} }
override suspend fun findByTimelineIdAndPostIdGT(timelineId: TimelineId, postId: PostId): TimelineObject? {
return springDataMongoTimelineObjectRepository.findFirstByTimelineIdAndPostIdGreaterThanOrderByIdAsc(
timelineId.value,
postId.id
)
?.toTimelineObject()
}
override suspend fun findByTimelineIdAndPostIdLT(timelineId: TimelineId, postId: PostId): TimelineObject? {
return springDataMongoTimelineObjectRepository.findFirstByTimelineIdAndPostIdLessThanOrderByIdDesc(
timelineId.value,
postId.id
)
?.toTimelineObject()
}
override suspend fun findByTimelineId( override suspend fun findByTimelineId(
timelineId: TimelineId, timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption?, internalTimelineObjectOption: InternalTimelineObjectOption?,
@ -70,12 +87,12 @@ class MongoInternalTimelineObjectRepository(
if (page?.minId != null) { if (page?.minId != null) {
query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt")) query.with(Sort.by(Sort.Direction.ASC, "postCreatedAt"))
page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) } page.minId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) } page.maxId?.let { query.addCriteria(Criteria.where("postId").lt(it)) }
} else { } else {
query.with(Sort.by(Sort.Direction.DESC, "postCreatedAt")) query.with(Sort.by(Sort.Direction.DESC, "postCreatedAt"))
page?.sinceId?.let { query.addCriteria(Criteria.where("id").gt(it)) } page?.sinceId?.let { query.addCriteria(Criteria.where("postId").gt(it)) }
page?.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) } page?.maxId?.let { query.addCriteria(Criteria.where("postId").lt(it)) }
} }
page?.limit?.let { query.limit(it) } page?.limit?.let { query.limit(it) }
@ -83,16 +100,23 @@ class MongoInternalTimelineObjectRepository(
val timelineObjects = val timelineObjects =
mongoTemplate.find(query, SpringDataMongoTimelineObject::class.java).map { it.toTimelineObject() } mongoTemplate.find(query, SpringDataMongoTimelineObject::class.java).map { it.toTimelineObject() }
val objectList = if (page?.minId != null) {
timelineObjects.reversed()
} else {
timelineObjects
}
return PaginationList( return PaginationList(
timelineObjects, objectList,
timelineObjects.lastOrNull()?.postId, objectList.lastOrNull()?.postId,
timelineObjects.firstOrNull()?.postId objectList.firstOrNull()?.postId
) )
} }
} }
@Document @Document
data class SpringDataMongoTimelineObject( data class SpringDataMongoTimelineObject(
@Id
val id: Long, val id: Long,
val userDetailId: Long, val userDetailId: Long,
val timelineId: Long, val timelineId: Long,
@ -194,4 +218,14 @@ interface SpringDataMongoTimelineObjectRepository : CoroutineCrudRepository<Spri
suspend fun deleteByTimelineId(timelineId: Long) suspend fun deleteByTimelineId(timelineId: Long)
suspend fun findByTimelineId(timelineId: TimelineId): Flow<SpringDataMongoTimelineObject> suspend fun findByTimelineId(timelineId: TimelineId): Flow<SpringDataMongoTimelineObject>
suspend fun findFirstByTimelineIdAndPostIdGreaterThanOrderByIdAsc(
timelineId: Long,
postId: Long
): SpringDataMongoTimelineObject?
suspend fun findFirstByTimelineIdAndPostIdLessThanOrderByIdDesc(
timelineId: Long,
postId: Long
): SpringDataMongoTimelineObject?
} }

View File

@ -205,6 +205,11 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
removeTimelineObject(timeline.id) removeTimelineObject(timeline.id)
} }
protected abstract suspend fun getNextPaging(
timelineId: TimelineId,
page: Page?
): PaginationList<TimelineObjectDetail, PostId>
override suspend fun readTimeline( override suspend fun readTimeline(
timeline: Timeline, timeline: Timeline,
option: ReadTimelineOption?, option: ReadTimelineOption?,
@ -212,6 +217,9 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
principal: Principal principal: Principal
): PaginationList<TimelineObjectDetail, PostId> { ): PaginationList<TimelineObjectDetail, PostId> {
val timelineObjectList = getTimelineObject(timeline.id, option, page) val timelineObjectList = getTimelineObject(timeline.id, option, page)
if (timelineObjectList.isEmpty()) {
return getNextPaging(timeline.id, page)
}
val lastUpdatedAt = timelineObjectList.minBy { it.lastUpdatedAt }.lastUpdatedAt val lastUpdatedAt = timelineObjectList.minBy { it.lastUpdatedAt }.lastUpdatedAt
val newerFilters = getNewerFilters(timeline.userDetailId, lastUpdatedAt) val newerFilters = getNewerFilters(timeline.userDetailId, lastUpdatedAt)

View File

@ -18,6 +18,7 @@ import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.support.page.Page import dev.usbharu.hideout.core.domain.model.support.page.Page
import dev.usbharu.hideout.core.domain.model.support.page.PaginationList import dev.usbharu.hideout.core.domain.model.support.page.PaginationList
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail.TimelineObjectDetail
import dev.usbharu.hideout.core.domain.model.timeline.Timeline import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId import dev.usbharu.hideout.core.domain.model.timeline.TimelineId
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
@ -135,6 +136,30 @@ open class DefaultTimelineStore(
) )
} }
override suspend fun getNextPaging(
timelineId: TimelineId,
page: Page?
): PaginationList<TimelineObjectDetail, PostId> {
if (page?.maxId != null) {
return PaginationList(
emptyList(),
null,
internalTimelineObjectRepository.findByTimelineIdAndPostIdLT(timelineId, PostId(page.maxId!!))?.postId
?: PostId(0)
)
} else if (page?.minId != null) {
return PaginationList(
emptyList(),
internalTimelineObjectRepository.findByTimelineIdAndPostIdGT(timelineId, PostId(page.minId!!))?.postId
?: PostId(Long.MAX_VALUE),
null
)
}
return PaginationList(emptyList(), page?.maxId?.let { PostId(it) }, page?.minId?.let { PostId(it) })
}
override suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor> { override suspend fun getActors(actorIds: List<ActorId>): Map<ActorId, Actor> {
return actorRepository.findAllById(actorIds).associateBy { it.id } return actorRepository.findAllById(actorIds).associateBy { it.id }
} }

View File

@ -19,6 +19,17 @@ interface InternalTimelineObjectRepository {
suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId) suspend fun deleteByTimelineIdAndActorId(timelineId: TimelineId, actorId: ActorId)
suspend fun deleteByTimelineId(timelineId: TimelineId) suspend fun deleteByTimelineId(timelineId: TimelineId)
/**
* 指定したTimelineIdより大きく近いものを返す
*/
suspend fun findByTimelineIdAndPostIdGT(timelineId: TimelineId, postId: PostId): TimelineObject?
/**
* 指定したTimelineIdより小さく近いものを返す
*/
suspend fun findByTimelineIdAndPostIdLT(timelineId: TimelineId, postId: PostId): TimelineObject?
suspend fun findByTimelineId( suspend fun findByTimelineId(
timelineId: TimelineId, timelineId: TimelineId,
internalTimelineObjectOption: InternalTimelineObjectOption? = null, internalTimelineObjectOption: InternalTimelineObjectOption? = null,

View File

@ -20,7 +20,12 @@ class TimelineController(
private val transaction: Transaction private val transaction: Transaction
) { ) {
@GetMapping("/home") @GetMapping("/home")
suspend fun homeTimeline(model: Model, @RequestParam sinceId: String?, @RequestParam maxId: String?): String { suspend fun homeTimeline(
model: Model,
@RequestParam sinceId: String?,
@RequestParam maxId: String?,
@RequestParam minId: String?
): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal() val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
val userDetail = transaction.transaction { val userDetail = transaction.transaction {
userDetailRepository.findByActorId(principal.actorId.id) userDetailRepository.findByActorId(principal.actorId.id)
@ -36,7 +41,9 @@ class TimelineController(
remoteOnly = false, remoteOnly = false,
page = Page.of( page = Page.of(
maxId = maxId?.toLongOrNull(), maxId = maxId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull() sinceId = sinceId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
limit = 20
) )
), principal ), principal
) )

View File

@ -6,12 +6,14 @@
</Console> </Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="INFO"> <Root level="DEBUG">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Root> </Root>
<Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/> <Logger name="dev.usbharu.owl.broker.service.QueuedTaskAssignerImpl" level="TRACE"/>
<Logger name="org.mongodb.driver.cluster" level="WARN"/> <!-- <Logger name="org.mongodb.driver.cluster" level=""/>-->
<Logger name="org.apache.tomcat.util.net.NioEndpoint" level="INFO"/> <Logger name="org.apache.tomcat.util.net.NioEndpoint" level="INFO"/>
<Logger name="Exposed" level="DEBUG"/> <Logger name="Exposed" level="DEBUG"/>
<Logger name="sun.rmi" level="INFO"/>
<Logger name="javax.management.remote.rmi" level="INFO"/>
</Loggers> </Loggers>
</Configuration> </Configuration>

View File

@ -1,6 +1,8 @@
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9 common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB
common.paging-load=\u3082\u3063\u3068\u898B\u308B
common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B common.video=\u52D5\u753B

View File

@ -1,6 +1,8 @@
common.audio=Audio common.audio=Audio
common.audio-download-link=Download the audio. common.audio-download-link=Download the audio.
common.empty=Empty
common.media-original-link=original common.media-original-link=original
common.paging-load=Show more
common.thumbnail=thumbnail common.thumbnail=thumbnail
common.unknwon-file-type=Unknown filetype common.unknwon-file-type=Unknown filetype
common.video=Video common.video=Video

View File

@ -1,6 +1,8 @@
common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA common.audio=\u30AA\u30FC\u30C7\u30A3\u30AA
common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9 common.audio-download-link=\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.empty=\u8868\u793A\u3059\u308B\u3082\u306E\u304C\u3042\u308A\u307E\u305B\u3093
common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB common.media-original-link=\u30AA\u30EA\u30B8\u30CA\u30EB
common.paging-load=\u3082\u3063\u3068\u898B\u308B
common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B common.video=\u52D5\u753B

View File

@ -7,14 +7,16 @@
<body> <body>
<th:block th:fragment="simple-timline(timelineObject,href)"> <th:block th:fragment="simple-timline(timelineObject,href)">
<!--/*@thymesVar id="timelineObject" type="dev.usbharu.hideout.core.domain.model.support.page.PaginationList<dev.usbharu.hideout.core.application.post.PostDetail,dev.usbharu.hideout.core.domain.model.post.PostId>"*/--> <!--/*@thymesVar id="timelineObject" type="dev.usbharu.hideout.core.domain.model.support.page.PaginationList<dev.usbharu.hideout.core.application.post.PostDetail,dev.usbharu.hideout.core.domain.model.post.PostId>"*/-->
<div th:if="${timelineObject.prev == null}"> <div th:if="${timelineObject.prev != null}">
<a th:href="${href + '/?maxId=' + timelineObject.prev}"></a> <a th:href="${href + '?minId=' + timelineObject.prev.id}" th:text="#{common.paging-load}">Show more</a>
</div> </div>
<div th:if="${timelineObject.isEmpty()}" th:text="#{common.empty}"></div>
<div th:each="postDetail : ${timelineObject}"> <div th:each="postDetail : ${timelineObject}">
<th:block th:replace="fragments-post :: single-simple-post(${postDetail})"></th:block> <th:block th:replace="~{fragments-post :: single-simple-post(${postDetail})}"></th:block>
<th:block th:replace="~{fragments-post :: single-post-controller(${postDetail})}"></th:block>
</div> </div>
<div th:if="${timelineObject.next == null}"> <div th:if="${timelineObject.next != null}">
<a th:href="${href + '/?sinceId=' + timelineObject.next}"></a> <a th:href="${href + '?maxId=' + timelineObject.next.id}" th:text="#{common.paging-load}">Show more</a>
</div> </div>
</th:block> </th:block>
</body> </body>