laravel / nova-issues

556 stars 34 forks source link

MorphTo Too Slow to Load Relationship #3708

Closed mahfoudfx closed 2 years ago

mahfoudfx commented 2 years ago

Description: When I use InlineCreate for a polymorphic relation using the morphTo, the field takes long time to load the relationship. At the begining, the issue was not visible, but when the DB reached 12K entries, it takes up to 20s to load.

MorphTo::make(__('Survey'), 'analysissurveyable')->types([pcrannex4::class])->searchable()->showCreateRelationButton()->hideFromIndex()

image

When I investigate the issue, I found that is similair to #2391.

I have dug deeper and followed the generated route by Nova: https://nova.test/nova-api/analyses/morphable/analysissurveyable?type=pcrannex4s&current=13296&first=false&search=&withTrashed=false&viaResource=&viaResourceId=&viaRelationship=&editing=true&editMode=create I found that it requested all the records with format: {"resources":[{"display":"Annexe 4","value":13296},{"display":"Annexe 4","value":13295},{"display":"Annexe 4","value":13294},{"display":"Annexe 4","value":13293}..........

Analyzing this route and the result, I suspected the part &first=false, which I have changed to &first=true and it returned the last inserted relation. So, even if the field is ->searchable(), the generated route is wrong. Continuing the investigation by searching for morphable, I found MorphToField.vue which contains this part that (line 383) I suspect is responsible to generate the concerned route.

shouldLoadFirstResource() {
      return (
        this.isSearchable &&
        this.shouldSelectInitialResource &&
        this.initializingWithExistingResource
      )
    },

    /**
     * Get the query params for getting available resources
     */
    queryParams() {
      return {
        params: {
          type: this.resourceType,
          current: this.selectedResourceId,
          first: this.shouldLoadFirstResource,
          search: this.search,
          withTrashed: this.withTrashed,
          viaResource: this.viaResource,
          viaResourceId: this.viaResourceId,
          viaRelationship: this.viaRelationship,
          editing: true,
          editMode:
            _.isNil(this.resourceId) || this.resourceId === ''
              ? 'create'
              : 'update',
        },

I didn't go deeper than that, but I suspect that the condition in the function shouldLoadFirstResource() will always return false and maybe revised to

shouldLoadFirstResource() {
      return (
        this.isSearchable &&
        this.shouldSelectInitialResource OR
        this.initializingWithExistingResource
      )
    },

Please confirm if this investigation is correct, will solve this issue and will not generate a conflict with other features especially in a multi-user environment.

Detailed steps to reproduce the issue on a fresh Nova installation:

Just use MorphTo with more than 10K entries relation table.

crynobone commented 2 years ago

Please provide full reproducing code

mahfoudfx commented 2 years ago

Please provide full reproducing code

Which parts of code you exactly need?

mahfoudfx commented 2 years ago

I have checked with fresh installation of Nova 3.30.0 and the issue remains the same. When creating a relation via MorphTo, the generated route is https://nova.test/nova-api/analyses/morphable/analysissurveyable?type=pcrannexs&current=13302&first=false&search=&withTrashed=false&viaResource=&viaResourceId=&viaRelationship=&editing=true&editMode=create, and the part &first=false remains false regardless the Create or Edit operation. This route will return all the records, which in the case of +10K entry will take several seconds to load the relation.

To reproduce this, as I mentionned before, just use 2 models with a Polymorphic relation. I have reproduced it on my project by the simplifed following two models: Analysis and Pcrannex and re-generated their Nova Resources.

PS: For the Model 'Analysis' I've also tried without the use of Laravel\Nova\Actions\Actionable to avoid any suspicion regarding this class.

Models Analysis Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

use Laravel\Nova\Actions\Actionable;

class Analysis extends Model
{
    use HasFactory;
    use Actionable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'analysistype_id',
        'patient_id',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'id' => 'integer',
        'analysistype_id' => 'integer',
        'patient_id' => 'integer',
    ];

    public function analysistype()
    {
        return $this->belongsTo(\App\Models\Analysistype::class);
    }

    public function patient()
    {
        return $this->belongsTo(\App\Models\Patient::class);
    }

    public function analysissurveyable()
    {
        return $this->morphTo();
    }
}

Pcrannex Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Pcrannex extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'purpose',
        'suspect_contact',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'id' => 'integer',
        'purpose' => 'integer',
        'suspect_contact' => 'integer',
    ];

    public function analysissurvey()
    {
        return $this->morphOne(\App\Models\Analysis::class, 'analysissurveyable');
    }
}

