Skip to content
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

fix: 이전 글 다음 글만 fetch 하도록 쿼리 최적화 #75

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.wafflestudio.csereal.common.aop.AuthenticatedStaff
import com.wafflestudio.csereal.core.notice.dto.*
import com.wafflestudio.csereal.core.notice.service.NoticeService
import jakarta.validation.Valid
import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
Expand All @@ -18,18 +19,19 @@ class NoticeController(
fun searchNotice(
@RequestParam(required = false) tag: List<String>?,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false, defaultValue = "0") pageNum: Long
@RequestParam(required = false, defaultValue = "1") pageNum: Int,
@RequestParam(required = false, defaultValue = "false") usePageBtn: Boolean
): ResponseEntity<NoticeSearchResponse> {
return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageNum))
val pageSize = 20
val pageRequest = PageRequest.of(pageNum - 1, pageSize)
return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageRequest, usePageBtn))
}

@GetMapping("/{noticeId}")
fun readNotice(
@PathVariable noticeId: Long,
@RequestParam(required = false) tag: List<String>?,
@RequestParam(required = false) keyword: String?,
@PathVariable noticeId: Long
): ResponseEntity<NoticeDto> {
return ResponseEntity.ok(noticeService.readNotice(noticeId, tag, keyword))
return ResponseEntity.ok(noticeService.readNotice(noticeId))
}

@AuthenticatedStaff
Expand Down Expand Up @@ -63,6 +65,7 @@ class NoticeController(
) {
noticeService.unpinManyNotices(request.idList)
}

@DeleteMapping
fun deleteManyNotices(
@RequestBody request: NoticeIdListRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,42 @@ import com.wafflestudio.csereal.core.notice.database.QNoticeEntity.noticeEntity
import com.wafflestudio.csereal.core.notice.database.QNoticeTagEntity.noticeTagEntity
import com.wafflestudio.csereal.core.notice.dto.NoticeSearchDto
import com.wafflestudio.csereal.core.notice.dto.NoticeSearchResponse
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import kotlin.math.ceil

interface NoticeRepository : JpaRepository<NoticeEntity, Long>, CustomNoticeRepository {
fun findAllByIsImportant(isImportant: Boolean): List<NoticeEntity>
fun findFirstByCreatedAtLessThanOrderByCreatedAtDesc(timestamp: LocalDateTime): NoticeEntity?
fun findFirstByCreatedAtGreaterThanOrderByCreatedAtAsc(timestamp: LocalDateTime): NoticeEntity?
}

interface CustomNoticeRepository {
fun searchNotice(tag: List<String>?, keyword: String?, pageNum: Long): NoticeSearchResponse
fun findPrevNextId(noticeId: Long, tag: List<String>?, keyword: String?): Array<NoticeEntity?>?
fun searchNotice(
tag: List<String>?,
keyword: String?,
pageable: Pageable,
usePageBtn: Boolean
): NoticeSearchResponse
}

@Component
class NoticeRepositoryImpl(
private val queryFactory: JPAQueryFactory,
) : CustomNoticeRepository {
override fun searchNotice(tag: List<String>?, keyword: String?, pageNum: Long): NoticeSearchResponse {
override fun searchNotice(
tag: List<String>?,
keyword: String?,
pageable: Pageable,
usePageBtn: Boolean
): NoticeSearchResponse {
val keywordBooleanBuilder = BooleanBuilder()
val tagsBooleanBuilder = BooleanBuilder()

if (!keyword.isNullOrEmpty()) {

val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex())
keywordList.forEach {
if (it.length == 1) {
Expand All @@ -39,8 +53,8 @@ class NoticeRepositoryImpl(
.or(noticeEntity.description.contains(it))
)
}
}

}
}
if (!tag.isNullOrEmpty()) {
tag.forEach {
Expand All @@ -50,18 +64,27 @@ class NoticeRepositoryImpl(
}
}

val jpaQuery = queryFactory.select(noticeEntity).from(noticeEntity)
val jpaQuery = queryFactory.selectFrom(noticeEntity)
.leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity))
.where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true))
.where(keywordBooleanBuilder).where(tagsBooleanBuilder)
.where(keywordBooleanBuilder, tagsBooleanBuilder)

