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

laravelcloud logo
Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Cloud

The latest

View all →
Email Sending and Testing for Laravel in One Account image

Email Sending and Testing for Laravel in One Account

Read article
Decide with Jev: Tie Every AI Check to the Exact Draft image

Decide with Jev: Tie Every AI Check to the Exact Draft

Read article
PIE 1.5 Changes PHP Extension Installs in CI image

PIE 1.5 Changes PHP Extension Installs in CI

Read article
Delete Expired Sessions in Laravel With a Scheduled Command image

Delete Expired Sessions in Laravel With a Scheduled Command

Read article
Pollora: Run WordPress Inside a Laravel Application image

Pollora: Run WordPress Inside a Laravel Application

Read article
Scheduled Session Garbage Collection in Laravel 13.39 image

Scheduled Session Garbage Collection in Laravel 13.39

Read article