laravel-firstorcreate-replace-first-create-tip.png
Шаблон кодуLaravel

Laravel firstOrCreate(): знайти або створити без ручного if

Як замінити ручний first() та create() на firstOrCreate(), правильно розділити умови пошуку і значення для нового запису, використати wasRecentlyCreated та не плутати метод з firstOrNew() і updateOrCreate().

Іноді код має дуже простий намір: знайти модель за стабільними атрибутами, а якщо такого запису ще немає - створити його.

Для цього не обов'язково вручну викликати first(), перевіряти результат через if і окремо викликати create(). Eloquent має firstOrCreate(), який виражає цей сценарій напряму.

Замість first() та ручного create()

До
php
$setting = UserSetting::query()    ->where('user_id', $userId)    ->where('key', 'timezone')    ->first(); if ($setting === null) {    $setting = UserSetting::create([        'user_id' => $userId,        'key' => 'timezone',        'value' => 'Europe/Kyiv',    ]);}
Після
php
$setting = UserSetting::firstOrCreate(    [        'user_id' => $userId,        'key' => 'timezone',    ],    [        'value' => 'Europe/Kyiv',    ],);

Тут важливо не тільки те, що код став коротшим. Структура виклику одразу показує два різні набори даних: за чим шукаємо запис і що додаємо, якщо доведеться створити новий.

Перший масив - умови пошуку

PHP - Lookup attributes та create values
$setting = UserSetting::firstOrCreate(    [        'user_id' => $userId,        'key' => 'locale',    ],    [        'value' => 'uk',    ],);

Laravel спочатку шукає запис за атрибутами з першого масиву. У цьому прикладі ідентичність запису визначає пара user_id + key.

Якщо відповідний запис уже існує, він просто повертається. Другий масив при цьому не використовується для оновлення.

Другий масив використовується тільки при створенні

До
php
$setting = UserSetting::firstOrCreate(    [        'user_id' => $userId,        'key' => 'theme',    ],    [        'value' => 'dark',    ],); // Existing value may still be "light".
Після
php
$setting = UserSetting::updateOrCreate(    [        'user_id' => $userId,        'key' => 'theme',    ],    [        'value' => 'dark',    ],); // Existing value becomes "dark".
Це головна відмінність від updateOrCreate(): firstOrCreate() не оновлює значення вже існуючої моделі. Другий масив використовується лише тоді, коли створюється новий запис.

Тому вибір між цими двома методами має залежати від бізнес-наміру. Якщо існуючий запис потрібно залишити як є - firstOrCreate(). Якщо його стан потрібно привести до нових значень - updateOrCreate().

Метод повертає вже збережену модель

PHP - Повернена модель вже існує в БД
$role = Role::firstOrCreate(    [        'slug' => 'editor',    ],    [        'name' => 'Editor',    ],); $roleId = $role->id;

Незалежно від того, модель знайшли чи створили, результатом буде Eloquent model, яка вже існує в базі даних. Додатковий save() після firstOrCreate() не потрібний.

Як визначити, чи модель створили саме зараз

