Laravel Tutorials

Eloquent Refreshes: Load Generated Columns After Save

Published
Eloquent Refreshes: Load Generated Columns After Save image

If you create an Eloquent model and read a generated column straight back, you get null. When you update a model the column keeps its old value until you call refresh().

Laravel 13.33 introduced the #[Refreshes] attribute to fix this potential issue: list the columns the database computes, and Eloquent reads them back after every insert and update.

Let's see how you can use this trait to automatically load generated columns after save without remembering to referesh.

Generated Columns and Stale Models

Take an orders table where the database calculates total from two other columns:

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();
});

You can query and index total like any other column, but the model you get back from create() does not have it:

$order = Order::create(['subtotal' => 100, 'tax' => 8.25]);
 
$order->total; // null
 
$order->update(['tax' => 9.00]);
 
$order->total; // still null

Before this release, you had to manually call $order->refresh() after each write, which reloads every column and every loaded relationship.

Adding the Refreshes Attribute

Caleb White added #[Refreshes] in #61523. Put it on the model and list the columns the database sets:

use Illuminate\Database\Eloquent\Attributes\Refreshes;
use Illuminate\Database\Eloquent\Model;
 
#[Refreshes('total')]
class Order extends Model
{
protected $fillable = ['subtotal', 'tax'];
}

After each insert or update, Eloquent selects those columns for the saved row and copies them onto the model:

$order = Order::create(['subtotal' => 100, 'tax' => 8.25]);
 
$order->total; // "108.25"
 
$order->update(['tax' => 9.00]);
 
$order->total; // "109.00"

To refresh more than one column, pass an array or several arguments. #[Refreshes(['total', 'slug'])] and #[Refreshes('total', 'slug')] do the same thing.

If you configure models with properties instead of attributes, set $refreshes:

class Order extends Model
{
protected array $refreshes = ['total'];
}

When a model has both, the property is used and the attribute is ignored. A model with neither runs no extra query.

Generated Values in Model Events

The refresh runs after the INSERT or UPDATE and before Eloquent fires created or updated, so listeners on either event see the new value. An observer that sends a receipt can read the total the database calculated:

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));
}
}
}

The wasChanged('total') check works because Eloquent records the changes after the refresh. Your code never sets total, but it appears in getChanges() when the database calculates a new value.

Which Writes Trigger a Refresh

The refresh runs on a model instance in these scenarios:

  • Inserts from create(), save(), saveQuietly(), and saveOrIgnore()
  • Updates from update(), save(), and saveQuietly()
  • Increment/decrement calls: increment(), decrement(), incrementEach(), and decrementEach()

How the Refresh Query Works

The refresh is one SELECT for the listed columns, scoped to the model's primary key:

select `total` from `orders` where `id` = ? limit 1

The query runs on the write connection. With read replicas, a replica might not have the new row yet.

It also skips global scopes, which means a tenant or soft-delete scope cannot hide the row Eloquent just saved. And it uses firstOrFail(): if the row is gone by the time the query runs, the save throws a ModelNotFoundException.

Refreshes or refresh()

#[Refreshes] fits columns you read right after a write: storedAs and virtualAs columns, values that a trigger sets, and database defaults that Eloquent does not set. The attribute adds a query to every write, so list only the columns you read back.

Call $model->refresh() when you need the whole row or the loaded relationships re-read. You can also call it for a one-off write, where adding the attribute to the model would put an extra query on every other save.

The Eloquent documentation covers the attribute under "Refreshing Attributes After Writes."

Further Reading

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
PHP News: A New Site for PHP Releases, RFCs, and Packages image

PHP News: A New Site for PHP Releases, RFCs, and Packages

Read article
Pause Polling in Background Tabs in Inertia.js v3.9 image

Pause Polling in Background Tabs in Inertia.js v3.9

Read article
Look What You Helped Me Build image

Look What You Helped Me Build

Read article
BashBox: A Sandboxed Bash Interpreter for AI Agents in PHP image

BashBox: A Sandboxed Bash Interpreter for AI Agents in PHP

Read article
Running Laravel's Scheduler on Multiple Servers image

Running Laravel's Scheduler on Multiple Servers

Read article
Laravel Fake Assertions Now Accept Property Arrays image

Laravel Fake Assertions Now Accept Property Arrays

Read article