Using UUIDs with Laravel’s Eloquent ORM

By default, Eloquent uses an auto-incrementing integer as the primary key for its tables. While most of the time this is totally acceptable, sometimes there is a need for primary keys to be less predictable.

Example: Table Reservations

You are writing an application for table reservations at a restaurant. You would likely have a database table that holds those reservations and ties them to a physical table and user account.

CREATE TABLE `reservations` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` int(11) unsigned NOT NULL,
    `table_id` int(11) unsigned NOT NULL,
    `reservation_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`),
    KEY `table_id` (`table_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

If you were to create a way to display existing reservations at a given URL, say http://acmereservations.com/reservation/15 where 15 refers to the `reservations`.`id` field, you can see how it would be very easy to simply bump that number up to 16 and see someone else’s reservation and perhaps even cancel it. For the sake of simplicity we’ll ignore the fact that some authentication and access control can prevent this from happening.

Why UUID?

UUID is a uniquely generated 36 character string and looks something like: 22beb489-2ba9-44c8-b189-5855e1d4d1ad. While I say that the UUID is unique, that’s not entirely true. Let’s just say the likelihood of duplicates is extremely small. Along with being unique, it’s also very unpredictable. You can see how it would be much harder to guess other reservation IDs given a URL of http://acmereservations.com/reservation/22beb489-2ba9-44c8-b189-5855e1d4d1ad.

Secondarily, UUIDs allow site owners to mask the number of records in their database tables. If your registration ID is 15 you could assume there are only 14 other reservations and maybe this isn’t the best place to eat. A UUID will mask the number of records in a table with the generated string.

So how do we use UUIDs with Eloquent?

It turns out it’s not overly difficult. I would suggest the best way is for all Eloquent models needing a UUID to extend a base model that implements the following functionality:

<?php

/**
 * This is a great UUID generator package available on Composer
 * but you can generate your UUID however you see fit.
 */
use Rhumsaa\Uuid\Uuid;

class UuidModel extends Eloquent
{
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        /**
         * Attach to the 'creating' Model Event to provide a UUID
         * for the `id` field (provided by $model->getKeyName())
         */
        static::creating(function ($model) {
            $model->{$model->getKeyName()} = (string)$model->generateNewId();
        });
    }

    /**
     * Get a new version 4 (random) UUID.
     *
     * @return \Rhumsaa\Uuid\Uuid
     */
    public function generateNewId()
    {
        return Uuid::uuid4();
    }
}

Now, all you have to do is extend the UuidModel class for your models and the UUID will be set automatically on the primary key before creation.

It’s worth noting that you will need to change your database schema to accommodate the UUID. For our `reservations` table, we would revise it like so:

