DEV Community 👩‍💻👨‍💻

Cover image for Eloquent UUIDs.
Kati Frantz
Kati Frantz

Posted on

Eloquent UUIDs.

In this tutorial, I'm going to show you how and why to use uuids as your primary keys in your eloquent models.

What are UUIDs?

A Uuid (Unique universal identifier) is a 128bit number used to identify information on the internet. Depending on the specific mechanisms used, a UUID is either guaranteed to be different or is, at least, extremely likely to be different from any other UUID generated until 3400 A.D (that means for more than the next thousand years, it's highly unlikely that the same UUID would be generated again no matter the mechanism used).

UUIDs are therefore important specifically because of their uniqueness.

Why should we use UUIDs?

  • UUIDs do not reveal information about your database records, so it's safer to use them in a public URL. For example, seeing /users/12/ in the URL strongly suggests that a user with an id of 11 exists, which greatly increases the possibility of attack.
  • They are unique across tables, databases, and servers, and therefore migration from one database to another is incredibly easy.
  • UUIDs are generated anywhere, and therefore you can know the unique identifier of the next database record without necessarily hitting your database.

Integrating UUIDs into your eloquent models.

We'll start by installing a fresh laravel application.

    laravel new eloquent-uuids
Enter fullscreen mode Exit fullscreen mode

Generating UUIDs

The first thing we'll do is set up a way to generate a UUID.
The first option would be to use an external package that does this for us, and I'll recommend this package.Personally, I'll rather just have a custom function that does this, so from the PHP documentation, we'll get a function that generates UUIDs for us.


<?php

function uuid4()
{
    return sprintf(
        '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
        mt_rand(0, 0xffff),
        mt_rand(0, 0xffff),
        mt_rand(0, 0xffff),
        mt_rand(0, 0x0fff) | 0x4000,
        mt_rand(0, 0x3fff) | 0x8000,
        mt_rand(0, 0xffff),
        mt_rand(0, 0xffff),
        mt_rand(0, 0xffff)
    );
}

Enter fullscreen mode Exit fullscreen mode

Now, where do we place this file in our laravel application? I'll create a new file in /app/Helpers/uuid.php, and I'll place this function in it.

Next, I'll autoload this file with composer so that this function would be globally available in my laravel application.

// composer.json
...
    "autoload": {
        "files": [
            "App/Helpers/uuid.php"
        ]
    }
...

Enter fullscreen mode Exit fullscreen mode

Disabling default primary key system in laravel

To disable the default way primary keys works:

  • Replace the increments field for id with a string field.
// database\migrations\create_users_table.php
Schema::create('users', function (Blueprint $table) {
  $table->string('id');
  $table->string('name');
  $table->string('email')->unique();
  $table->string('password');
  $table->rememberToken();
  $table->timestamps();
});

Enter fullscreen mode Exit fullscreen mode
  • Set the incrementing property on the model to false:

// App\User.php

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

Enter fullscreen mode Exit fullscreen mode

Registering eloquent creating hook

On the model, we will register a creating hook, which laravel will execute before making a database query to save the record.

// App\User.php

/**
 * Boot the Model.
 */
 public static function boot()
 {
    parent::boot();

    static::creating(function ($instance) {
       $instance->id = uuid4();
    });
 }

Enter fullscreen mode Exit fullscreen mode

That's it ! Anytime we create a new database record of the User model, a UUID is generated and automatically saved into the database for it.

To test this out, I added a simple route, to create a user and return the newly created user.


// routes/web.php

Route::get('/', function () {
    return \App\User::create([
        'name' => 'bahdcoder',
        'email' => 'bahdcoder@gmail.com',
        'password' => bcrypt('password'),
    ]);
});


Enter fullscreen mode Exit fullscreen mode

If we check this out in the browser, we have this json response:


{
  "name": "bahdcoder",
  "email": "bahdcoder@gmail.com",
  "id": "c784692b-c7ea-4d76-9d5a-409df46d4cae",
  "updated_at": "2018-04-09 19:13:27",
  "created_at": "2018-04-09 19:13:27"
}

Enter fullscreen mode Exit fullscreen mode

Great! Automagically our UUID was generated when creating the User model.

Extra

Having to do this for all of your models would be a lot of duplication, so we could have a custom base model, that inherits from laravel's base model, and all of our models are going to extend from this one. So let's create a custom Model.php in our app directory:


// app/Model.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model as BaseModel;

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

    /**
     * Boot the Model.
     */
    public static function boot()
    {
        parent::boot();

        static::creating(function ($instance) {
            $instance->id = uuid4();
        });
    }
}


Enter fullscreen mode Exit fullscreen mode

Therefore for any further models created, they will extend this new app\Model.php.

  • Do not forget to change the $table->increments('id'); in your migrations to $table->string('id');.

Top comments (18)

Collapse
jakebman profile image
jakebman

Please don't use a random algorithm to generate UUIDs. You're defeating the exact purpose for which they were created.

Yes, 128 bits of randomness are not likely to collide (beating out all lotteries ever), but you know what's better than "not likely to collide"? "Guaranteed by construction to never collide with another UUID, ever"

GUIDs are designed to be unique, not random

