Pokročilejší vztahy – Laravel Query


Autor Pavel Zaněk

8. 8. 2022

Obsah článku

Pokud tvoříte aplikace v Laravelu, určitě jste se setkali se vztahy mezi modely. U jednoduchých aplikací mohou být tyto vztahy jednoduché, kolikrát nepřekročíte jednu úroveň vztahů. Například máte seznam států a k nim příslušející města. Mezi státy a městy nejspíše budete mít vztah OneHasMany/BelongsTo. Ale co když tvoříte daleko obsáhlejší aplikaci, kde například ještě pod danými městy máte další (klidně stejný) vztah vůči prodejnám (tedy každá město může mít neomezeně prodejen). A vy se například potřebujete podívat, v jakých státech existují prodejny s názvem XYZ. Tedy Vaše Query nepůjde pouze o jeden level do hloubky, ale rovnou o dva. Pojďme se spolu podívat, jak pomocí Laravelu můžete tvořit pokročilejší queries.

Vstupní data

Nejdříve je důležité si definovat vstupní data, resp. ukázková data, ze kterých si vyzkoušíme složit query. Pro zjednodušení si představte podobnou databázovou strukturu jako tabulky níže. Tuto strukturu si v rámci našeho MySQL/MaraDB vytvoříme.

Předpokládám, že jste si nainstalovali nebo máte po ruce framework Laravel. Pokud nemáte, můžete si jej nainstalovat.

idname
1Česká republika
2Slovensko
3Rakousko
Countries – Tabulka států/zemí

idcountry_idname
11Praha
21Brno
31Pardubice
42Bratislava
Cities – tabulka měst

idcity_idname
11KFC
21Subway
34KFC
Shops – tabulka prodejen

Vytvořme si tedy migrace.

Migrace zemí/států

<?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::create('countries', function (Blueprint $table) {
            $table->id();
            $table->string('name', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('countries');
    }
};

Migrace měst

<?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::create('cities', function (Blueprint $table) {
            $table->id();
            $table->foreignId('country_id')->constrained();
            $table->string('name', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('cities');
    }
};

Migrace obchodů

<?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::create('shops', function (Blueprint $table) {
            $table->id();
            $table->foreignId('city_id')->constrained();
            $table->string('name', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('shops');
    }
};

Seeder

Můžeme si také vytvořit seeder, a tak nemusíme vytvářet záznamy ručně. Jelikož jsem nechtěl využít faker, přeci jen jsem část vytvořil manuálně – pro lepší přehlednost v rámci této ukázky.

Upravme tedy DatabaseSeeder.php:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            CountryPackSeeder::class,
        ]);
    }
}

A vytvořme CountryPackSeeder.php:

<?php

namespace Database\Seeders;

use App\Models\City;
use App\Models\Country;
use App\Models\Shop;
use Illuminate\Database\Seeder;

class CountryPackSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $countries = [
            [
                'name' => 'Česká republika',
                'cities' => [
                    [
                        'name' => 'Praha',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Brno',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Pardubice',
                        'shops' => [
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                ]
            ],
            [
                'name' => 'Slovensko',
                'cities' => [
                    [
                        'name' => 'Bratislava',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Košice',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Prešov',
                        'shops' => [
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                ]
            ],
            [
                'name' => 'Polsko',
                'cities' => [
                    [
                        'name' => 'Varšava',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Krakov',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Lodž',
                        'shops' => [
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                ]
            ],
            [
                'name' => 'Rakousko',
                'cities' => [
                    [
                        'name' => 'Vídeň',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Linec',
                        'shops' => [
                            [
                                'name' => "McDonald's",
                            ],
                            [
                                'name' => "KFC",
                            ],
                        ]
                    ],
                    [
                        'name' => 'Salzburg',
                        'shops' => [
                            [
                                'name' => "KFC",
                            ],
                            [
                                'name' => "Subway",
                            ],
                        ]
                    ],
                ]
            ],
        ];

        foreach ($countries as $country) {
            $newCountry = Country::updateOrCreate(['name' => $country['name']], [
                'name' => $country['name']
            ]);

            foreach($country['cities'] as $city) {
                $newCity = City::updateOrCreate(
                    [
                        'country_id' => $newCountry->id,
                        'name' => $city['name']
                    ],
                    [
                        'country_id' => $newCountry->id,
                        'name' => $city['name']
                    ]
                );

                foreach($city['shops'] as $shop) {
                    Shop::updateOrCreate(
                        [
                            'city_id' => $newCity->id,
                            'name' => $shop['name']
                        ],
                        [
                            'city_id' => $newCity->id,
                            'name' => $shop['name']
                        ]
                    );
                }
            }
        }
    }
}

Nezapomeňte spustit migraci.

sail artisan db:seed --class=CountryPackSeeder

Tím máme data v databázi a můžeme vytvářet zajímavé dotazy.

Modely

Ještě, než začneme tvořit dotaz/querynu, podíváme se na jednotlivé modely.

Country Model

<?php

namespace App\Models;

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

class Country extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    public function cities()
    {
        return $this->hasMany(City::class);
    }
}

City Model

<?php

namespace App\Models;

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

class City extends Model
{
    use HasFactory;

    protected $fillable = [
        'country_id',
        'name',
    ];

    public function shops()
    {
        return $this->hasMany(Shop::class);
    }

    public function country()
    {
        return $this->belongsTo(Country::class);
    }
}

Shop Model

<?php

namespace App\Models;

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

class Shop extends Model
{
    use HasFactory;

    protected $fillable = [
        'city_id',
        'name',
    ];

    public function city()
    {
        return $this->belongsTo(City::class);
    }
}

Routy, controller, view

V rychlosti ještě můžeme přidat kontroler, např. AdvancedQueriesController.php.

<?php

namespace App\Http\Controllers;

use App\Models\Country;
use Illuminate\Http\Request;

class AdvancedQueriesController extends Controller
{
    public function countriesWithShopsCount()
    {
        $countries = Country::all();
        return view('advanced-queries.countries-with-shops-count', compact('countries'));
    }
}

Kontroler si naroutujeme.

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get(
    '/advanced-queries',
    [
        \App\Http\Controllers\AdvancedQueriesController::class,
        'countriesWithShopsCount'
    ]
)->name('advanced-queries');

A vytvoříme pohled/view.

<table border="1">
    <thead>
        <tr>
            <td>Country</td>
            <td>Shops</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($countries as $country)
            <tr>
                <td>{{ $country->name }}</td>
                <td>How to query shops count?</td>
            </tr>
        @endforeach
    </tbody>
</table>

Výsledkem byste měli na url /advanced-queries vidět tabulku podobnou této:

Výchozí tabulka

Dotazy z praxe

Podíváme se na ukázky kódu, které celkem dost používám a vidím je i v ostatních projektech.

1) Počet obchodů v jednotlivých zemích

Tedy potřebujeme získat tabulku, kde v jednom sloupci budou uvedeny země/státy a ve druhém počet obchodů. Hlavně se prosím vyvarujme následujícímu (ani jsem netestoval, zda by vůbec fungovalo):

v kontrolleru například:
$countries = Country::with(['cities', 'cities.shops'])->get();
...
a pak někde v blade.php:
@foreach($countries as $country)
    <td>{{ $country->name }}</td>
    <td>
        @php $myCount = 0; @endphp
        @foreach($country->cities as $city)
            @php $myCount += count($city->shops); @endphp
        @endforeach
        {{ $myCount }}
    </td>
@endforeach

Jde o to, že:

  1. není to profesionální
  2. není to optimalizované
  3. je to pomalé
  4. zkrátka odpad… Nemůžete kvůli pár datům žádat databázi o všechna data, která jsou související se státy… 🙂

Jak tedy na to?

Musíme se ptát vztahu, který nepřímo souvisí se státy, tedy Country -> City -> Shop.

HasManyThrough

Jedna z možností, jak získat počet obchodů dle zemí je v použití vztahu hasManyThrough.

Náš Country Model si doplníme o následující metodu:

...
public function shops()
{
    return $this->hasManyThrough(Shop::class, City::class);
}
...

V kontroleru můžeme použít následující:

...
public function countriesWithShopsCount()
{
    $countries = Country::with('shops')->get();
    return view('advanced-queries.countries-with-shops-count', compact('countries'));
}
...

A nakonec pohled/view můžeme upravit, aby obsahoval počet obchodů:

<table border="1">
    <thead>
        <tr>
            <td>Country</td>
            <td>Shops</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($countries as $country)
            <tr>
                <td>{{ $country->name }}</td>
                <td>{{ $country->shops->count() }}</td>
            </tr>
        @endforeach
    </tbody>
</table>
Výsledek pomocí vztahu hasManyThrough
Výsledek pomocí vztahu hasManyThrough

Když se podíváme detailněji, tento postup vygeneroval tyto SQL dotazy:

select * from `countries`
select `shops`.*, `cities`.`country_id` as `laravel_through_key` from `shops` inner join `cities` on `cities`.`id` = `shops`.`city_id` where `cities`.`country_id` in (1, 2, 3, 4)
Has Many Through nám vygeneroval 2 SQL dotazy
Has Many Through nám vygeneroval 2 SQL dotazy

Optimalizace dotazu

Pojďme náš dotaz více optimalizovat. Jelikož nás u obchodů zajímá pouze počet, nemusíme získávat veškerá data z tabulky shops. Upravme tedy ještě nás kontroler:

...
public function countriesWithShopsCount()
{
    $countries = Country::withCount('shops')->get();
    return view('advanced-queries.countries-with-shops-count', compact('countries'));
}
...
<table border="1">
    <thead>
        <tr>
            <td>Country</td>
            <td>Shops</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($countries as $country)
            <tr>
                <td>{{ $country->name }}</td>
                <td>{{ $country->shops_count }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

Díky tomu jsme se dostaly pouze na jeden SQL dotaz:

select `countries`.*, (select count(*) from `shops` inner join `cities` on `cities`.`id` = `shops`.`city_id` where `countries`.`id` = `cities`.`country_id`) as `shops_count` from `countries`
Has Many Through spolus použitím withCount nám vygeneroval 1 SQL dotaz
Has Many Through spolus použitím withCount nám vygeneroval 1 SQL dotaz

2) Obchody a k nim příslušné země/státy

Opět se budeme ptát databáze – konkrétně chceme získat obchody a k nim související země (Obchod->Město->Země/stát).

Nejdříve si pojďme připravit kontroler, routu a pohled/view.

Do kontroleru přidáme novou metodu:

...
public function shopsWithCountries()
{
    $shops = Shop::all();
    return view('advanced-queries.shops-with-countries', compact('shops'));
}
...

Nyní si vytvoříme příslušný pohled/view:

<table border="1">
    <thead>
        <tr>
            <td>Shop</td>
            <td>Country</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($shops as $shop)
            <tr>
                <td>{{ $shop->name }}</td>
                <td>How to query country name?</td>
            </tr>
        @endforeach
    </tbody>
</table>

A naroutujeme:

...
Route::get(
    '/advanced-queries/shops-with-countries',
    [
        \App\Http\Controllers\AdvancedQueriesController::class,
        'shopsWithCountries'
    ]
)->name('advanced-queries.shops-with-countries');
...

Díky tomu získáme přibližně podobnou tabulku:

Pokročilé vztahy – obchody s názvem státu/země

Možná Vás napadne, a díky struktuře Modelů to nejspíše bude i fungovat, že na pohledu/view použijete toto:

<table border="1">
    <thead>
        <tr>
            <td>Shop</td>
            <td>Country</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($shops as $shop)
            <tr>
                <td>{{ $shop->name }}</td>
                <td>{{ $shop->city->country->name }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

Tato možnost Vám však vygeneruje množství SQL dotazů, což není vůbec ideální.

Ukázka pouze pár dotazů, které se vygenerovaly

Logiku však můžeme optimalizovat – stačí upravit metodu v kontroleru:

...
public function shopsWithCountries()
{
    $shops = Shop::with('city.country')->get();
    return view('advanced-queries.shops-with-countries', compact('shops'));
}
...

Pomocí této úpravy jsme docílili toho, že se vygenerovaly pouze 3 SQL dotazy.

Pomocí with('city.country') se nám vygenerovaly pouze 3 SQL dotazy
Pomocí with(‚city.country‘) se nám vygenerovaly pouze 3 SQL dotazy

Konkrétně se jedná o tyto dotazy:

select * from `shops`
select * from `cities` where `cities`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
select * from `countries` where `countries`.`id` in (1, 2, 3, 4)

Jak si můžete všimnout, stále dostáváme data, která bychom nepotřebovali. Ano, mohli bychom použít select, ale zkusme ještě více tuto logiku optimalizovat.

Bohužel Laravel obsahuje pouze vztah hasManyThrough. V tomto případě potřebujeme pravý opak, tedy vztah belongsToThrough. Můžeme si pomoci balíčkem/package staudenmeir / belongs-to-through od Jonase Staudenmeira.

Po nainstalování balíčku rozšíříme nás Shop Model.

<?php

namespace App\Models;

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

class Shop extends Model
{
    use HasFactory;
    use \Znck\Eloquent\Traits\BelongsToThrough;

    protected $fillable = [
        'city_id',
        'name',
    ];

    public function city()
    {
        return $this->belongsTo(City::class);
    }

    public function country()
    {
        return $this->belongsToThrough(Country::class, City::class);
    }
}

Tedy doplnili jsme traitu a přidali metodu country obsahující belongsToThrough. Nyní ještě upravíme náš kontroler.

...
public function shopsWithCountries()
{
    $shops = Shop::with('country')->get();
    return view('advanced-queries.shops-with-countries', compact('shops'));
}
...

Už se ptáme pouze na vztah country, nikoliv city.country. City můžeme vynechat i v našem pohledu/view:

<table border="1">
    <thead>
        <tr>
            <td>Shop</td>
            <td>Country</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($shops as $shop)
            <tr>
                <td>{{ $shop->name }}</td>
                <td>{{ $shop->country->name }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

Jak si můžete všimnout, dostali jsme se na dva SQL dotazy.

využití belongsToThrough pomocí balíčku staudenmeir / belongs-to-through od Jonase Staudenmeira
využití belongsToThrough pomocí balíčku staudenmeir / belongs-to-through od Jonase Staudenmeira
select * from `shops`
select `countries`.*, `cities`.`id` as `laravel_through_key` from `countries` inner join `cities` on `cities`.`country_id` = `countries`.`id` where `cities`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)

Díky tomu jsme úplně vypustili data v rámci měst (city). Takto jsme během pár kroků optimalizovali aplikaci a Vám se tak opět o trochu rychleji bude načítat 🙂

Tip na závěr – další úroveň vztahů

Někdy se však můžete setkat, že potřebujete jít ještě více do hloubky v rámci vztahů. Z ukázky výše jsme se pohybovali ve třech úrovních (např. shop->city->country). Co když nám však přibude další úroveň?

Zkusme se podívat na případ, kdy by pod obchodem byli ještě zaměstnanci (employees).

Migrace

<?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::create('employees', function (Blueprint $table) {
            $table->id();
            $table->foreignId('shop_id')->constrained();
            $table->string('name', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('employees');
    }
};

Factory

Pro zaměstnance si vytvoříme jednoduchou továrnu s testovacími daty.

<?php

namespace Database\Factories;

use App\Models\Shop;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Employee>
 */
class EmployeeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'shop_id' => Shop::inRandomOrder()->first()->id,
            'name' => fake()->lastName(),
        ];
    }
}

Seeder

Upravíme si naše seedery. Nejdříve soubor si vytvoříme EmployeesSeeder.php.

<?php

namespace Database\Seeders;

use App\Models\Employee;
use Illuminate\Database\Seeder;

class EmployeesSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Employee::factory()->count(200)->create();
    }
}