CREATE TABLE `reservations` (
    `id` char(36) NOT NULL DEFAULT '',
    `user_id` int(11) unsigned NOT NULL,
    `table_id` int(11) unsigned NOT NULL,
    `reservation_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`),
    KEY `table_id` (`table_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

And, that’s actually it! Thanks to r15ch13 for the vast majority of the code involved. If you have any thoughts or a different way to approach this, let me know in the comments below.

Update [1/16/2014]:

It’s worth updating here that I did have some issues when running tests with PHPUnit. The first test would always work as expected, however, the second test wouldn’t add in the UUID on the primary key. I don’t know what is going on here, but it seems to have something to do with PHPUnit since it worked fine when run inside of Laravel. To get around the issue I pulled the event declarations out of the UuidModel and put them into a dedicated events.php file like so:

<?php

    $models = ['Reservation', 'User', 'Table'];

    foreach ($models as $model) {
        $model::creating(function ($model) {
            $model->{$model->getKeyName()} = (string)Uuid::uuid4();
        });
    }

I then revised the UuidModel as follows:

<?php

class UuidModel extends \Eloquent
{
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

}

Update [11/25/2015]:

As was suggested in the comments below, a Trait is a perfect solution to clean up our models and consolidate our code.

<?php

namespace App;

use Ramsey\Uuid\Uuid;

trait UuidForKey
{
    /**
     * Boot the Uuid trait for the model.
     *
     * @return void
     */
    public static function bootUuidForKey()
    {
        static::creating(function ($model) {
            $model->incrementing = false;
            $model->{$model->getKeyName()} = (string)Uuid::uuid4();
        });
    }

    /**
     * Get the casts array.
     *
     * @return array
     */
    public function getCasts()
    {
        return $this->casts;
    }
}

I then revised the UuidModel as follows:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Reservation extends Model
{
    use UuidForKey;

}

  • jrsalunga

    this is nice approach! im also using uuid for the ‘id’ field but im having trouble of when using the lazy loading functionality of the Eloquent when querying relationships like this:

    $reservations = Reservation::with(‘user’)->where(‘user_id’, ‘=’, ’22beb489-2ba9-44c8-b189-5855e1d4d1ad’)->get();

    i wonder how you configured your model to make ur FK a string and not an integer.

  • Garrett St. John

    Make sure you are adding

    public $incrementing = false;

    as a property on your models. That will tell Laravel to not assume an integer as the primary key.

  • jrsalunga

    yeah i get that.. my problem si getting the users info

    when i try to listen for query. eg:

    Event::listen('illuminate.query', function($query, $bindings, $time, $name){
    var_dump($query);
    var_dump($bindings);
    });

    i get this:

    string(46) "select * from `reservation` where `user_id` = ?"
    array(1) {
    [0]=>
    string(36) "22beb489-2ba9-44c8-b189-5855e1d4d1ad"
    }
    string(53) "select * from `user` where `user`.`id` in (?)"
    array(1) {
    [0]=>
    int(0)
    }

    the problem is in the second query i could no retrieve the users info because the user.id expecting int.

  • Garrett St. John

    I’m guessing there is something off with your model relationships. A bit tough to debug here. Sorry!

  • Ryan Rosiek

    I was banging my head against the wall for two days trying to understand why PHPUnit failed on only my second test. Thank you for posting the work around! I’m still curious as to why, but probably not clever enough to figure that one out.

    I implemented it slightly different with service providers. In case it helps anyone else out:

    // EventServiceProvider.php
    public function boot()
    {
    Event::listen(‘eloquent.creating: *’, ‘HandlersInstanceEventHandler@setUuid’);
    }

    // InstanceEventHandler.php
    public function setUuid($instance)
    {
    $instance->{$instance->getKeyName()} = (string) Uuid:uuid4();

    return true;
    }

    Thanks again!

  • fwolfcn

    Got same problem, my generated SQL is like:

    insert into `table_name` (`uuid`) values (533817ae-30e8-0002-6198-19e087617cdd)

    The UUID value is not quoted.

  • jrsalunga

    try to add

    public $incrementing = false;

    on your model

  • fwolfcn

    Yes my problem is gone, I have update reply above.

  • Mike

    I’m new to Laravel and investigating supporting an old database with it. Is this possible using binary(16) rather than char(36)?

  • fwolfcn

    It should work, why not give a try ?

  • Adam Klein

    I was curious did you ever figure out what do do in situations where all the foreign keys rely on a UUID that is a string?

  • jrsalunga

    yah.. just add

    public $incrementing = false;

  • Joseph

    You can find other approach here: http://alexconesa.wordpress.com/2014/01/25/laravel-uuid-primary-key-base-class-for-eloquent-orm/

    He takes care also when you are migrating one database where it is already exist records with their own UUID! Using your model will change the Primary Key.

    BTW Nice article!

    Cheers

  • Nice tip. I wonder if something like this would work as a trait, rather than a class that extends the Eloquent model class?

  • I just created a Uuid trait based on this post: http://pastebin.com/LRPLYdHG

    use RhumsaaUuidUuid;

    trait UuidTrait
    {
    /**
    * Boot the Uuid trait for the model.
    *
    * @return void
    */
    public static function bootUuidTrait()
    {
    static::creating(function($model) {
    $model->incrementing = false;
    $model->{$model->getKeyName()} = Uuid::uuid4()->__toString();
    });
    }
    }

  • Guest

    That would be preferable to me. I’d rather put `use UuidTrait` in my model, than have to change which class it extends.

  • This would be more preferable to me, adding use UuidTrait to my models rather than changing which class it extends.

  • rlweb

    Worked perfectly, but it changes the ID if you have received the model from the database? Is there, a way for it not to update the ID if the model has been taken from the database? Seems like $this->exists is not being set here correctly either…

  • Mac

    Is it necessary to change primary key in the database and refuse default “id” field? I only added new field “uuid”, which users can see, but other manipulations in the DB I left in a standard Laravel’s way.

  • Mangu

    Using a char(36) field for store the UUID in the database is probably not the best approach. It will dramatically decrease compare performance. This is the primary key and we don’t want it to be slow. At a bit level, a UUID is 128 bits, which means it will fit into 16 bytes. Using a binary(16) field should do the trick. It won’t be very human readable, but it will keep storage low and query executions fast.

  • Great post!

  • Miguel Stevens

    Binary is not a standard function of the schema builder i suppose?

  • Alex

    You can still order by UUID if you use time based uuids.

  • Agreed, BINARY(16) is a much more preferable solution, Percona has a great article on this subject.

    It is worth mentioning now that in Laravel 5.2 you can now create CHAR(36) columns in migrations as follows: $table->uuid(‘id’);

  • FYI, if one chooses to use $table->uuid(‘id’); instead of $table->char(‘id’, 36); Doctrine/DBAL breaks on these columns.

  • As of Laravel Framework v5.2.11 the getCasts() method is now public, due to commit 1995dee4bd8ce3090e91ac49a9cc06f60b3af0f3

  • Great point. I’ll update the post now that 5.2 is released.

  • Garrett,

    Not sure if you were just going to do a quick update, but you might want to hold off as something isn’t right.

    While it is still true that the getCasts() needs to be updated as a public method. The trait still does not work.

    For instance in my migration I have the following:
    $table->uuid(‘id’)->primary();

    In my trait, after setting incrementing to false we execute:
    $model->{$model->getKeyName()} = (string)Uuid::uuid4();

    getKeyName() should return the column set as the primary ID as defined in https://github.com/illuminate/database/blob/master/Eloquent/Model.php#L2006

    However, after inserting a record, a primary key is never set. It isn’t null, just empty.

  • Interesting. I’m using this exact code in a little app I am working on right now and haven’t seen any issue. Will check and report back.

  • Running this on the Laravel Users model, maybe something introduced the Authenticatable trait… Still working on it.

  • Kai K

    Having the same problem. Any clue what might be happening? I think MySQL’s strict mode may be coming into play. Only happens on 5.7 for me.

  • Thanks for this trip..

  • If uuid is used as primary key, Eloquent relations are not working.. Do you any idea?

  • What version of MySQL are you using? I’ve heard people are having issues with 5.7.

  • I used sqlite by writing unit test.. I think when primary key is not integer, Eloquent’s relations is not working…

    Did you try use model relations by using uuid as primary key ?

  • Yes, I have. As noted in the article above, though, I did have issues with it not working in unit tests. I never dug in too deep on why though.

  • coderleo42

    What fixed it for me was disabling MySQL strict mode (if you’re on 5.7), then moving the “static::creating()” call in the trait to the boot() method of the model. Not the best way, but it seems to work fine.

  • Ryan Ginnow

    How would you implement something like this with a users table extending the Authenticatable class?

  • Aditya Sitanggang

    Hi Daniel,
    Currently I experienced the same issue,
    Have you found any solution to that?

  • dascorp

    Hi, how do you check if UUID is valid UUID say i make it up in the url, i get the error of:

    SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for uuid:

    if I take one character out the error is thrown by sql but if i replace one character with other the sql returns empty result, which is correct

    so my question is how to check in my controller if UUID from the url is valid UUID?

  • Yassine Khachlek

    I have used your solution for a time, it was the best, until i found a better one.

    Defining a default value using the database uuid() function in migration like this:

    $table->uuid(‘id’)->default(‘uuid()’);

    $table->primary(‘id’);

    So simple, less code, and it’s the database who created the uuid for us, what you think guys?

  • malhal

    Where do you put the trait class? In the models folder?

  • Pk

    Has this been fixed?

  • Pk

    What calls the bootUuidForKey? I’m trying to simply override the creating event listener in the models that I use the trait in.