Collapse
bahdcoder profile image
Kati Frantz Author

Hey, thanks a lot for your reply @jakebman . What's your suggestion for substituting randomness with uniqueness ?

Collapse
aabdyli profile image
Albi Abdyli • Edited on
//change 
$table->incrementing('id');
//to
$table->uuid('id');

laravel.com/docs/5.6/migrations#cr...

Collapse
aleksikauppila profile image
Aleksi Kauppila

You should check this library out: github.com/ramsey/uuid. It's very popular.

Thread Thread
aabdyli profile image
Albi Abdyli

That package is required by default by Laravel Framework so it is already present.

Collapse
jakebman profile image
jakebman

My apologies, that is a valid uuid4 method. I implied that it wasn't a valid UUID generator, and I was wrong on that point.

I'd still recommend not writing it yourself, and using a tested library implementation, which would provide other variants of UUID like 1, 3, or 5 that have the uniqueness guarantee and are harder to write yourself.

They also make it easier to follow the recommendations in this thread about not using the string representation of a UUID, which takes up more than twice the space in your database.

Collapse
adam_cameron profile image
Adam Cameron

I second the sentiment about not using randomness as a proxy for uniqueness. In general it's probably not gonna be a problem doing it your way, but if yer writing a blog article about it... best to demonstrate the "right" way.

There's an RFC for UUIDs, which should be the first port of call when considering this stuff: tools.ietf.org/html/rfc4122 (warning... like with all RFCs, it's pretty dry reading ;-)

Another consideration is make sure you use a DB which actually stores the UUID as an integer (or has a specific UUID type, which will be an integer under the hood), if you want the DB to scale. String-based UUID values don't index so cleanly. For small (and possibly medium) loads, this is unlikely to be an issue, but should be a consideration.

One way to mitigate this is to still use just an auto-increment int as the PK for a table and in FKs, but for stored objects that need to be fetched via a public interface, use a UUID for that lookup (for the less-hackable URLs, as per your article).

Collapse
jakebman profile image
jakebman

I jumped down Kati's throat a little hastily - uuid4() is RFC compliant as far as I could skim. It sets the correct bits for a version 4 UUID, which generates the rest of the bits randomly. That's a valid UUID.

Collapse
mjrider profile image
Robbert Müller

when using mysql as reference; Substitute integer with binary(16) because a integer is just 4 bytes long, and a UUID needs 16 bytes

Collapse
adam_cameron profile image
Adam Cameron

Oh sure, sorry I was not clear. I was talking about the distinction between the concepts of integers vs strings in the general sense, not an implementation-specific one.

I figured the size require was a given, but probably should not have assumed.

I see MySQL does not have an integer type that can contain a UUID:
dev.mysql.com/doc/refman/5.7/en/in...

That said, how well will a binary column work as a PK and for FK relationships?

Thread Thread
mjrider profile image
Robbert Müller

Bad,

with innodb, the primary key is added to every indexrow.

so lets say we have the following table
|uuid|name(10)|password(10)|
with indexes:

  • primary key uuid, index row length 16
  • index name, index row length 16+10
  • index name,password, index row length 16+10+10

this adds quite the overhead, i would go for the layout
|id(int)|uuid|name(10)|password(10)|
id: auto increment, only used for internal inter table relations
uuid: used as unique id on the outside of the application

where we guarantee that uuid will stay the same for ever, and is used the user identification

id could change when we export/import the dataset in a new application or when we connect to an external application
where

Collapse
mjrider profile image
Robbert Müller

Storing a UUID as a string is very bad for your database performance,
see also:

There are packages for using uuid's as primary key, but i never used them

Collapse
jenschude profile image
Jens

Please don't use mt_rand for generating UUIDs. As the PHP documentation notes this function does not generate cryptographically secure values and you will end up with a lot of collisions.

Better use random_bytes which is crypthographically secure. Example:

function uuidv4()
    {
        return implode('-', [
            bin2hex(random_bytes(4)),
            bin2hex(random_bytes(2)),
            bin2hex(chr((ord(random_bytes(1)) & 0x0F) | 0x40)) . bin2hex(random_bytes(1)),
            bin2hex(chr((ord(random_bytes(1)) & 0x3F) | 0x80)) . bin2hex(random_bytes(1)),
            bin2hex(random_bytes(6))
        ]);
    }

Kindly taken from paragonie.com/b/JvICXzh_jhLyt4y3

Besides that as noted in the different comments use some library which is able to generate the uuids in binary form and store them also as binary in the database.

Collapse
khairahmanplus profile image
Khai Rahman

On laravel 5.6, you can use Str::uuid() or Str::orderedUuid()

Collapse
billriess profile image
Bill Riess

Instead of creating a UUIDv4, you should use:

Str::orderedUuid();

The helper is already provided by Laravel and is meant for this exact purpose with the benefit of being ordered.

Collapse
biros profile image
Boris Jamot ✊ /
Collapse
pl profile image
Phil Lindsay

Hi, very interesting. Would you also need to add an index to the 'id' field in the schema since it's changed from 'increments' to 'string'?

🌱 DEV runs on 100% open source code known as Forem.

 
Contribute to the codebase or learn how to host your own.