Куда пропадают публикации: анатомия одной ошибки в Eloquent

mr. Cooper • 6 часов назад • Веб-разработка
Куда пропадают публикации: анатомия одной ошибки в 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 возвращает не все строки, которые разработчик видит в таблице, а только те, которые соответствуют построенному запросу.

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи