DEV Community

Cover image for Laravel #[Refreshes]: Refresh Attributes After Writes
devTalk
devTalk

Posted on Originally published at dev-talk.com

Laravel #[Refreshes]: Refresh Attributes After Writes

You create a model, and one of its columns is calculated by the database: a full name built from two fields, a slug, a total. You save it and read the value back. You get null, even though the row in the database is correct. The model only knows what you gave it, and the database never told it what it computed. The Laravel 13 documentation has a small feature for exactly this: the #[Refreshes] attribute. I ran it against a real MariaDB database to see what it does and what it costs.

The problem, reproduced

Here is a table with generated columns. full_name is stored, and slug is computed when read:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('first_name');
    $table->string('last_name');
    $table->string('full_name')
        ->storedAs("concat(first_name, ' ', last_name)");
    $table->string('slug')
        ->virtualAs("lower(replace(concat(first_name, '-', last_name), ' ', '-'))");
});
Enter fullscreen mode Exit fullscreen mode

With a normal model, this happened:

$user = User::create(['first_name' => 'Ada', 'last_name' => 'Lovelace']);

$user->full_name;   // null  (the database has "Ada Lovelace")
$user->slug;        // null
Enter fullscreen mode Exit fullscreen mode

Eloquent ran one query, the INSERT, and did not read anything back. After an update it was the same: I changed the first name to Grace and saved, the database now said "Grace Lovelace", and the model still held null.

The fix: #[Refreshes]

Tell the model which attributes to reload after a write. This is straight from the documentation:

use Illuminate\Database\Eloquent\Attributes\Refreshes;
use Illuminate\Database\Eloquent\Model;

#[Refreshes('full_name')]
class User extends Model
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

For more than one, pass an array:

#[Refreshes(['full_name', 'slug'])]
class User extends Model {}
Enter fullscreen mode Exit fullscreen mode

Results with the attribute in place:

  • After User::create(...), full_name was "Ada Lovelace".
  • After changing first_name to Grace and saving, full_name was "Grace Lovelace".
  • With ['full_name', 'slug'], both values came back (Alan Turing and alan-turing).
  • Only the columns you list are loaded. In the single-column example, slug was still null.
  • The refreshed values do not leave the model marked as changed: isDirty() returned false afterwards.

What it costs: one extra query per write

This is the trade-off to know before you use it. I logged the SQL:

insert into `users` (`first_name`, `last_name`) values (?, ?)
select `full_name` from `users` where `id` = ? limit 1
Enter fullscreen mode Exit fullscreen mode

An update behaves the same way: the UPDATE, then a SELECT for just the listed columns. Reading the framework source, that SELECT uses the write connection, which is the safe choice when you have read replicas, because a replica may not have your new row yet. If nothing changed, save() ran no queries at all, so the refresh did not add any either.

The refresh also works with increment(). I used a stored column logins_x2 defined as logins * 2 and listed it in the attribute. After $user->increment('logins', 5), the model showed logins = 5 and logins_x2 = 10 using two queries (the UPDATE and the SELECT).

Refreshes vs. refresh()

You could call $user->refresh() after saving. It reloads every column, not just the ones you care about, and you have to remember to call it at every write. #[Refreshes] does it automatically for the columns you name, wherever the model is saved. Choose refresh() if you only need the value in one place, and the attribute if the model should always be correct after writing.

When to use it, and when to skip it

  • Use it when your code or API response reads a generated column right after a write.
  • Skip it when nobody reads that column straight after saving. You would pay for a SELECT that nothing uses.
  • Be careful on bulk jobs. Creating 10,000 models one by one now means 20,000 queries. Bulk insert() does not create model instances, so it is unaffected, but it also gives you no refreshed values.

Common questions

Does it work for columns set by triggers or database defaults?

The docs describe it for generated columns. The mechanism reloads whatever columns you list, so it should work for any column the database fills in, but I only tested generated columns (stored and virtual).

Which Laravel version has it?

It is in the Laravel 13.x documentation and in the 13.35.0 source I tested. I did not check older releases, so on an older version use refresh() instead.

References


Originally published on DEV Talk.

Top comments (0)