Eloquent pod pokličkou: has()
Když skládáme dotaz pomocí Eloquentu, můžeme využít metodu has()
, která zkontroluje jestli existuje vazba oproti modelu, se kterým pracujeme. V tomto článku se proto na ni zaměříme, detailněji zjistíme jak pracuje na databázové úrovni a proč ji využívat.
Mějme jednoduchý příklad:
Pohled pak může vypadat například takto:
<ul>
@foreach ($users as $user) {
@if ($user->tasks->count())
<li>
{{ $user->name }} ({{ $user->tasks->count() }})
</li>
@endif
@endforeach
</ul>
I když tento příklad bude fungovat správně, znamená to však, že budeme vytahovat z databáze více dat než potřebujeme. Prvně totiž musíme provést ještě kontrolu/filtrování našeho výsledku před tím, než s ním můžeme dále pracovat. Jak si umíte jistě dále představit, pokud bychom tuto funkcionalitu potřebovali použít na dalších místech, stává se kód hůře udržovatelný a musíme všude zajistit stejnou konzistenci dat.
Jako řešení by se mohlo zdát zapouzdření této logiky a filtrování do samostatné funkce, takže bychom dosáhli snadné znovupoužitelnosti:
// App/User.php
public static function filterThoseWithTasks($users)
{
return $users->filter(function ($user) {
return $user->tasks->count() > 0;
});
}
Použitím této metody nemusíme využívat logiku v pohledu a tím pádem filtrace může být použita v celé aplikaci.
Co kdybychom ale jednoduše změnili náš databázový dotaz aby přímo vybíral pouze uživatele, kteří mají nějaký úkol...
Následující kód ukazuje, jak můžeme využít metodu has()
a tím upravit náš databázový dotaz, aby získal pouze uživatele s přidělenými úkoly.
User::has('tasks')->get();
Pokud bychom vykonali předchozí kód, získali bychom následující dotaz:
SELECT *
FROM "users"
WHERE EXISTS
(SELECT *
FROM "tasks"
WHERE "users"."id" = "tasks"."user_id")
Všimněte si přítomnosti klauzule WHERE
v dotazu, který by nebyl přítomen pokud bychom jednoduše chtěli získat všechny uživatele použitím User::all()
. Když se podíváme na přidaný poddotaz, vidíme že jsou vybrány všechny úkoly kde se user_id
úkolu shoduje s id
uživatele z vnějšího dotazu. Vidíme také klíčové slovo EXISTS
, což znamená, že je vybrán pouze uživatel, pokud existuje jeden nebo více úkolů v tabulce tasks
s user_id
patřící danému uživateli. Ve výsledku je tedy mnohem výhodnější filtrovat dataset než výsledek procházet v PHP.
Aktuálně se zaměříme pouze na první tři. První je samozřejmě název vztahu u kterého budeme hledat existenci, tak jako jsme udělali v předchozím příkladě. Na další dva se teď zaměříme více.
Nyní se podívejme na SQL dotaz, který se vykoná po úpravě:
SELECT *
FROM "users"
WHERE
(SELECT count(*)
FROM "tasks"
WHERE "users"."id" = "tasks"."user_id") < 10
Můžeme vidět, že se náš poddotaz mírně změnil a namísto toho využívá SQL funkce COUNT
k počtu úkolů, které mají shodné user_id
s daným uživatelem. Také si můžeme všimnout, že jsme specifikovali parametr $operator
, tedy jak se má hodnota v $count
porovnávat. To nám tak dává flexibilitu definovat scénář jak by vazba k úkolům měla existovat a podle toho jaké uživatelské záznamy vrátit.
Důležité je si zde všimnout, že naše WHERE
podmínka nyní využívá standardního porovnávání oproti počtu, namísto využití EXISTS
. V případě našeho prvního příkladu byl Eloquent natolik chytrý, že si uvědomil že nás nezajímá kolik úkolů existuje, prostě ho zajímalo pouze jestli nějaký úkol existuje. Díky tomu bylo v dotazu použito klíčové slovo EXISTS
, jelikož tato metoda těží z trochu vyšší rychlosti v porovnání s funkcí COUNT
. Nebudu moc zabíhat do detailu ohledně výkonu a jaké jsou výhody užití EXISTS
, nicméně v následující tabulce je ukázka rychlosti dotazu na základě našich předešlých příkladů. Ve zkratce MySQL zastaví proces EXISTS
ihned jakmile nalezne alespoň jednu shodu, zatímco COUNT
potřebuje projít všechny záznamy.
| METODA | DOTAZ | CAS |
|--------|--------------------------------------------------------------------------------------------------------|---------|
| EXISTS | SELECT * FROM "users" WHERE exists (SELECT * FROM "tasks" WHERE "users"."id" = "tasks"."user_id") | 2.150ms |
| COUNT | SELECT * FROM "users" WHERE (SELECT count(*) FROM "tasks" WHERE "users"."id" = "tasks"."user_id") < 10 | 9.010ms |
Samozřejmě, je to velmi jednoduchý příklad a vytahuje se pouze 50 uživatelů s 10 úkoly, avšak už teď můžeme vidět velké rozdíly.
public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
{
return $this->has($relation, '<', 1, $boolean, $callback);
}
public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'and', $callback);
}
public function orWhereHas($relation, Closure $callback, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or', $callback);
}
public function whereDoesntHave($relation, Closure $callback = null)
{
return $this->doesntHave($relation, 'and', $callback);
}