DEV Community

Mzer Terdoo
Mzer Terdoo

Posted on • Originally published at Medium on

A UUID Lesson in Laravel

laravel uuid lesson

Lesons learned in Laravel while building Swyftride -   a WhatsApp bus-booking system

Early in the development of Swyftride , I decided every table would use UUIDs instead of auto-incrementing integers, generated at the database level. See the logic I used to generate uuids in my migration file below:

$table->uuid('id')->primary()->default(DB::raw('gen_random_uuid()'));
Enter fullscreen mode Exit fullscreen mode

This worked for the most part until until trips creation actually revealed something I missed earlier. This broke the app in way, and I had to rethink this approach to one that truly served the purpose I had in mind.

The bug

Each time the I created a trip, right in the transaction, n number of seats needed to be generated based on the capacity of the bus assigned to that trip. See the code snippet below showing the DB transaction responsible for this :

DB::transaction(function () use ($validatedTripData) {
    foreach ($validatedTripData['trips'] as $tripData) {
        $trip = Trip::create([...]);

        for ($i = 1; $i <= $tripData['capacity']; $i++) {
            Seat::create(['trip_id' => $trip->id, 'seat_number' => (string) $i, 'status' => 'available']);
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Every seat insert into the seats table failed since $trip->id was null. $trip held every attribute I had set explicitly. Only $trip->id was missing. While a trip was indeed created, and a UUID sat in the trips table, my php program knew nothing about this UUID that was created in the id column at the database level.

What happened?

Model::create() only auto-populates a DB-generated key when the model is incrementing = true that path calls insertGetId(), which on Postgres compiles to INSERT ... RETURNING id. For a non-incrementing key, create() just inserts and stops.

The Fix I discovered

use Illuminate\Database\Eloquent\Concerns\HasUuids;

class Trip extends Model
{
    use HasUuids;
}

$trip = Trip::create([...]);

$trip->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"
Enter fullscreen mode Exit fullscreen mode

Illuminate\Database\Eloquent\Concerns\HasUuids trait on the model generates the UUID in PHP before the INSERT is built, via Eloquent's creating event. According to Laravel’s official documentation, “By default, theHasUuids trait will generate UUIDv7 identifiers for your models”. What this then means isPostgres never has to invent anything since PHP already handed it a value, and because PHP generated it, PHP already knows it. $trip->id is populated the instant create()returns. Seat creation worked immediately after.

One side effect: my DEFAULT gen_random_uuid() at the schema level now never fires since PHP always supplies the value first. Not wrong, just dead weight and needed a cleanup.

What I could have done otherwise?

The HasUuids wasn’t the only fix I could have utilised. I could have kept the DB-level default and just asked Postgres to return the value it generated by utlising a different approach insertGetId([…]) — the query builder method:

$tripId = DB::table('trips')->insertGetId([...], 'id'); // line 1
$trip = Trip::find($tripId); // line 2. check the inserted trip exists
Enter fullscreen mode Exit fullscreen mode

The insertGetId() on the query builder effectively returns the id of the insert. Conceptually, it runs this under the hood:

INSERT INTO trips (
    origin,
    destination,
    departure_time,
    capacity,
    price,
    status,
    ...
)
VALUES (
    'lagos',
    'abuja',
    '2026-07-25 22:00:00',
    12,
    15000.00,
    'active',
    ...
)
RETURNING id;
Enter fullscreen mode Exit fullscreen mode

The Returning id is the id of the inserted record. However I had key other features I needed that eloquent provided -  $fillable attribute to protect against mass assignment, and also attribute casting.

The mental model going forward

This wasn’t a framework ambush. It was solving a problem at the wrong layer before checking whether Laravel already had an idiom for it. Database-level UUID generation isn’t a mistake in the abstract . The actual gap was narrower: not knowing that create() has no Returning equivalent for non-incrementing keys, and that Laravel's answer is a trait you opt into per model, not a schema default you set once.

The question I now ask earlier, before writing custom logic at any layer: does the framework already have an idiomatic way to express this - and does that idiom actually fit what the rest of my system needs, or just what this one model needs?

Top comments (0)