val countQuery = jpaQuery.clone()
val total = countQuery.select(noticeEntity.countDistinct()).fetchOne()
val total: Long
var pageRequest = pageable

if (usePageBtn) {
val countQuery = jpaQuery.clone()
total = countQuery.select(noticeEntity.countDistinct()).fetchOne()!!
pageRequest = exchangePageRequest(pageable, total)
} else {
total = (10 * pageable.pageSize).toLong() // 10개 페이지 고정
}

val noticeEntityList = jpaQuery.orderBy(noticeEntity.isPinned.desc())
val noticeEntityList = jpaQuery
.orderBy(noticeEntity.isPinned.desc())
.orderBy(noticeEntity.createdAt.desc())
.offset(20 * pageNum)
.limit(20)
.offset(pageRequest.offset)
.limit(pageRequest.pageSize.toLong())
.distinct()
.fetch()

Expand All @@ -77,62 +100,25 @@ class NoticeRepositoryImpl(
)
}

return NoticeSearchResponse(total!!, noticeSearchDtoList)
}

override fun findPrevNextId(noticeId: Long, tag: List<String>?, keyword: String?): Array<NoticeEntity?>? {
val keywordBooleanBuilder = BooleanBuilder()
val tagsBooleanBuilder = BooleanBuilder()
return NoticeSearchResponse(total, noticeSearchDtoList)

if (!keyword.isNullOrEmpty()) {
val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex())
keywordList.forEach {
if (it.length == 1) {
throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.")
} else {
keywordBooleanBuilder.and(
noticeEntity.title.contains(it)
.or(noticeEntity.description.contains(it))
)
}
}

}
}
if (!tag.isNullOrEmpty()) {
tag.forEach {
tagsBooleanBuilder.or(
noticeTagEntity.tag.name.eq(it)
)
}
}
private fun exchangePageRequest(pageable: Pageable, total: Long): Pageable {
/**
* 요청한 페이지 번호가 기존 데이터 사이즈 초과할 경우 마지막 페이지 데이터 반환
*/

val noticeSearchDtoList = queryFactory.select(noticeEntity).from(noticeEntity)
.leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity))
.where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true))
.where(keywordBooleanBuilder).where(tagsBooleanBuilder)
.orderBy(noticeEntity.isPinned.desc())
.orderBy(noticeEntity.createdAt.desc())
.distinct()
.fetch()
val pageNum = pageable.pageNumber
val pageSize = pageable.pageSize
val requestCount = (pageNum - 1) * pageSize

val findingId = noticeSearchDtoList.indexOfFirst { it.id == noticeId }

