Как в Laravel получить записи связей отфильтрованные по критерию
Laravel - это мощный PHP-фреймворк, который для работы с базой данных использует Eloquent
, мощную и удивительную ORM, которая позволяет выполнять сложные запросы SQL очень простым и интуитивно понятным способом. Настолько простая и понятная обёртка над SQL-запросами полностью избавляет нас от работы с самим SQL, и построение запросов к базе данных происходит на уровне PHP. Но иногда нам нужно больше, чем простые выборки по ID, в этой статье я покажу примеры того, как можно гибко работать со связями в Laravel, и производить фильтрацию связей по определённым условиям.
При работе с примитивными запросами к базе данных в Laravel, возможно, у вас никогда не возникало никаких проблем с построением запросов через билдер (если все ваши задачи сводятся к использованию методов where
, whereIn
и т.д.). Но когда ваше приложение начинает расти, или появляется задача, выходящая за рамки примитивных SQL-запросов, вы начинаете думать: "А как я могу сделать это в Laravel"?
В некоторых случаях вы знаете, как это сделать, используя чистый SQL, но, когда вы используете фреймворк, пишите красивый код, используя билдеры, негоже использовать чистый SQL в середине всего. Потому, моя сегодняшняя статья и призвана к тому, чтобы показать, как можно выполнить операции в Eloquent, выходящие за рамки тривиальных.
Иногда, когда вы делаете запрос к базе данных, вы ищите данные, которые имеют соответствующме значения в их связях. То есть, грубо говоря, вы ищите те сущности, у которых существуют связи и определённым значением. И в этой статье я покажу, как выполнять такой поиск, и как получить только те значения связей, параметры которых удовлетворяют фильтр.
Что будем делать
Для демонстрации логики построения кода я описал несколько сущностей: User
, который может иметь несколько привязанных кошельков в кабинете, ``
class User extends Model
{
public function wallets()
{
return $this->hasMany(Wallet::class);
}
}
class Wallet extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
И так, чтобы получить список кошельков пользователя, достаточно выполнить:
$user = User::find(1);
$wallets = $user->wallets;
Здесь ничего нового.
Построение сложных запросов
Иногда бывает необходимость получить те записи из базы данных, у которых связи имеют определённое значение. К примеру, нужно получить список тех пользователей, у которых есть биткоин-кошелёк. Для этого существует 2 метода: whereHas
и orWhereHas
, которые осуществляют выборку по условию в связи.
$users = User::whereHas('wallets', function($query) {
$query->where('address', '=', 'BTC');
})->get();
Метод
whereHas
первым аргументом принимает название связи, в которой будет производиться поиск, а вторым параметром анонимную функцию, в котором мы указываем конкретные критерии этого поиска. Внутри анонимной функции вы можете использовать любые методы, существующие аEloquent
.
Связь
Проблема в том, что после выполнения запроса на получение отфильтрованных данных по связи, при вызове оригинальной связи, данные останутся неизменными (будут получены все записи):
$user = User::whereHas(...)->first();
$user->wallets; // все доступные кошельки
Но иногда просто хочется использовать использовать $user->wallets
получив только те данные, которые подпадают под описанный фильтр. В данном случае, BTC-кошельки, а не любой кошелёк пользователя. Другими словами, вы хотели бы использовать ту же логику, которую использовали для фильтрации сообщений в отношении кошельков.
Решение
В Laravel очень просто работать со связями, даже когда они загружаются "жадно". Для жадной загрузки связей используется метод with
, с которым все Laravel разработчики уже давно знакомы. Снова ничего нового.
Мой совет здесь заключается в том, чтобы повторно использовать ту же анонимную функцию, описывающую фильтрацию, которую передавали методу whereHas
с методом with
. Итак, давайте сохраним эту анонимную функцию в локальную переменной, которая называется $filter
, и перепишем код со старого формата:
$users = User::whereHas('wallets', $filter = function($query) {
$query->where('address', '=', 'BTC');
})->get();
На новый, с использованием with
:
$users = User::whereHas('wallets', $filter = function($query) {
$query->where('address', '=', 'BTC');
})
->with(['wallets' => $filter ])
->get();
В этом коде мы сначала описали анонимную функцию для запроса, который возвращает только те записи пользователей, которые имеют запись с соответствующим описанным условием filter
. И эту же анонимную функцию мы применили и для фильтрации записей связи wallets
.
Ведь, как оказалось, метод with
может принимать не только массив ключей с именами связей, а так же, у нас есть возможность передать ассоциативный массив. Где ключом будет имя связи (а текущем случае wallet
), и анонимная функция, по которой связи будут фильтроваться.
И теперь, если вы будете вызывать $user->wallets
, вы получите только те записи, которые соответствуют фильтру (в текущем случае, будут доступны только кошельки BTC
.
К примеру, если вы будете тестировать этот код, создадим несколько записей для тестов.
$user = User::find(1);
$user->wallets()->saveMany([
Wallet::make(['address' => 'BTC']),
Wallet::make(['address' => 'WM']),
Wallet::make(['address' => 'Visa']),
]);
И можем протестировать:
$user = User::whereHas(...)->with(...)->get()->first();
$this->assertCount(1, $user->wallets);
$this->assertEquals('BTV', $user->wallets->first()->address);
Или, если распечатать содержимое, то получим одну запись, описанную в фильтре, чем все существующие:
Резюме
Работать со сложными запросами в Laravel не так сложно, как кажется. Но программирование сложных запросов - это именно программирование на языке программирование, а не написание чистого SQL. Нетривильные связи могут внести некоторую сложность в вашу логику, потому что вы думаете на уровне объектов и отношений, что, на мой взгляд, хорошо, потому что в этом случае вы полноценно занимаетесь программированием.
Решение, описанное в этой статье очень помогает при создании отчётов, потому что, обычно, вам приходится фильтровать модели и работать с их отношениями, выполняя лишние операции по работе с данными, дополнительно их обрабатывая.
Laravel предоставляет удобный механизм, который может принести вам большую гибкость, потому что вы можете инкапсулировать индивидуальную логику внутри "одного и того же запроса", повторно используя анонимные функции.
А для того, чтобы стать более продвинутым в программировании с Eloquent, советую изучить основные методы при работе с коллекциями Laravel.