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