val prevNext: Array<NoticeEntity?>?
if (findingId == -1) {
prevNext = arrayOf(null, null)
} else if (findingId != 0 && findingId != noticeSearchDtoList.size - 1) {
prevNext = arrayOf(noticeSearchDtoList[findingId + 1], noticeSearchDtoList[findingId - 1])
} else if (findingId == 0) {
if (noticeSearchDtoList.size == 1) {
prevNext = arrayOf(null, null)
} else {
prevNext = arrayOf(noticeSearchDtoList[1], null)
}
} else {
prevNext = arrayOf(null, noticeSearchDtoList[noticeSearchDtoList.size - 2])
if (total > requestCount) {
return pageable
}

return prevNext
val requestPageNum = ceil(total.toDouble() / pageNum).toInt()
return PageRequest.of(requestPageNum, pageSize)

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ data class NoticeDto(
fun of(
entity: NoticeEntity,
attachmentResponses: List<AttachmentResponse>,
prevNext: Array<NoticeEntity?>?
prevNotice: NoticeEntity? = null,
nextNotice: NoticeEntity? = null
): NoticeDto = entity.run {
NoticeDto(
id = this.id,
Expand All @@ -39,10 +40,10 @@ data class NoticeDto(
isPublic = this.isPublic,
isPinned = this.isPinned,
isImportant = this.isImportant,
prevId = prevNext?.get(0)?.id,
prevTitle = prevNext?.get(0)?.title,
nextId = prevNext?.get(1)?.id,
nextTitle = prevNext?.get(1)?.title,
prevId = prevNotice?.id,
prevTitle = prevNotice?.title,
nextId = nextNotice?.id,
nextTitle = nextNotice?.title,
attachments = attachmentResponses,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.wafflestudio.csereal.core.notice.dto.*
import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService
import com.wafflestudio.csereal.core.user.database.UserEntity
import com.wafflestudio.csereal.core.user.database.UserRepository
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.core.oidc.user.OidcUser
Expand All @@ -17,8 +18,14 @@ import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.multipart.MultipartFile

interface NoticeService {
fun searchNotice(tag: List<String>?, keyword: String?, pageNum: Long): NoticeSearchResponse
fun readNotice(noticeId: Long, tag: List<String>?, keyword: String?): NoticeDto
fun searchNotice(
tag: List<String>?,
keyword: String?,
pageable: Pageable,
usePageBtn: Boolean
): NoticeSearchResponse

fun readNotice(noticeId: Long): NoticeDto
fun createNotice(request: NoticeDto, attachments: List<MultipartFile>?): NoticeDto
fun updateNotice(noticeId: Long, request: NoticeDto, attachments: List<MultipartFile>?): NoticeDto
fun deleteNotice(noticeId: Long)
Expand All @@ -40,27 +47,25 @@ class NoticeServiceImpl(
override fun searchNotice(
tag: List<String>?,
keyword: String?,
pageNum: Long
pageable: Pageable,
usePageBtn: Boolean
): NoticeSearchResponse {
return noticeRepository.searchNotice(tag, keyword, pageNum)
return noticeRepository.searchNotice(tag, keyword, pageable, usePageBtn)
}

@Transactional(readOnly = true)
override fun readNotice(
noticeId: Long,
tag: List<String>?,
keyword: String?
): NoticeDto {
val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId)
override fun readNotice(noticeId: Long): NoticeDto {
val notice = noticeRepository.findByIdOrNull(noticeId)
?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)")

if (notice.isDeleted) throw CserealException.Csereal404("삭제된 공지사항입니다.(noticeId: $noticeId)")

val attachmentResponses = attachmentService.createAttachmentResponses(notice.attachments)

val prevNext = noticeRepository.findPrevNextId(noticeId, tag, keyword)
val prevNotice = noticeRepository.findFirstByCreatedAtLessThanOrderByCreatedAtDesc(notice.createdAt!!)
val nextNotice = noticeRepository.findFirstByCreatedAtGreaterThanOrderByCreatedAtAsc(notice.createdAt!!)

return NoticeDto.of(notice, attachmentResponses, prevNext)
return NoticeDto.of(notice, attachmentResponses, prevNotice, nextNotice)
}

@Transactional
Expand Down Expand Up @@ -92,15 +97,15 @@ class NoticeServiceImpl(
NoticeTagEntity.createNoticeTag(newNotice, tag)
}

if(attachments != null) {
if (attachments != null) {
attachmentService.uploadAllAttachments(newNotice, attachments)
}

noticeRepository.save(newNotice)

val attachmentResponses = attachmentService.createAttachmentResponses(newNotice.attachments)

return NoticeDto.of(newNotice, attachmentResponses, null)
return NoticeDto.of(newNotice, attachmentResponses)

}

Expand All @@ -112,7 +117,7 @@ class NoticeServiceImpl(

notice.update(request)

if(attachments != null) {
if (attachments != null) {
notice.attachments.clear()
attachmentService.uploadAllAttachments(notice, attachments)
} else {
Expand All @@ -137,7 +142,7 @@ class NoticeServiceImpl(

val attachmentResponses = attachmentService.createAttachmentResponses(notice.attachments)

return NoticeDto.of(notice, attachmentResponses, null)
return NoticeDto.of(notice, attachmentResponses)
}

@Transactional
Expand All @@ -151,15 +156,16 @@ class NoticeServiceImpl(

@Transactional
override fun unpinManyNotices(idList: List<Long>) {
for(noticeId in idList) {
for (noticeId in idList) {
val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId)
?: throw CserealException.Csereal404("존재하지 않는 공지사항을 입력하였습니다.(noticeId: $noticeId)")
notice.isPinned = false
}
}

@Transactional
override fun deleteManyNotices(idList: List<Long>) {
for(noticeId in idList) {
for (noticeId in idList) {
val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId)
?: throw CserealException.Csereal404("존재하지 않는 공지사항을 입력하였습니다.(noticeId: $noticeId)")
notice.isDeleted = true
Expand Down