Dále přidáme náš nový seeder do DatabaseSeeder.php.

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            CountryPackSeeder::class,
            EmployeesSeeder::class,
        ]);
    }
}

Model

Také je zapotřebí vytvořit model a nastavit mu vztah (obchod <-> zaměstnanec).

<?php

namespace App\Models;

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

class Employee extends Model
{
    use HasFactory;

    protected $fillable = [
        'shop_id',
        'name',
    ];

    public function shop()
    {
        return $this->belongsTo(Shop::class);
    }
}

Úpravu uděláme i v modelu Shop, kde uvedeme nový vztah na Employee.

...
public function employees()
{
    return $this->hasMany(Employee::class);
}
...

Příklad z praxe – Zobrazení tabulky s počtem zaměstnanců v jednotlivých zemích

V tomto případě se budeme pohybovat do čtvrté úrovně, tedy Stát/země->Město->Obchod->Zaměstnanci. Opět se budeme snažit o co nejlepší možnou optimalizaci SQL dotazu. Než začneme, připravíme si nový pohled, který přidáme do kontroleru a opět naroutujeme.

Kontroler

Můžeme si upravit náš AdvancedQueriesController.php a přidáme do něj novou metodu, která nám bude vracet view.

...
public function countriesWithEmployeesCount()
{
    $countries = Country::all();
    return view('advanced-queries.countries-with-employees-count', compact('countries'));
}
...