PHP - wasRecentlyCreated після firstOrCreate()
$role = Role::firstOrCreate(    [        'slug' => 'editor',    ],    [        'name' => 'Editor',    ],); if ($role->wasRecentlyCreated) {    // The row was created by this call.}

Властивість wasRecentlyCreated дозволяє відрізнити два результати одного виклику. Це корисно, якщо окрему дію потрібно виконати тільки після фактичного створення нового запису.

firstOrCreate() проти firstOrNew()

PHP - firstOrNew() повертає незбережену нову модель
$setting = UserSetting::firstOrNew(    [        'user_id' => $userId,        'key' => 'timezone',    ],    [        'value' => 'Europe/Kyiv',    ],); // A new model is not persisted yet.$setting->save();

firstOrNew() має схожу логіку пошуку, але якщо запису немає, він лише створює новий об'єкт моделі в пам'яті. До бази даних він потрапить тільки після save().

Це корисно, коли перед збереженням потрібно додатково змінити модель, виконати перевірку або взагалі вирішити, чи треба її зберігати.

firstOrCreate() працює через relationships

PHP - firstOrCreate() через relationship
$setting = $user    ->settings()    ->firstOrCreate(        [            'key' => 'timezone',        ],        [            'value' => 'Europe/Kyiv',        ],    );

Якщо дочірній запис належить конкретній моделі, виклик через relationship часто прибирає зайві деталі з lookup array. Контекст користувача вже задає сам relationship, а в масиві залишається бізнес-ключ дочірнього запису.

Lookup attributes повинні описувати стабільну ідентичність

До
php
UserSetting::firstOrCreate(    [        'user_id' => $userId,        'key' => 'timezone',        'value' => $timezone,    ],);
Після
php
UserSetting::firstOrCreate(    [        'user_id' => $userId,        'key' => 'timezone',    ],    [        'value' => $timezone,    ],);

Якщо змінне поле потрапить у lookup array, нове значення може означати для Laravel вже іншу комбінацію пошуку.

Тому в перший масив варто класти атрибути, які визначають ідентичність запису, а значення за замовчуванням для нового запису - у другий.

Unique constraint має закріплювати ту саму ідентичність у БД

PHP - Unique index для бізнес-ідентичності
Schema::table(    'user_settings',    function (Blueprint $table) {        $table->unique([            'user_id',            'key',        ]);    },);

Якщо user_id + key повинні бути унікальними, це правило краще закріпити не тільки в application code, а й у самій базі даних.

Це особливо важливо при паралельних requests. Два процеси можуть майже одночасно не знайти запис і спробувати його створити.

Сучасний Laravel обробляє конфлікт унікальності

У сучасному Eloquent після первинного пошуку firstOrCreate() переходить до внутрішнього createOrFirst(). Якщо insert отримує помилку порушення unique constraint, Laravel намагається ще раз знайти запис і повернути його.

Але ця поведінка не замінює правильний unique index. Навпаки, саме обмеження БД дає Laravel надійний сигнал про те, що інший процес уже створив такий запис.

firstOrCreate() не означає, що будь-який набір полів автоматично стає унікальним. Бізнес-інваріант усе одно повинен бути явно закріплений у схемі бази даних.

Не очікуйте одного SQL statement

firstOrCreate() описує зручну Eloquent-операцію, а не гарантію одного SQL statement. Laravel спочатку намагається знайти модель, а якщо її немає - створює її.

Тому користь методу передусім у правильному вираженні наміру і відсутності ручної розгалуженої логіки.

Mass assignment все одно має значення

Якщо модель потрібно створити, Eloquent проходить звичайний шлях створення моделі. Тому правила $fillable або $guarded нікуди не зникають.

firstOrCreate() не є способом обійти модельні правила. Це лише зручна композиція пошуку та створення.

Коли firstOrCreate() підходить найкраще

  • довідники та стабільні reference records;
  • налаштування з природним унікальним ключем;
  • ролі, теги, типи та інші сутності, які треба створити лише за відсутності;
  • пов'язані записи, для яких existing state не потрібно перезаписувати;
  • імпорт або синхронізація, де повторний запуск повинен повторно використовувати вже наявний запис.

Коли краще інший метод

  • якщо existing record треба оновити новими значеннями - updateOrCreate();
  • якщо нову модель треба підготувати перед збереженням - firstOrNew();
  • якщо відсутність запису є помилкою - firstOrFail();
  • якщо потрібно масово вставляти або оновлювати багато записів - розгляньте upsert();
  • якщо create і existing branches мають зовсім різну бізнес-логіку - явний код може бути зрозумілішим.

Хороший firstOrCreate() читається майже як правило предметної області: знайди сутність за її стабільною ідентичністю, а якщо її ще немає - створи з цими початковими значеннями.

#eloquent #laravel-12 #laravel-13 #php-8-4 #php-8-5 #model-persistence #first-or-create #first-or-new

Схожі поради

laravel-updateorcreate-replace-first-if-tip.pngПорада
Переглянути пораду
#eloquent #laravel-12 #laravel-13 #php-8-4 #php-8-5 #update-or-create #model-persistence #upsert
laravel-withwherehas-avoid-duplicate-conditions-tip.pngПорада
Переглянути пораду
#eloquent #laravel-12 #laravel-13 #php-8-4 #php-8-5 #where-has #with-where-has #eager-loading #relationships
laravel-tap-side-effects-keep-original-value-tip.pngПорада
Переглянути пораду
#eloquent #laravel-12 #laravel-13 #php-8-4 #php-8-5 #tap #helpers #tappable
laravel-whererelation-replace-wherehas-tip.pngПорада
Переглянути пораду
#eloquent #laravel-12 #laravel-13 #php-8-4 #php-8-5 #where-relation #where-has #relationship-queries