DB: Started update of entity loading to avoid global selects

Removes page/chpater addSelect global query, to load book slug, and
instead extracts base queries to be managed in new static class, while
updating specific entitiy relation loading to use our more efficient
MixedEntityListLoader where appropriate.

Related to 
This commit is contained in:
Dan Brown 2024-02-04 14:39:01 +00:00
parent 2460e7c56e
commit a70ed81908
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 115 additions and 41 deletions

@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
@ -14,11 +15,10 @@ use Illuminate\Database\Eloquent\Relations\Relation;
class ActivityQueries
{
protected PermissionApplicator $permissions;
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
public function __construct(
protected PermissionApplicator $permissions,
protected MixedEntityListLoader $listLoader,
) {
}
/**
@ -29,11 +29,13 @@ class ActivityQueries
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['user', 'entity'])
->with(['user'])
->skip($count * $page)
->take($count)
->get();
$this->listLoader->loadIntoRelations($activityList->all(), 'entity', false);
return $this->filterSimilar($activityList);
}

@ -5,6 +5,7 @@ namespace BookStack\App;
use BookStack\Activity\ActivityQueries;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Queries\RecentlyViewed;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Repos\BookRepo;
@ -26,9 +27,7 @@ class HomeController extends Controller
$draftPages = [];
if ($this->isSignedIn()) {
$draftPages = Page::visible()
->where('draft', '=', true)
->where('created_by', '=', user()->id)
$draftPages = PageQueries::currentUserDraftsForList()
->orderBy('updated_at', 'desc')
->with('book')
->take(6)
@ -40,11 +39,10 @@ class HomeController extends Controller
(new RecentlyViewed())->run(12 * $recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
$favourites = (new TopFavourites())->run(6);
$recentlyUpdatedPages = Page::visible()->with('book')
$recentlyUpdatedPages = PageQueries::visibleForList()
->where('draft', false)
->orderBy('updated_at', 'desc')
->take($favourites->count() > 0 ? 5 : 10)
->select(Page::$listAttributes)
->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@ -95,7 +93,7 @@ class HomeController extends Controller
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
/** @var Page $customHomepage */
$customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
$customHomepage = PageQueries::start()->where('draft', '=', false)->findOrFail($id);
$pageContent = new PageContent($customHomepage);
$customHomepage->html = $pageContent->render(false);

@ -18,20 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*/
abstract class BookChild extends Entity
{
protected static function boot()
{
parent::boot();
// Load book slugs onto these models by default during query-time
static::addGlobalScope('book_slug', function (Builder $builder) {
$builder->addSelect(['book_slug' => function ($builder) {
$builder->select('slug')
->from('books')
->whereColumn('books.id', '=', 'book_id');
}]);
});
}
/**
* Scope a query to find items where the child has the given childSlug
* where its parent has the bookSlug.

@ -3,10 +3,16 @@
namespace BookStack\Entities\Queries;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
abstract class EntityQuery
{
protected function mixedEntityListLoader(): MixedEntityListLoader
{
return app()->make(MixedEntityListLoader::class);
}
protected function permissionService(): PermissionApplicator
{
return app()->make(PermissionApplicator::class);

@ -0,0 +1,31 @@
<?php
namespace BookStack\Entities\Queries;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
class PageQueries
{
public static function start(): Builder
{
return Page::query();
}
public static function visibleForList(): Builder
{
return Page::visible()
->select(array_merge(Page::$listAttributes, ['book_slug' => function ($builder) {
$builder->select('slug')
->from('books')
->whereColumn('books.id', '=', 'pages.book_id');
}]));
}
public static function currentUserDraftsForList(): Builder
{
return static::visibleForList()
->where('draft', '=', true)
->where('created_by', '=', user()->id);
}
}

@ -10,7 +10,7 @@ class RecentlyViewed extends EntityQuery
public function run(int $count, int $page): Collection
{
$user = user();
if ($user === null || $user->isGuest()) {
if ($user->isGuest()) {
return collect();
}
@ -23,11 +23,13 @@ class RecentlyViewed extends EntityQuery
->orderBy('views.updated_at', 'desc')
->where('user_id', '=', user()->id);
return $query->with('viewable')
$views = $query
->skip(($page - 1) * $count)
->take($count)
->get()
->pluck('viewable')
->filter();
->get();
$this->mixedEntityListLoader()->loadIntoRelations($views->all(), 'viewable', false);
return $views->pluck('viewable')->filter();
}
}

@ -25,11 +25,13 @@ class TopFavourites extends EntityQuery
->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id);
return $query->with('favouritable')
$favourites = $query
->skip($skip)
->take($count)
->get()
->pluck('favouritable')
->filter();
->get();
$this->mixedEntityListLoader()->loadIntoRelations($favourites->all(), 'favouritable', false);
return $favourites->pluck('favouritable')->filter();
}
}

@ -26,7 +26,7 @@ class MixedEntityListLoader
* This will look for a model id and type via 'name_id' and 'name_type'.
* @param Model[] $relations
*/
public function loadIntoRelations(array $relations, string $relationName): void
public function loadIntoRelations(array $relations, string $relationName, bool $loadParents): void
{
$idsByType = [];
foreach ($relations as $relation) {
@ -40,7 +40,7 @@ class MixedEntityListLoader
$idsByType[$type][] = $id;
}
$modelMap = $this->idsByTypeToModelMap($idsByType);
$modelMap = $this->idsByTypeToModelMap($idsByType, $loadParents);
foreach ($relations as $relation) {
$type = $relation->getAttribute($relationName . '_type');
@ -56,7 +56,7 @@ class MixedEntityListLoader
* @param array<string, int[]> $idsByType
* @return array<string, array<int, Model>>
*/
protected function idsByTypeToModelMap(array $idsByType): array
protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents): array
{
$modelMap = [];
@ -67,10 +67,10 @@ class MixedEntityListLoader
$instance = $this->entityProvider->get($type);
$models = $instance->newQuery()
->select($this->listAttributes[$type])
->select(array_merge($this->listAttributes[$type], $this->getSubSelectsForQuery($type)))
->scopes('visible')
->whereIn('id', $ids)
->with($this->getRelationsToEagerLoad($type))
->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])
->get();
if (count($models) > 0) {
@ -100,4 +100,19 @@ class MixedEntityListLoader
return $toLoad;
}
protected function getSubSelectsForQuery(string $type): array
{
$subSelects = [];
if ($type === 'chapter' || $type === 'page') {
$subSelects['book_slug'] = function ($builder) {
$builder->select('slug')
->from('books')
->whereColumn('books.id', '=', 'book_id');
};
}
return $subSelects;
}
}

@ -23,7 +23,7 @@ class ReferenceFetcher
public function getReferencesToEntity(Entity $entity): Collection
{
$references = $this->queryReferencesToEntity($entity)->get();
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from');
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', true);
return $references;
}

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('views', function (Blueprint $table) {
$table->index(['updated_at'], 'views_updated_at_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('views', function (Blueprint $table) {
$table->dropIndex('views_updated_at_index');
});
}
};