Pohled/View

<table border="1">
    <thead>
        <tr>
            <td>Country</td>
            <td>Employees</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($countries as $country)
            <tr>
                <td>{{ $country->name }}</td>
                <td>How to query employees count?</td>
            </tr>
        @endforeach
    </tbody>
</table>

Router

Ještě nám zbývá naroutovat metodu z kontroleru na nějakou url adresu.

...
Route::get(
    '/advanced-queries/countries-with-employees-count',
    [
        \App\Http\Controllers\AdvancedQueriesController::class,
        'countriesWithEmployeesCount'
    ]
)->name('advanced-queries.countries-with-employees-count');
...

Jak získat počet zaměstnanců?

Celkem jednoduchá otázka, složitější odpověď. Laravel Eloquent bohužel ani v tomto případě není dostačující na zvládnutí této otázky jednodušší cestou. Máme však řešení, které nám pomůže. Konkrétně se řešení nachází opět u Jonase Staudenmeira, resp. v jeho vytvořeném balíčku staudenmeir / eloquent-has-many-deep.

Po nainstalování balíčku upravme model City:

<?php

namespace App\Models;

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

class Country extends Model
{
    use HasFactory;
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    protected $fillable = [
        'name',
    ];

    public function cities()
    {
        return $this->hasMany(City::class);
    }

