Очень часто возникает ситуация, когда вам необходимо получить коллекцию моделей отсортированную по полю из связанной модели. Например, вы хотите получить список постов отсортированных по последнему оставленному комментарию. Сегодня мы разберем один способ применения такой сортировки используя Eloquent.
Из коробки модели в Laravel не могут отсортировать по полю из связанной модели, такой код работать не будет:
Post::with('comments') ->orderBy('comments.published_at', 'desc') ->limit(10) ->get();
Решение очень простое но не очевидное:
$posts = Post::join('comments', 'comments.post_id', '=', 'posts.id') ->orderBy('comments.published_at', 'desc') ->with('comments') ->select('posts.*') ->limit(10) ->get();
Здесь мы делаем join
с нужной таблицей и в условии orderBy
прописываем поле, в моём случае это comments.published_at
. Таким образом мы получим коллекцию моделей Post
отсортированную по дате последнего комментария, при это у нас еще и будут доступны модели из связи comments
.
Давайте рассмотрим более подробно на примере.
Я создал простой проект для тестирования одного из методов. Первым делом создаём 2 модели с миграциями запустив комманды:
php artisan make:model Post -m php artisan make:model Comment -m
Открываем миграции и заполняем их следующим содержимым. Файл 2019_07_04_171440_create_posts_table.php
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }
Файл 2019_07_04_171451_create_comments_table.php
:
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCommentsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('comments', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('author'); $table->unsignedBigInteger('post_id'); $table->text('body'); $table->timestamp('published_at'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('comments'); } }
Файлы моделей меняем соответственно
app/Post.php
<?php namespace App; use Illuminate\Database\Eloquent\Model; /** * Class Post * @package App */ class Post extends Model { /** * @var array */ protected $fillable = [ 'title', 'body' ]; /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function comments() { return $this->hasMany(Comment::class); } }
app/Comment.php
<?php namespace App; use Illuminate\Database\Eloquent\Model; /** * Class Comment * @package App */ class Comment extends Model { /** * @var array */ protected $fillable = [ 'author', 'post_id', 'published_at', 'body' ]; /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function post() { return $this->belongsTo(Post::class); } }
Таблицу я заполнил данными при помощи фабрик и сидера (ссылка на проект в github будет в конце статьи).
После этого я создал простую страницу для отображения результата и контроллер.
app/Http/Controllers/IndexController.php
<?php namespace App\Http\Controllers; use App\Post; /** * Class IndexController * @package App\Http\Controllers */ class IndexController extends Controller { /** * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function index() { $posts = Post::with('comments') ->limit(10) ->get(); return view('welcome', compact('posts')); } }
resources/views/welcome.blade.php
<!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <!-- Fonts --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <div class="container my-5"> <div class="row"> <h1>Posts</h1> </div> <div class="row"> <table class="table table-striped"> <thead> <tr> <th>#</th> <th>Title</th> <th>Body</th> <th>Created</th> <th>Commented</th> </tr> </thead> <tbody> @foreach($posts as $post) <tr> <td>{{$post->id}}</td> <td>{{$post->title}}</td> <td>{{$post->body}}</td> <td>{{$post->created_at}}</td> <td>{{$post->comments->sortByDesc('published_at')->first()->published_at}}</td> </tr> @endforeach </tbody> </table> </div> </div> </body> </html>
Применяем миграции и сиды, после запускаем сервер php artisan serve
. Открываем в браузере адрес http://127.0.0.1:8000
и видим что в данном случае наши посты отсортированы по полю id
в порядке возрастания.

Я хочу отсортировать записи по последнему оставленному комментарию. Для этого в контроллере мне нужно поменять условие выборки из базы данных на:
$posts = Post::join('comments', 'comments.post_id', '=', 'posts.id') ->orderBy('comments.published_at', 'desc') ->with('comments') ->select('posts.*') ->limit(10) ->get();
Обновляем страницу и получаем записи отсортированные по последнему оставленному комментарию:

Вот таким простым способом при помощи Eloquent можно отсортировать по полю из связанной модели не используя QueryBuilder. В результате мы получим коллекцию объектов с его связанными моделями, что в случае с QueryBuilder было бы невозможно.
Весь используемый код я залил на github.com. Можете себе скачать и посмотреть как это работает на реальном примере)
А что если у нас у каждой записи по 5000 комментариев, все повиснет же.
Если памяти скрипту хватит, то повиснет)