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 #4823
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

View File

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

View File

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

View File

@ -18,20 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*/ */
abstract class BookChild extends Entity 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 * Scope a query to find items where the child has the given childSlug
* where its parent has the bookSlug. * where its parent has the bookSlug.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
});
}
};