    public function shops()
    {
        return $this->hasManyThrough(Shop::class, City::class);
    }

    public function employees()
    {
        return $this->hasManyDeep(Employee::class, [City::class, Shop::class]);
    }
}

V kontroleru, abychom co nejvíce optimalizovali, využijeme withCount(), který jsme si ukázali dříve v tomto článku:

...
public function countriesWithEmployeesCount()
{
    $countries = Country::withCount('employees')->get();
    return view('advanced-queries.countries-with-employees-count', compact('countries'));
}
...

Pohled upravíme o zobrazování počtu:

<table border="1">
    <thead>
        <tr>
            <td>Country</td>
            <td>Employees</td>
        </tr>
    </thead>
    <tbody>
        @foreach ($countries as $country)
            <tr>
                <td>{{ $country->name }}</td>
                <td>{{ $country->employees_count }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

Výsledek by měl vypadat podobně (jelikož jsme použili továrnu, která generuje zaměstnance u ochodů náhodně, počty se mohu lišit).

Výsledek pomocí hasManyDeep - pokročile Laravel vztahy
Výsledek pomocí hasManyDeep – pokročile Laravel vztahy

Tímto jsme docílili, že se vygeneroval pouze jeden SQL dotaz.

select `countries`.*, (select count(*) from `employees` inner join `shops` on `shops`.`id` = `employees`.`shop_id` inner join `cities` on `cities`.`id` = `shops`.`city_id` where `countries`.`id` = `cities`.`country_id`) as `employees_count` from `countries`
Laravel použil pouze jeden dotaz na databázi s použitím hasManyDeep
Laravel použil pouze jeden dotaz na databázi s použitím hasManyDeep

Závěrem

Myslete na to, jak data načítáte a dbejte na to, abyste nezatěžovali server. I z bezpečnostních důvodů není potřeba kolikrát sahat po všech datech, ale jen po vybraných. Nechtějte z databáze data, která ani nevyužijete. Pomůže Vám to v rámci optimalizace aplikace (rychlost).

Další články

Autor Pavel Zaněk

Tvořím webové stránky s velkou oblibou v online marketingu - především v odvětví optimalizace pro vyhledávače (SEO). Především se specializuji na tvorbu webových aplikací na míru (PHP framework Laravel s využitím dalších moderních technologií) a na tvorbu webových stránek s použitím redakčního systému Wordpress. Ačkoliv se více zaměřuji na logiku webových aplikací (back end), jsem schopný tvořit i moderní a responzivní šablony v rámci webové grafiky (front end).

8. 8. 2022