Nova Resources Analysis Nova Resource

<?php

namespace App\Nova;

use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\MorphTo;
use Laravel\Nova\Http\Requests\NovaRequest;

class Analysis extends Resource
{
    /**
     * The model the resource corresponds to.
     *
     * @var string
     */
    public static $model = \App\Models\Analysis::class;

    /**
     * The single value that should be used to represent the resource when being displayed.
     *
     * @var string
     */
    public static $title = 'id';

    /**
     * The columns that should be searched.
     *
     * @var array
     */
    public static $search = [
        'id',
    ];

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),
            BelongsTo::make(__('Type'), 'analysistype', Analysistype::class)
                  ->sortable()->withoutTrashed()->rules('required'),
            BelongsTo::make(__('Patient'), 'patient', Patient::class)
                  ->sortable()->searchable()->withSubtitles()->withoutTrashed()
                  ->showCreateRelationButton()->rules('required'),
            MorphTo::make(__('Survey'), 'analysissurveyable')->types([Pcrannex::class])
                  ->searchable()->showCreateRelationButton()->hideFromIndex()
        ];
    }

    /**
     * Get the cards available for the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function cards(Request $request)
    {
        return [];
    }

    /**
     * Get the filters available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function filters(Request $request)
    {
        return [];
    }

    /**
     * Get the lenses available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function lenses(Request $request)
    {
        return [];
    }

    /**
     * Get the actions available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function actions(Request $request)
    {
        return [];
    }
}

Pcrannex Nova Resource

<?php

namespace App\Nova;

use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Http\Requests\NovaRequest;

class Pcrannex extends Resource
{
    /**
     * The model the resource corresponds to.
     *
     * @var string
     */
    public static $model = \App\Models\Pcrannex::class;

    /**
     * The single value that should be used to represent the resource when being displayed.
     *
     * @var string
     */
    public static $title = 'id';

    /**
     * The columns that should be searched.
     *
     * @var array
     */
    public static $search = [
        'id',
    ];

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),
            Select::make(__('Purpose'), 'purpose')
                  ->options(['1' => __('Diagnosis'), '2' => __('Screening'), '3' => __('Travel')])
                  ->displayUsingLabels()->rules('required'),
            Select::make(__('Suspect contact'), 'suspect_contact')
                  ->options(['0' => __('Unkown'), '1' => __('Suspect')])
                  ->rules('required')->displayUsingLabels(),
        ];
    }

    /**
     * Get the cards available for the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function cards(Request $request)
    {
        return [];
    }

    /**
     * Get the filters available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function filters(Request $request)
    {
        return [];
    }

    /**
     * Get the lenses available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function lenses(Request $request)
    {
        return [];
    }

    /**
     * Get the actions available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function actions(Request $request)
    {
        return [];
    }
}
crynobone commented 2 years ago

I can only reproduce the bug without ->searchable() and even with a fix, it'll still load all information on editing the morph to relation.

If you still having issue with searchable() I would assume you have an issue somewhere else and I don't have enough information to solve it.

mahfoudfx commented 2 years ago

I tested again with a fresh installation of Laravel and Nova as mentioned above, with only these models, with freshly generated resources using php artisan nova:resource exactly as described above and I still get &first=false. The only quick fix that I found, is to hardcode shouldLoadFirstResource:function(){return true} on the public/vendor/nova/app.js.

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

crynobone commented 2 years ago

Fixed in v3.31.0: https://nova.laravel.com/releases/3.31.0