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), ' ', '-'))");
});
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
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
{
// ...
}
For more than one, pass an array:
#[Refreshes(['full_name', 'slug'])]
class User extends Model {}
Results with the attribute in place:
- After
User::create(...),full_namewas"Ada Lovelace". - After changing
first_nameto Grace and saving,full_namewas"Grace Lovelace". - With
['full_name', 'slug'], both values came back (Alan Turingandalan-turing). - Only the columns you list are loaded. In the single-column example,
slugwas stillnull. - The refreshed values do not leave the model marked as changed:
isDirty()returnedfalseafterwards.
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
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)