Merge pull request #107 from usbharu/feature/refactor-note-service

refactor: APNoteServiceをNote取得クエリを少なくなるようにリファクタリング
This commit is contained in:
usbharu 2023-11-02 12:44:50 +09:00 committed by GitHub
commit 36aa131c3d
4 changed files with 120 additions and 74 deletions

View File

@ -5,10 +5,12 @@ import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.infrastructure.exposedrepository.* import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@ -24,7 +26,22 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(Media) .leftJoin(Media)
.select { Posts.id eq id } .select { Posts.id eq id }
.let { it.toNote() to postQueryMapper.map(it).first() } .let {
it.toNote() to postQueryMapper.map(it)
.singleOr { FailedToGetResourcesException("id: $id does not exist.") }
}
}
override suspend fun findByApid(apId: String): Pair<Note, Post> {
return Posts
.leftJoin(Users)
.leftJoin(PostsMedia)
.leftJoin(Media)
.select { Posts.apId eq apId }
.let {
it.toNote() to postQueryMapper.map(it)
.singleOr { FailedToGetResourcesException("apid: $apId does not exist.") }
}
} }
private suspend fun ResultRow.toNote(mediaList: List<dev.usbharu.hideout.core.domain.model.media.Media>): Note { private suspend fun ResultRow.toNote(mediaList: List<dev.usbharu.hideout.core.domain.model.media.Media>): Note {
@ -58,7 +75,7 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
return this.groupBy { it[Posts.id] } return this.groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { it.first().toNote(it.mapNotNull { it.toMediaOrNull() }) } .map { it.first().toNote(it.mapNotNull { it.toMediaOrNull() }) }
.first() .singleOr { FailedToGetResourcesException("resource does not exist.") }
} }
private fun visibility(visibility: Visibility, followers: String?): Pair<List<String>, List<String>> { private fun visibility(visibility: Visibility, followers: String?): Pair<List<String>, List<String>> {

View File

@ -5,4 +5,5 @@ import dev.usbharu.hideout.core.domain.model.post.Post
interface NoteQueryService { interface NoteQueryService {
suspend fun findById(id: Long): Pair<Note, Post> suspend fun findById(id: Long): Pair<Note, Post>
suspend fun findByApid(apId: String): Pair<Note, Post>
} }

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
import dev.usbharu.hideout.activitypub.service.common.resolve import dev.usbharu.hideout.activitypub.service.common.resolve
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
@ -62,7 +63,8 @@ class APNoteServiceImpl(
@Qualifier("activitypub") private val objectMapper: ObjectMapper, @Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val postService: PostService, private val postService: PostService,
private val apResourceResolveService: APResourceResolveService, private val apResourceResolveService: APResourceResolveService,
private val postBuilder: Post.PostBuilder private val postBuilder: Post.PostBuilder,
private val noteQueryService: NoteQueryService
) : APNoteService, PostCreateInterceptor { ) : APNoteService, PostCreateInterceptor {
@ -98,9 +100,9 @@ class APNoteServiceImpl(
override suspend fun fetchNote(url: String, targetActor: String?): Note { override suspend fun fetchNote(url: String, targetActor: String?): Note {
logger.debug("START Fetch Note url: {}", url) logger.debug("START Fetch Note url: {}", url)
try { try {
val post = postQueryService.findByUrl(url) val post = noteQueryService.findByApid(url)
logger.debug("SUCCESS Found in local url: {}", url) logger.debug("SUCCESS Found in local url: {}", url)
return postToNote(post) return post.first
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
} }
@ -115,27 +117,11 @@ class APNoteServiceImpl(
) )
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e) throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
} }
val savedNote = saveIfMissing(note, targetActor, url) val savedNote = saveNote(note, targetActor, url)
logger.debug("SUCCESS Fetch Note url: {}", url) logger.debug("SUCCESS Fetch Note url: {}", url)
return savedNote return savedNote
} }
private suspend fun postToNote(post: Post): Note {
val user = userQueryService.findById(post.userId)
val reply = post.replyId?.let { postQueryService.findById(it) }
return Note(
name = "Post",
id = post.apId,
attributedTo = user.url,
content = post.text,
published = Instant.ofEpochMilli(post.createdAt).toString(),
to = listOfNotNull(public, user.followers),
sensitive = post.sensitive,
cc = listOfNotNull(public, user.followers),
inReplyTo = reply?.url
)
}
private suspend fun saveIfMissing( private suspend fun saveIfMissing(
note: Note, note: Note,
targetActor: String?, targetActor: String?,
@ -146,12 +132,11 @@ class APNoteServiceImpl(
// return internalNote(note, targetActor, url) // return internalNote(note, targetActor, url)
} }
val findByApId = try { return try {
postQueryService.findByApId(note.id!!) noteQueryService.findByApid(note.id!!).first
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
return saveNote(note, targetActor, url) saveNote(note, targetActor, url)
} }
return postToNote(findByApId)
} }
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note { private suspend fun saveNote(note: Note, targetActor: String?, url: String): Note {

View File

@ -7,6 +7,7 @@ import dev.usbharu.hideout.activitypub.domain.model.Image
import dev.usbharu.hideout.activitypub.domain.model.Key import dev.usbharu.hideout.activitypub.domain.model.Key
import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.domain.model.Person import dev.usbharu.hideout.activitypub.domain.model.Person
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
@ -126,7 +127,8 @@ class APNoteServiceImplTest {
objectMapper = objectMapper, objectMapper = objectMapper,
postService = mock(), postService = mock(),
apResourceResolveService = mock(), apResourceResolveService = mock(),
postBuilder = postBuilder postBuilder = postBuilder,
noteQueryService = mock()
) )
val postEntity = postBuilder.of( val postEntity = postBuilder.of(
1L, 1L, null, "test text", 1L, Visibility.PUBLIC, "https://example.com" 1L, 1L, null, "test text", 1L, Visibility.PUBLIC, "https://example.com"
@ -141,29 +143,10 @@ class APNoteServiceImplTest {
val url = "https://example.com/note" val url = "https://example.com/note"
val post = PostBuilder.of() val post = PostBuilder.of()
val postQueryService = mock<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doReturn post
}
val user = UserBuilder.localUserOf(id = post.userId) val user = UserBuilder.localUserOf(id = post.userId)
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(post.userId)) } doReturn user onBlocking { findById(eq(post.userId)) } doReturn user
} }
val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = mock(),
postQueryService = postQueryService,
mediaQueryService = mock(),
objectMapper = objectMapper,
postService = mock(),
apResourceResolveService = mock(),
postBuilder = Post.PostBuilder(CharacterLimit())
)
val actual = apNoteServiceImpl.fetchNote(url)
val expected = Note( val expected = Note(
name = "Post", name = "Post",
id = post.apId, id = post.apId,
@ -175,6 +158,26 @@ class APNoteServiceImplTest {
cc = listOfNotNull(public, user.followers), cc = listOfNotNull(public, user.followers),
inReplyTo = null inReplyTo = null
) )
val noteQueryService = mock<NoteQueryService> {
onBlocking { findByApid(eq(url)) } doReturn (expected to post)
}
val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = mock(),
postQueryService = mock(),
mediaQueryService = mock(),
objectMapper = objectMapper,
postService = mock(),
apResourceResolveService = mock(),
postBuilder = Post.PostBuilder(CharacterLimit()),
noteQueryService = noteQueryService
)
val actual = apNoteServiceImpl.fetchNote(url)
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@ -184,7 +187,6 @@ class APNoteServiceImplTest {
val post = PostBuilder.of() val post = PostBuilder.of()
val postQueryService = mock<PostQueryService> { val postQueryService = mock<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException()
onBlocking { findByApId(eq(post.apId)) } doReturn post onBlocking { findByApId(eq(post.apId)) } doReturn post
} }
val user = UserBuilder.localUserOf(id = post.userId) val user = UserBuilder.localUserOf(id = post.userId)
@ -205,10 +207,45 @@ class APNoteServiceImplTest {
val apResourceResolveService = mock<APResourceResolveService> { val apResourceResolveService = mock<APResourceResolveService> {
onBlocking { resolve<Note>(eq(url), any(), isNull<Long>()) } doReturn note onBlocking { resolve<Note>(eq(url), any(), isNull<Long>()) } doReturn note
} }
val noteQueryService = mock<NoteQueryService> {
onBlocking { findByApid(eq(url)) } doThrow FailedToGetResourcesException()
}
val person = Person(
name = user.name,
id = user.url,
preferredUsername = user.name,
summary = user.description,
inbox = user.inbox,
outbox = user.outbox,
url = user.url,
icon = Image(
type = emptyList(),
name = user.url + "/icon.png",
mediaType = "image/png",
url = user.url + "/icon.png"
),
publicKey = Key(
type = emptyList(),
name = "Public Key",
id = user.keyId,
owner = user.url,
publicKeyPem = user.publicKey
),
endpoints = mapOf("sharedInbox" to "https://example.com/inbox"),
followers = user.followers,
following = user.following,
)
val apUserService = mock<APUserService> {
onBlocking { fetchPersonWithEntity(eq(note.attributedTo!!), isNull()) } doReturn (person to user)
}
val postRepository = mock<PostRepository> {
onBlocking { generateId() } doReturn TwitterSnowflakeIdGenerateService.generateId()
}
val apNoteServiceImpl = APNoteServiceImpl( val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(), jobQueueParentService = mock(),
postRepository = mock(), postRepository = postRepository,
apUserService = mock(), apUserService = apUserService,
userQueryService = userQueryService, userQueryService = userQueryService,
followerQueryService = mock(), followerQueryService = mock(),
postQueryService = postQueryService, postQueryService = postQueryService,
@ -216,7 +253,8 @@ class APNoteServiceImplTest {
objectMapper = objectMapper, objectMapper = objectMapper,
postService = mock(), postService = mock(),
apResourceResolveService = apResourceResolveService, apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder(CharacterLimit()) postBuilder = Post.PostBuilder(CharacterLimit()),
noteQueryService = noteQueryService
) )
val actual = apNoteServiceImpl.fetchNote(url) val actual = apNoteServiceImpl.fetchNote(url)
@ -232,7 +270,6 @@ class APNoteServiceImplTest {
val post = PostBuilder.of() val post = PostBuilder.of()
val postQueryService = mock<PostQueryService> { val postQueryService = mock<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException()
onBlocking { findByApId(eq(post.apId)) } doReturn post onBlocking { findByApId(eq(post.apId)) } doReturn post
} }
val user = UserBuilder.localUserOf(id = post.userId) val user = UserBuilder.localUserOf(id = post.userId)
@ -274,6 +311,9 @@ class APNoteServiceImplTest {
), "" ), ""
) )
} }
val noteQueryService = mock<NoteQueryService> {
onBlocking { findByApid(eq(url)) } doThrow FailedToGetResourcesException()
}
val apNoteServiceImpl = APNoteServiceImpl( val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(), jobQueueParentService = mock(),
postRepository = mock(), postRepository = mock(),
@ -285,7 +325,8 @@ class APNoteServiceImplTest {
objectMapper = objectMapper, objectMapper = objectMapper,
postService = mock(), postService = mock(),
apResourceResolveService = apResourceResolveService, apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder(CharacterLimit()) postBuilder = Post.PostBuilder(CharacterLimit()),
noteQueryService = noteQueryService
) )
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) } assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }
@ -297,9 +338,6 @@ class APNoteServiceImplTest {
val user = UserBuilder.localUserOf() val user = UserBuilder.localUserOf()
val generateId = TwitterSnowflakeIdGenerateService.generateId() val generateId = TwitterSnowflakeIdGenerateService.generateId()
val post = PostBuilder.of(id = generateId, userId = user.id) val post = PostBuilder.of(id = generateId, userId = user.id)
val postQueryService = mock<PostQueryService> {
onBlocking { findByApId(eq(post.apId)) } doThrow FailedToGetResourcesException()
}
val postRepository = mock<PostRepository> { val postRepository = mock<PostRepository> {
onBlocking { generateId() } doReturn generateId onBlocking { generateId() } doReturn generateId
} }
@ -329,18 +367,22 @@ class APNoteServiceImplTest {
onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user) onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user)
} }
val postService = mock<PostService>() val postService = mock<PostService>()
val noteQueryService = mock<NoteQueryService> {
onBlocking { findByApid(eq(post.apId)) } doThrow FailedToGetResourcesException()
}
val apNoteServiceImpl = APNoteServiceImpl( val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(), jobQueueParentService = mock(),
postRepository = postRepository, postRepository = postRepository,
apUserService = apUserService, apUserService = apUserService,
userQueryService = mock(), userQueryService = mock(),
followerQueryService = mock(), followerQueryService = mock(),
postQueryService = postQueryService, postQueryService = mock(),
mediaQueryService = mock(), mediaQueryService = mock(),
objectMapper = objectMapper, objectMapper = objectMapper,
postService = postService, postService = postService,
apResourceResolveService = mock(), apResourceResolveService = mock(),
postBuilder = postBuilder postBuilder = postBuilder,
noteQueryService = noteQueryService
) )
val note = Note( val note = Note(
@ -373,26 +415,9 @@ class APNoteServiceImplTest {
val user = UserBuilder.localUserOf() val user = UserBuilder.localUserOf()
val post = PostBuilder.of(userId = user.id) val post = PostBuilder.of(userId = user.id)
val postQueryService = mock<PostQueryService> {
onBlocking { findByApId(eq(post.apId)) } doReturn post
}
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(user.id)) } doReturn user onBlocking { findById(eq(user.id)) } doReturn user
} }
val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = mock(),
postQueryService = postQueryService,
mediaQueryService = mock(),
objectMapper = objectMapper,
postService = mock(),
apResourceResolveService = mock(),
postBuilder = postBuilder
)
val note = Note( val note = Note(
name = "Post", name = "Post",
id = post.apId, id = post.apId,
@ -404,6 +429,24 @@ class APNoteServiceImplTest {
cc = listOfNotNull(public, user.followers), cc = listOfNotNull(public, user.followers),
inReplyTo = null inReplyTo = null
) )
val noteQueryService = mock<NoteQueryService> {
onBlocking { findByApid(eq(post.apId)) } doReturn (note to post)
}
val apNoteServiceImpl = APNoteServiceImpl(
jobQueueParentService = mock(),
postRepository = mock(),
apUserService = mock(),
userQueryService = userQueryService,
followerQueryService = mock(),
postQueryService = mock(),
mediaQueryService = mock(),
objectMapper = objectMapper,
postService = mock(),
apResourceResolveService = mock(),
postBuilder = postBuilder,
noteQueryService = noteQueryService
)
val fetchNote = apNoteServiceImpl.fetchNote(note, null) val fetchNote = apNoteServiceImpl.fetchNote(note, null)
assertEquals(note, fetchNote) assertEquals(note, fetchNote)