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:
Gauthier Roebroeck 2019-12-17 17:05:07 +08:00
parent 4603049012
commit 02361e154f
9 changed files with 75 additions and 26 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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
}

View file

@ -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>
}

View file

@ -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 {

View file

@ -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(

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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