Как получить предыдущую и следующую записи в Laravel

В моём приложении Подкасты мне нужно было показывать предыдущий и следующий подкасты из этого же шоу. План фронтенда был прост: у нас две кнопки со ссылками на соседние записи, чтобы пользователь мог перейти на них не используя поиск.Ссылки на предыдущую и следующую записи

Вроде звучит просто, но на самом деле это не так, ведь для этого не существует стандартного SQL-запроса, чтобы взять и одним оператором получить всё что нужно. Я имею ввиду, мы не можем просто сказать базе данных об этом и движок сам догадается, что нам нужно.

SELECT *
FROM 'podcasts'
WHERE podcasts.id = 4
WITH NEXT, PREV

Другими словами, нет такого запроса, который выдаст нам соседние записи, потом что SQL не знает, что для нас являются допустимыми «соседями», тем более это зависит и от самого движка.

Разумеется, для этой проблемы есть несколько решений. Я пробовал разные, но в итоге нашёл не слишком навороченный способ, который работает без проблем и не зависит от SQL-движка: SQLite, MySQL, MariaDB, PostgreSQL, SQL Server и тому подобное.

Два SQL-запроса, чтобы рулить ими всеми

Вместо того, чтобы погрязнуть в сложных необработанных SQL-запросах, только для того чтобы уложиться в одну строку, мы просто выполним два запроса: один для следующей записи, а второй — для предыдущей.

Что считать за следующую и предыдущую? Ну, вы можете сказать, что следующий ID. И это правильный ответ, учитывая, что у более старой записи будет меньший ID, а у более новой записи — более больший.

В этих запросах мы сортируем таблицу в обратном порядке, чтобы сделать текущую запись «потолком», а затем берем первую запись из списка.

$podcast = Podcast::find(4);

$next = Podcast::where('id', '>', $podcast->id)
    ->oldest('id')
    ->first();

$prev = Podcast::where('id', '<', $podcast->id)
    ->latest('id')
    ->first();

Я не понимаю, что вы пытаетесь сделать

Для получения следующего Подкаста, мы сначала отсортируем таблицу по самому старому подкасту, что означает «сортируем ID в порядке возрастания», отфильтруем тех, у кого ID больше, чем у текущего Подкаста и возьмём первую запись:

Алгорит вычисления следующей записи

Для получения предыдущего Подкаста сделаем наоборот. Отсортируем ID в порядке убывания, отфильтруем тех, у кого ID меньше и возьмём первую запись:

Алгорит вычисления предыдущей записи

Таким образом, если Подкаст имеет ID 3, то мы можем смело брать запись с ID 2 как «следующая» и запись с ID 4 — как «предыдущая».

Больше преимуществ, чем недостатков

Этот подход решает проблемы, которые есть у многих других решений.

  • Нет необходимости добавлять или вычитать идентификаторы, потому что это ОЧЕНЬ ненадежно и может привести к неожиданным результатам.
  • Мы можем отфильтровать запрос через where, например для конкретного подкаст-шоу.
  • Если в модели используется «Мягкое удаление» (Soft Deletes), то удаленные записи рассматриваться не будут, так как Eloquent будет применять этот скоуп автоматически.
  • Мы можем спокойно ожидать нулевого значения для следующей и предыдущей записи, соответственно, если мы проверяем последнюю или первую запись.

Пустая следующая запись

Если у вас вместо ID — UUID или другой непоследовательный тип поля, то вы можете использовать временные метки (дефолтные created_at and updated_at).

Если вы не используете индексы или первичные ключи в этих запросах, то при росте таблиц ваши SQL-запросы могут начать тормозить, ведь им придется обрабатывать всю таблицу.

$next = Podcast::where('created_at', '>', $podcast->created_at)
    ->oldest()
    ->first();

Вы можете использовать кеш, но не забывайте его обновлять при изменении или удаление записей. Но этого можно избежать, просто добавив составной индекс или первичные ключи. Допустим, что мы используем UUID и хотим отфильтровать подкасты по их UUID-шоу.

$next = Podcast::where('show_uuid', $podcast->show_uuid)
    ->where('created_at', '>', $podcast->created_at)
    ->oldest()
    ->first();

Этот запрос займет много времени, если, к примеру, у нас в таблице тысяча записей. Но индекс по обоим полями существенно его ускорит.

Schema::create('podcasts', function (Blueprint $table) {
    // ...    
    
    $table->index(['show_uuid','created_at']);
});

Опять таки, я думаю это отличный способ решения проблемы для некоторых сайтов, без переусложнения SQL-запросов.

Автор: DarkGhostHunter
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.