mirror of
https://github.com/gotson/komga.git
synced 2025-12-26 02:15:40 +01:00
make BookMetadata.pages lazy
add caching for Bookmetadata, Bookmetadata.pages, Series.books enhance books retrieval to reduce database load rollback SeriesDto.booksCount to use books.size and leverage hibernate l2 cache and collection cache fix Series thumbnail by getting the book by number instead of the first in the collection
This commit is contained in:
parent
4603049012
commit
02361e154f
9 changed files with 75 additions and 26 deletions
|
|
@ -1,7 +1,10 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||
import org.hibernate.annotations.Cache
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy
|
||||
import java.util.*
|
||||
import javax.persistence.Cacheable
|
||||
import javax.persistence.CollectionTable
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.ElementCollection
|
||||
|
|
@ -21,6 +24,8 @@ private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNatural
|
|||
|
||||
@Entity
|
||||
@Table(name = "book_metadata")
|
||||
@Cacheable
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.bookmetadata")
|
||||
class BookMetadata(
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false)
|
||||
|
|
@ -43,9 +48,10 @@ class BookMetadata(
|
|||
@OneToOne(optional = false, fetch = FetchType.LAZY, mappedBy = "metadata")
|
||||
lateinit var book: Book
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "book_metadata_page", joinColumns = [JoinColumn(name = "book_metadata_id")])
|
||||
@OrderColumn(name = "number")
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.bookmetadata.collection.pages")
|
||||
private var _pages: MutableList<BookPage> = mutableListOf()
|
||||
|
||||
var pages: List<BookPage>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Series(
|
|||
lateinit var library: Library
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "series")
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "cache.series.collection.books")
|
||||
private var _books: MutableList<Book> = mutableListOf()
|
||||
|
||||
var books: List<Book>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package org.gotson.komga.domain.persistence
|
|||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadata
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
|
@ -20,6 +19,4 @@ interface BookRepository : JpaRepository<Book, Long>, JpaSpecificationExecutor<B
|
|||
fun findAllByMetadataStatus(status: BookMetadata.Status): List<Book>
|
||||
fun findAllByMetadataThumbnailIsNull(): List<Book>
|
||||
fun findBySeriesLibraryIn(seriesLibrary: Collection<Library>, pageable: Pageable): Page<Book>
|
||||
|
||||
fun countBySeries(series: Series): Int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ interface SeriesRepository : JpaRepository<Series, Long>, JpaSpecificationExecut
|
|||
fun findByLibraryIdAndUrlNotIn(libraryId: Long, urls: Collection<URL>): List<Series>
|
||||
fun findByLibraryIdAndUrl(libraryId: Long, url: URL): Series?
|
||||
fun deleteByLibraryId(libraryId: Long)
|
||||
fun findByLibraryIdIn(libraryIDs: Collection<Long>): List<Series>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,19 +62,25 @@ class BookController(
|
|||
)
|
||||
|
||||
return mutableListOf<Specification<Book>>().let { specs ->
|
||||
if (!principal.user.sharedAllLibraries) {
|
||||
specs.add(Book::series.`in`(seriesRepository.findByLibraryIn(principal.user.sharedLibraries)))
|
||||
when {
|
||||
!principal.user.sharedAllLibraries && !libraryIds.isNullOrEmpty() -> {
|
||||
val authorizedLibraryIDs = libraryIds.intersect(principal.user.sharedLibraries.map { it.id })
|
||||
if (authorizedLibraryIDs.isEmpty()) return@let Page.empty<Book>(pageRequest)
|
||||
else specs.add(Book::series.`in`(seriesRepository.findByLibraryIdIn(authorizedLibraryIDs)))
|
||||
}
|
||||
|
||||
!principal.user.sharedAllLibraries -> specs.add(Book::series.`in`(seriesRepository.findByLibraryIn(principal.user.sharedLibraries)))
|
||||
|
||||
!libraryIds.isNullOrEmpty() -> {
|
||||
val libraries = libraryRepository.findAllById(libraryIds)
|
||||
specs.add(Book::series.`in`(seriesRepository.findByLibraryIn(libraries)))
|
||||
}
|
||||
}
|
||||
|
||||
if (!searchTerm.isNullOrEmpty()) {
|
||||
specs.add(Book::name.likeLower("%$searchTerm%"))
|
||||
}
|
||||
|
||||
if (!libraryIds.isNullOrEmpty()) {
|
||||
val libraries = libraryRepository.findAllById(libraryIds)
|
||||
specs.add(Book::series.`in`(seriesRepository.findByLibraryIn(libraries)))
|
||||
}
|
||||
|
||||
if (specs.isNotEmpty()) {
|
||||
bookRepository.findAll(specs.reduce { acc, spec -> acc.and(spec)!! }, pageRequest)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ data class SeriesDto(
|
|||
val booksCount: Int
|
||||
)
|
||||
|
||||
fun Series.toDto(booksCount: Int) = SeriesDto(
|
||||
fun Series.toDto() = SeriesDto(
|
||||
id = id,
|
||||
libraryId = library.id,
|
||||
name = name,
|
||||
url = url.toString(),
|
||||
lastModified = lastModifiedDate?.toUTC(),
|
||||
booksCount = booksCount
|
||||
booksCount = books.size
|
||||
)
|
||||
|
||||
data class BookDto(
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ class SeriesController(
|
|||
!principal.user.sharedAllLibraries -> specs.add(Series::library.`in`(principal.user.sharedLibraries))
|
||||
|
||||
!libraryIds.isNullOrEmpty() -> {
|
||||
val values = libraryRepository.findAllById(libraryIds)
|
||||
specs.add(Series::library.`in`(values))
|
||||
val libraries = libraryRepository.findAllById(libraryIds)
|
||||
specs.add(Series::library.`in`(libraries))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ class SeriesController(
|
|||
} else {
|
||||
seriesRepository.findAll(pageRequest)
|
||||
}
|
||||
}.map { it.toDto(bookRepository.countBySeries(it)) }
|
||||
}.map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("/latest")
|
||||
|
|
@ -95,7 +95,7 @@ class SeriesController(
|
|||
seriesRepository.findAll(pageRequest)
|
||||
} else {
|
||||
seriesRepository.findByLibraryIn(principal.user.sharedLibraries, pageRequest)
|
||||
}.map { it.toDto(bookRepository.countBySeries(it)) }
|
||||
}.map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("{seriesId}")
|
||||
|
|
@ -105,7 +105,7 @@ class SeriesController(
|
|||
): SeriesDto =
|
||||
seriesRepository.findByIdOrNull(id)?.let {
|
||||
if (!principal.user.canAccessSeries(it)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||
it.toDto(bookRepository.countBySeries(it))
|
||||
it.toDto()
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(value = ["{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE])
|
||||
|
|
@ -116,7 +116,7 @@ class SeriesController(
|
|||
seriesRepository.findByIdOrNull(id)?.let { series ->
|
||||
if (!principal.user.canAccessSeries(series)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||
|
||||
val thumbnail = series.books.firstOrNull()?.metadata?.thumbnail
|
||||
val thumbnail = series.books.minBy { it.number }?.metadata?.thumbnail
|
||||
if (thumbnail != null) {
|
||||
ResponseEntity.ok()
|
||||
.cacheControl(CacheControl
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ caffeine.jcache {
|
|||
}
|
||||
}
|
||||
|
||||
cache.series.collection.books {
|
||||
monitoring {
|
||||
statistics = true
|
||||
}
|
||||
policy {
|
||||
maximum {
|
||||
size = 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.book {
|
||||
monitoring {
|
||||
statistics = true
|
||||
|
|
@ -31,4 +42,26 @@ caffeine.jcache {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.bookmetadata {
|
||||
monitoring {
|
||||
statistics = true
|
||||
}
|
||||
policy {
|
||||
maximum {
|
||||
size = 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.bookmetadata.collection.pages {
|
||||
monitoring {
|
||||
statistics = true
|
||||
}
|
||||
policy {
|
||||
maximum {
|
||||
size = 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired
|
|||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.nio.file.Paths
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
|
|
@ -30,7 +32,8 @@ class LibraryScannerTest(
|
|||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val libraryScanner: LibraryScanner,
|
||||
@Autowired private val bookLifecycle: BookLifecycle
|
||||
@Autowired private val bookLifecycle: BookLifecycle,
|
||||
@Autowired private val transactionManager: PlatformTransactionManager
|
||||
) {
|
||||
|
||||
@MockkBean
|
||||
|
|
@ -201,12 +204,14 @@ class LibraryScannerTest(
|
|||
verify(exactly = 2) { mockScanner.scanRootFolder(any()) }
|
||||
verify(exactly = 1) { mockParser.parse(any()) }
|
||||
|
||||
val book = bookRepository.findAll().first()
|
||||
assertThat(book.metadata.status).isEqualTo(BookMetadata.Status.READY)
|
||||
assertThat(book.metadata.mediaType).isEqualTo("application/zip")
|
||||
assertThat(book.metadata.pages).hasSize(2)
|
||||
assertThat(book.metadata.pages.map { it.fileName }).containsExactly("1.jpg", "2.jpg")
|
||||
assertThat(book.lastModifiedDate).isNotEqualTo(book.createdDate)
|
||||
TransactionTemplate(transactionManager).execute {
|
||||
val book = bookRepository.findAll().first()
|
||||
assertThat(book.metadata.status).isEqualTo(BookMetadata.Status.READY)
|
||||
assertThat(book.metadata.mediaType).isEqualTo("application/zip")
|
||||
assertThat(book.metadata.pages).hasSize(2)
|
||||
assertThat(book.metadata.pages.map { it.fileName }).containsExactly("1.jpg", "2.jpg")
|
||||
assertThat(book.lastModifiedDate).isNotEqualTo(book.createdDate)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue