N +1 on Select input with relationship when used within a repeater
Package
filament/filament
Package Version
4.1.6
Laravel Version
12.33
Livewire Version
3.6.4
PHP Version
8.3
Problem description
When adding a Select input with relationship() method (many to many) inside a repeater, instead of eager loading all the items' filament retrieves the relationships one by one. the result is n+1 problem. Below i am attaching a screenshot from the laravel debugbar in a simple filament form.
For the context, i have created a sample project using 3 models. Project, Task, and User.
I added a repeater within the project resource which has 2 fields. The task name, and the Select element which relates to the many to many task-user relationship.
Expected behavior
It should eager load the many to many relationship between Task and User models.
Steps to reproduce
download the project. run composer install, copy .env.example to .env, run php artisan key:generate, run php artisan db:seed, run php artisan make:filament-user and enter the user credentials. open the /admin/projects user. click a project and open laravel debugbar panel.
Reproduction repository (issue will be closed if this is not valid)
https://github.com/GeorgeFourkas/filament_repeater_issue
Relevant log output
It's my two cents, but I believe the relationship() function of Select is responsible for querying the relationship, since it also accepts query builder modifications in the form of the $modifyQueryUsing parameter, which would make no sense it used a collection rather than a query.
If you want the Select to use a custom logic with preloaded values, you can use something like this (even though it's not the nicest solution imo):
// App\Models\Task.php
// ...
protected $with = ['users'];
// ...
// App\Filament\Resources\Projects\Schemas\ProjectForm.php
// Code for repeater...
->schema([
TextInput::make('name'),
Select::make('users')
->multiple()
->options(User::pluck('name', 'id'))
->afterStateHydrated(function ($component, $state, $record) {
$component->state($record->users->pluck('id')->toArray());
})
->saveRelationshipsUsing(function ($component, $state, $record) {
$record->users()->sync($state ?? []);
$record->load('users');
}),
]),
Yeah, I'm on the edge about if this is a bug or not. I think there are some situations where the relation is loaded and there is no modifyQueryUsing that we could use the eager-loaded records from the model instead of querying again. But there would also be preloaded options queries to optimize too which couldn't be eager loaded with with().
Hello, thank you for taking the time to look into this :) Well i implemented a solution kinda like the one @balintcodes suggested, while it doesnt load the relationships one by one when the form loads, it does that when we save the form, but its a workaround.
I was also on the edge about that, if its a bug or not, but since it requires a more complex fix to avoid N+1 (which in the end you do not avoid it, you just delay it) i thought i should report it as a bug on the Select input