Merge pull request #12336 from snipe/features/category_query_optimization

Refactor category API for fewer queries
This commit is contained in:
snipe 2023-01-10 17:54:45 -08:00 committed by GitHub
commit 942681bee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 52 additions and 8 deletions

View file

@ -10,6 +10,7 @@ use App\Models\Category;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
class CategoriesController extends Controller class CategoriesController extends Controller
{ {
@ -107,7 +108,7 @@ class CategoriesController extends Controller
public function show($id) public function show($id)
{ {
$this->authorize('view', Category::class); $this->authorize('view', Category::class);
$category = Category::findOrFail($id); $category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
return (new CategoriesTransformer)->transformCategory($category); return (new CategoriesTransformer)->transformCategory($category);
} }
@ -126,8 +127,14 @@ class CategoriesController extends Controller
{ {
$this->authorize('update', Category::class); $this->authorize('update', Category::class);
$category = Category::findOrFail($id); $category = Category::findOrFail($id);
// Don't allow the user to change the category_type once it's been created
if (($request->filled('category_type')) && ($category->category_type != $request->input('category_type'))) {
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.update.cannot_change_category_type'))
);
}
$category->fill($request->all()); $category->fill($request->all());
$category->category_type = strtolower($request->input('category_type'));
$category = $request->handleImages($category); $category = $request->handleImages($category);
if ($category->save()) { if ($category->save()) {
@ -148,7 +155,7 @@ class CategoriesController extends Controller
public function destroy($id) public function destroy($id)
{ {
$this->authorize('delete', Category::class); $this->authorize('delete', Category::class);
$category = Category::findOrFail($id); $category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
if (! $category->isDeletable()) { if (! $category->isDeletable()) {
return response()->json( return response()->json(

View file

@ -22,6 +22,26 @@ class CategoriesTransformer
public function transformCategory(Category $category = null) public function transformCategory(Category $category = null)
{ {
// We only ever use item_count for categories in this transformer, so it makes sense to keep it
// simple and do this switch here.
switch ($category->category_type) {
case 'asset':
$category->item_count = $category->assets_count;
break;
case 'accessory':
$category->item_count = $category->accessories_count;
break;
case 'consumable':
$category->item_count = $category->consumables_count;
break;
case 'component':
$category->item_count = $category->components_count;
break;
default:
$category->item_count = 0;
}
if ($category) { if ($category) {
$array = [ $array = [
'id' => (int) $category->id, 'id' => (int) $category->id,
@ -33,7 +53,7 @@ class CategoriesTransformer
'eula' => ($category->getEula()), 'eula' => ($category->getEula()),
'checkin_email' => ($category->checkin_email == '1'), 'checkin_email' => ($category->checkin_email == '1'),
'require_acceptance' => ($category->require_acceptance == '1'), 'require_acceptance' => ($category->require_acceptance == '1'),
'item_count' => (int) $category->itemCount(), 'item_count' => (int) $category->item_count,
'assets_count' => (int) $category->assets_count, 'assets_count' => (int) $category->assets_count,
'accessories_count' => (int) $category->accessories_count, 'accessories_count' => (int) $category->accessories_count,
'consumables_count' => (int) $category->consumables_count, 'consumables_count' => (int) $category->consumables_count,

View file

@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
use App\Helpers\Helper; use App\Helpers\Helper;
use Illuminate\Support\Str;
/** /**
* Model for Categories. Categories are a higher-level group * Model for Categories. Categories are a higher-level group
@ -97,6 +98,7 @@ class Category extends SnipeModel
*/ */
public function isDeletable() public function isDeletable()
{ {
return Gate::allows('delete', $this) return Gate::allows('delete', $this)
&& ($this->itemCount() == 0); && ($this->itemCount() == 0);
} }
@ -150,7 +152,10 @@ class Category extends SnipeModel
} }
/** /**
* Get the number of items in the category * Get the number of items in the category. This should NEVER be used in
* a collection of categories, as you'll end up with an n+1 query problem.
*
* It should only be used in a single category context.
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0] * @since [v2.0]
@ -158,6 +163,11 @@ class Category extends SnipeModel
*/ */
public function itemCount() public function itemCount()
{ {
if (isset($this->{Str::plural($this->category_type).'_count'})) {
return $this->{Str::plural($this->category_type).'_count'};
}
switch ($this->category_type) { switch ($this->category_type) {
case 'asset': case 'asset':
return $this->assets()->count(); return $this->assets()->count();
@ -169,9 +179,10 @@ class Category extends SnipeModel
return $this->consumables()->count(); return $this->consumables()->count();
case 'license': case 'license':
return $this->licenses()->count(); return $this->licenses()->count();
default:
return 0;
} }
return '0';
} }
/** /**

View file

@ -13,7 +13,8 @@ return array(
'update' => array( 'update' => array(
'error' => 'Category was not updated, please try again', 'error' => 'Category was not updated, please try again',
'success' => 'Category updated successfully.' 'success' => 'Category updated successfully.',
'cannot_change_category_type' => 'You cannot change the category type once it has been created',
), ),
'delete' => array( 'delete' => array(

View file

@ -15,11 +15,16 @@
<div class="form-group {{ $errors->has('category_type') ? ' has-error' : '' }}"> <div class="form-group {{ $errors->has('category_type') ? ' has-error' : '' }}">
<label for="category_type" class="col-md-3 control-label">{{ trans('general.type') }}</label> <label for="category_type" class="col-md-3 control-label">{{ trans('general.type') }}</label>
<div class="col-md-7 required"> <div class="col-md-7 required">
{{ Form::select('category_type', $category_types , old('category_type', $item->category_type), array('class'=>'select2', 'style'=>'min-width:350px', 'aria-label'=>'category_type', $item->itemCount() > 0 ? 'disabled' : '')) }} {{ Form::select('category_type', $category_types , old('category_type', $item->category_type), array('class'=>'select2', 'style'=>'min-width:350px', 'aria-label'=>'category_type', ($item->category_type!='') || ($item->itemCount() > 0) ? 'disabled' : '')) }}
{!! $errors->first('category_type', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!} {!! $errors->first('category_type', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div> </div>
<div class="col-md-7 col-md-offset-3">
<p class="help-block">{!! trans('admin/categories/message.update.cannot_change_category_type') !!} </p>
</div>
</div> </div>
<!-- EULA text --> <!-- EULA text -->
<div class="form-group {{ $errors->has('eula_text') ? 'error' : '' }}"> <div class="form-group {{ $errors->has('eula_text') ? 'error' : '' }}">
<label for="eula_text" class="col-md-3 control-label">{{ trans('admin/categories/general.eula_text') }}</label> <label for="eula_text" class="col-md-3 control-label">{{ trans('admin/categories/general.eula_text') }}</label>