Проблема застарілих даних у моделях
Під час роботи з Eloquent моделями, які мають колонки зі значеннями, згенерованими базою даних, виникає типова проблема: після створення або оновлення моделі ці колонки залишаються порожніми або містять застарілі дані.
Розглянемо таблицю orders, де база даних автоматично обчислює колонку total на основі двох інших колонок:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->decimal('subtotal', 10, 2);
$table->decimal('tax', 10, 2);
$table->decimal('total', 10, 2)->storedAs('subtotal + tax');
$table->timestamps();
});
Хоча колонку total можна індексувати та використовувати в запитах як звичайну колонку, модель, отримана після create(), не містить її значення:
$order = Order::create(['subtotal' => 100, 'tax' => 8.25]);
$order->total; // null
$order->update(['tax' => 9.00]);
$order->total; // все ще null
До виходу Laravel 13.33 доводилося вручну викликати $order->refresh() після кожного запису, що перезавантажувало всі колонки та всі завантажені зв'язки.
Атрибут #[Refreshes]
Caleb White додав атрибут #[Refreshes] у pull request #61523. Цей атрибут розміщується на моделі зі списком колонок, які встановлює база даних:
use Illuminate\Database\Eloquent\Attributes\Refreshes;
use Illuminate\Database\Eloquent\Model;
#[Refreshes('total')]
class Order extends Model
{
protected $fillable = ['subtotal', 'tax'];
}
Після кожної операції вставки або оновлення Eloquent автоматично виконує SELECT цих колонок для збереженого рядка та копіює їх у модель:
$order = Order::create(['subtotal' => 100, 'tax' => 8.25]);
$order->total; // "108.25"
$order->update(['tax' => 9.00]);
$order->total; // "109.00"
Для оновлення кількох колонок можна передати масив або кілька аргументів. Обидва варіанти #[Refreshes(['total', 'slug'])] та #[Refreshes('total', 'slug')] працюють однаково.
Альтернативна конфігурація через властивість
Якщо ви налаштовуєте моделі через властивості замість атрибутів, можна використати властивість $refreshes:
class Order extends Model
{
protected array $refreshes = ['total'];
}
Якщо модель має обидва варіанти конфігурації, використовується властивість, а атрибут ігнорується. Модель без жодного з них не виконує додаткових запитів.
Згенеровані значення в подіях моделі
Оновлення виконується після INSERT або UPDATE, але перед тим, як Eloquent викликає події created або updated. Це означає, що обробники цих подій бачать нові значення. Спостерігач (observer), який надсилає квитанцію, може прочитати total, обчислений базою даних:
class OrderObserver
{
public function created(Order $order): void
{
Mail::to($order->customer)->send(new OrderReceipt($order));
}
public function updated(Order $order): void
{
if ($order->wasChanged('total')) {
$order->customer->notify(new OrderTotalChanged($order));
}
}
}
Перевірка wasChanged('total') працює коректно, оскільки Eloquent реєструє зміни після оновлення. Хоча ваш код ніколи безпосередньо не встановлює total, ця колонка з'являється у getChanges(), коли база даних обчислює нове значення.
Які операції запису запускають оновлення
Оновлення виконується для екземпляра моделі в таких сценаріях:
- Вставки через
create(), save(), saveQuietly() та saveOrIgnore()
- Оновлення через
update(), save() та saveQuietly()
- Виклики increment/decrement:
increment(), decrement(), incrementEach() та decrementEach()
Як працює запит оновлення
Оновлення виконується одним запитом SELECT для перелічених колонок з обмеженням по первинному ключу моделі:
select `total` from `orders` where `id` = ? limit 1
Запит виконується на з'єднанні для запису. При використанні read replicas репліка може ще не мати нового рядка.
Запит також пропускає глобальні scope, що означає, що tenant або soft-delete scope не можуть приховати рядок, який Eloquent щойно зберіг. Використовується firstOrFail(): якщо рядок зникне до моменту виконання запиту, збереження викине ModelNotFoundException.
#[Refreshes] або refresh()
Атрибут #[Refreshes] підходить для колонок, які ви читаєте одразу після запису: колонки storedAs та virtualAs, значення, встановлені тригерами, та значення за замовчуванням з бази даних, які Eloquent не встановлює. Атрибут додає запит до кожного запису, тому вказуйте лише ті колонки, які ви дійсно читаєте.
Викликайте $model->refresh(), коли потрібно перечитати весь рядок або завантажені зв'язки. Також можна використовувати його для разових операцій запису, де додавання атрибута до моделі створило б додатковий запит при кожному іншому збереженні.
Документація Eloquent висвітлює цей атрибут у розділі "Refreshing Attributes After Writes".