Куда пропадают публикации: анатомия одной ошибки в Eloquent
В таблице posts хранятся десять публикаций с user_id = 1. Запрос к базе данных их находит. Но в приложении вызов $user->posts()->first() возвращает null.
Не стоит сразу винить саму связь hasMany(). Даже если публикации есть в таблице, Eloquent может их не вернуть, поскольку запрос учитывает ключи связи и дополнительные условия.
Начать стоит с проверки самого пользователя:
//php
$user = User::find(1);
dump($user);Если результат - null, то публикации здесь ни при чём: модель не найдена по указанному идентификатору. Если пользователь загружен, можно переходить к проверке отношения.
В модели User оно обычно объявляется так:
//php
use Illuminate\Database\Eloquent\Relations\HasMany;
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}При таком объявлении Laravel ожидает внешний ключ user_id в таблице posts. Его значение должно соответствовать ключу родительской модели. Если у пользователя id = 1, Eloquent ищет публикации, связанные с этим идентификатором.
Именно здесь часто обнаруживается расхождение. В проекте поле может называться author_id, а в модели остаётся стандартное объявление. Laravel не угадывает назначение столбца по данным таблицы. Имя внешнего ключа нужно указать явно:
//php
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'author_id');
}Если связь строится по нестандартным ключам, придётся указать и локальный ключ родительской модели. Например, пользователь хранит идентификатор в поле uuid, а публикация - в поле author_uuid:
//php
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'author_uuid', 'uuid');
}Второй аргумент - поле в таблице публикаций, третий - поле модели пользователя, с которым оно сопоставляется. Именно эти значения определяют, какие записи попадут в отношение. Документация Laravel об отношениях Eloquent описывает оба аргумента.
Но бывает и так, что имена ключей правильные. В таблице есть запись с author_id = 12, а приложение загружает пользователя с id = 1. Запись существует, однако к этому пользователю она не относится. Проверить содержимое таблицы можно напрямую:
//sql
SELECT id, author_id, title
FROM posts
WHERE author_id = 1;Если запрос возвращает нужные публикации, а Eloquent - нет, следует посмотреть, какие ещё условия действуют при выборке.
Например, отношение может быть объявлено с фильтром:
//php
public function posts(): HasMany
{
return $this->hasMany(Post::class)
->where('published', true);
}Теперь черновики не попадут в результат. Десять записей в таблице ещё не означают, что хотя бы одна из них удовлетворяет условию published = true. Фильтр может добавляться и при вызове отношения:
//php
$post = $user->posts()
->where('status', 'published')
->first();Кроме этих условий, на выборку могут влиять глобальные scopes и SoftDeletes, исключающие мягко удалённые модели из обычных запросов. Поэтому сравнивать результат Eloquent с простым SQL-запросом имеет смысл только тогда, когда понятно, какие ограничения применяются в обоих случаях.
Сам запрос можно посмотреть так:
//php
$query = $user->posts();
dump($query->toSql());
dump($query->getBindings());Первый вызов покажет SQL с заполнителями, второй - параметры для них. По этому выводу можно проверить внешний ключ, идентификатор пользователя и дополнительные фильтры. Если запрос обращается не к той базе, что использовалась для ручной проверки, различие обнаружится уже на этом этапе.
Есть и другой случай: публикация появилась в базе, но приложение продолжает показывать прежний список. Например:
//php
$user = User::with('posts')->findOrFail(1);
$user->posts()->create([
'title' => 'Новая публикация',
]);Отношение было загружено ещё до создания записи. Метод create() сохранит новую публикацию и установит внешний ключ, но уже загруженная коллекция $user->posts автоматически не обновится. Если код продолжит читать это свойство, он может получить старый список.
В таком случае достаточно повторно загрузить данные:
//php
$user->refresh();
$posts = $user->posts;Метод refresh() повторно загружает атрибуты модели и обновляет ранее загруженные отношения. Если требуется сбросить только загруженное отношение, можно использовать другой вариант:
//php
$user->unsetRelation('posts');
$posts = $user->posts;При следующем обращении к свойству Eloquent запросит публикации заново. Исправлять ключи в этом случае не нужно.
У выражений $user->posts(), $user->posts и $user->posts()->first() разное назначение. Первое возвращает объект отношения, второе обращается к коллекции связанных моделей, третье выполняет запрос и возвращает одну модель либо null. Если коллекция пуста, это ещё не означает, что связь объявлена неправильно. А если публикация существует в базе, но отсутствует в уже загруженной коллекции, причина может быть вовсе не в запросе.
Поэтому, когда hasMany() будто бы теряет записи, полезнее проверить конкретные значения ключей и условия выборки, чем сразу менять модель. В конечном счёте Eloquent возвращает не все строки, которые разработчик видит в таблице, а только те, которые соответствуют построенному запросу.
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.