Wykonujemy ćwiczenie i wypełniamy pewne braki w wiedzy, jakie jeszcze mamy (limit, offset), uczymy się jak „pod spodem” działa metoda withAvg. Do dzieła.
Ok, najpierw coś prostego:
Artisan::command('movies-with-avg123', function () {
$films = Movie::withAvg('reviews', 'rating')->take(5)->get();
foreach($films as $m){
$this->comment("Movie title: {$m->title} Reviews avg rating: {$m->reviews_avg_rating}");
}
});
To już powinniśmy znać, ale take(5) oznacza limit 5 (daj tylko pierwsze 5 wyników). Bierzemy filmy ze średnią ocen ich recenzji.
Ok, teraz z offsetem:
Artisan::command('movies-with-avg-offset', function () {
$films = Movie::withAvg('reviews', 'rating')->offset(5)->take(5)->get();
foreach($films as $m){
$this->comment("Movie title: {$m->title} Reviews avg rating: {$m->reviews_avg_rating}");
}
});
W czystym PHP robiliśmy framework z paginacją, powinniśmy ogarniać o co chodzi.
Dobra, napiszmy sami sobie to withAvg w czystym SQL:
select m.*,
(
select avg(r.rating)
from `reviews` as r
where m.id = r.movie_id
) as `reviews_avg_rating`
from `movies` as m;
Subquery na zasadzie O(n^2) – tak naprawdę to nie wiem, czy w bazach danych tak samo liczy się złożoność obliczeniową, na razie jesteśmy na zasadzie rozumienia czym jest N+1, eager loading, lazy loading oraz jak działają podstawowe algorytmy indeksowania (na razie zazwyczaj będziemy używać BTREE).
Ale fakt faktem – dla każdego rekordu z selecta głównego odpalany jest subselect, to powinniśmy ogarniać. Robiliśmy już to, łatwizna, teraz tylko dodajmy limit i offset, bo tego nie robiliśmy (w czystym SQL) a to akurat bardzo łatwe:
select m.*,
(
select avg(r.rating)
from `reviews` as r
where m.id = r.movie_id
) as `reviews_avg_rating`
from `movies` as m
LIMIT 5
OFFSET 5;
Ok, fajnie, a teraz w ramach ćwiczenia odtwarzamy 1 do 1 ten select przy użyciu fasady DB:
Artisan::command('reviews-withAvg-db', function () {
$films = DB::table('movies as m')
->select(DB::raw("m.*"),
DB::raw('(select avg(r.rating) from `reviews` as r where m.id = r.movie_id) as `reviews_avg_rating`')
)
->offset(5)
->limit(5)
->get();
foreach($films as $m){
$this->comment("Movie title: {$m->title} Reviews avg rating: {$m->reviews_avg_rating}");
}
});
Nie wygląda to ładnie, ale działa i wykonuje identyczne query, co dowodzi, że rozumiemy co robimy. Oczywiście, DB::raw (pierwsze) nie jest potrzebne:
Artisan::command('reviews-withAvg-db', function () {
$films = DB::table('movies as m')
->select("m.*",
DB::raw('(select avg(r.rating) from `reviews` as r where m.id = r.movie_id) as `reviews_avg_rating`')
)
->offset(5)
->limit(5)
->get();
foreach($films as $m){
$this->comment("Movie title: {$m->title} Reviews avg rating: {$m->reviews_avg_rating}");
}
});
Byłem przekonany, że Laravel nie ogarnie, że zaaliasował sobie movies na m bez DB::raw, ale jak widać myliłem się. Wyobraźmy sobie jednak, że się nie pomyliłem i w select mamy wiele potencjalnych pól do użycia DB::raw – będziemy to db raw tak w nieskończoność wymieniać?
Nie musimy, jest selectRaw:
Artisan::command('reviews-withAvg-db2', function () {
$films = DB::table('movies as m')
->selectRaw("m.*, (select avg(r.rating) from `reviews` as r where m.id = r.movie_id) as `reviews_avg_rating`")
->offset(5)
->limit(5)
->get();
foreach($films as $m){
$this->comment("Movie title: {$m->title} Reviews avg rating: {$m->reviews_avg_rating}");
}
});
Zabrakło dobrego przykładu na to selectRaw, ale idea jest taka, że jak robimy select, w którym pisalibyśmy DB::raw obok DB::raw, to po prostu używamy selectRaw.
Małe przypomnienie raw methods – selectRaw:
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();
Też param binding to fajny touch dla bezpieczeństwa, choć w takich sprawach trzeba się bezwzględnie konsultować ze specjalistą w tej dziedzinie, nie brać mojego słowa, że ten binding zabezpieczy nas przed wszystkim.
Metoda whereRaw:
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();
Jak widać rawy są fajne do używania różnych funkcji, na przykład funkcja IF (w SQL Server IIF).
Ok, następny raw, czyli havingRaw:
$orders = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > ?', [2500])
->get();
Na orderByRaw to mamy przykład z poprzednich lekcji, bardzo dobry:
Artisan::command('movies-avg-order', function () {
$reviews = DB::table('reviews')
->select('movie_id', DB::raw('avg(rating) as avg_rating'))
->groupBy('movie_id')
->havingRaw('avg(rating) > ?', [2])
->orderByRaw('avg(rating)')
->get();
foreach($reviews as $r){
$this->comment("Movie id: {$r->movie_id} Avg rating: {$r->avg_rating}");
}
});
Mam nadzieję, że bez problemu ogarniamy, co tu się dzieje, robiliśmy już to. W głowie możemy mieć mętlik, ale tylko ucząc się ostro i mierząc się z problemami oraz czytając dokumentację ogarniemy temat. Inaczej wiecznie będzie to wszystko skomplikowane.