How to start using UUID in ActiveRecord with PostgreSQL
… and check why 5600+ Rails engineers read also this
How to start using UUID in ActiveRecord with PostgreSQL
Although it may be obvious for many developers, there are still some that are not aware of great features that PostgreSQL allows to use with ActiveRecord. This tutorial is intended to reveal UUID type, which we can use in our Rails applications, especially as models’ attributes.
Special Postgre’s data types are available in our databases by enabling so called extensions. According to the documentation:
- PostgreSQL is extensible because its operation is catalog-driven.
- The catalogs appear to the user as tables like any other.
- PostgreSQL stores much more information in its catalogs like data types, functions and access methods.
- These tables can be extended by the user.
- Traditional database systems can only be extended by changing source code or by loading modules written by the DBMS vendor.
So not only we get a possibility to create our own extensions, but we get a bunch of useful features out of the box as well. Let’s see one of them in action right now.
Setup
You can follow all of presented steps with your brand new Rails application. To create one, for the purpose of this tutorial, you can run:
rails new -T -J -V -S postgres-extensions --database postgresql
We skipped some tests, javascripts, views and sprockets and set our database to PostgreSQL.
UUID
Personally I think that UUID is extremely interesting topic to discuss and Andrzej has already written an excellent article about using this feature.
This is 16-octet / 128 bit type compatible with most common GUID and UUID generators, supporting distributed application design, defined by RFC 4122, ISO/IEC 9834-8:2005. It is represented by 32 lowercase hexadecimal digits, displayed in five groups separated by hyphens, in the form 8-4-4-4-12 for a total of 36 characters (32 alphanumeric characters and four hyphens).
Versions
Although UUID might appear in different versions (MAC address, DCE security, MD5 or SHA-1 hash), the most generators relies on random numbers and produces UUID version 4.
Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx where x is any hexadecimal digit and y is one of 8, 9, A, or B.
So the validation regexp may be as follows:
/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/
Ruby stdlib
How can we produce it in Ruby?
[1] (pry) main: 0> require 'securerandom'
true
[2] (pry) main: 0> SecureRandom.uuid
"624f6dd0-91f2-4026-a684-01924da4be84"
It’s available since Ruby 1.9
and provides UUID version 4 described above, which is derived entirely from random numbers.
Psql
How to use it in SQL code?
postgres=# CREATE EXTENSION "pgcrypto";
CREATE EXTENSION
postgres=# SELECT gen_random_uuid();
gen_random_uuid
--------------------------------------
f8c9ffd6-a234-4729-bd2a-68379df315fb
(1 row)
The pgcrypto module provides functions to generate universally unique identifiers (UUIDs) using one of several standard algorithms. There are also functions to produce certain special UUID constants.
Rails finally
Let’s see how can we use it in our applications. To enable this extension in our database through rails, we can use convenient helper method for PostgreSQLAdapter
:
rails g migration enable_uuid_extension
class EnableUuidExtension < ActiveRecord::Migration
def change
enable_extension 'pgcrypto'
end
end
We can create our model now:
rails g model book
class CreateBooks < ActiveRecord::Migration
def change
create_table :books, id: :uuid do |t|
t.string :title
t.timestamps
end
end
end
# app/models/book.rb
class Book < ActiveRecord::Base; end
And then we can play with them a little bit:
2.1.2 :001 > Book.create
(0.1ms) BEGIN
SQL (0.9ms) INSERT INTO "books" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2014-10-01 10:30:12.152568"], ["updated_at", "2014-10-01 10:30:12.152568"]]
(0.4ms) COMMIT
=> #<Book id: "adf5efad-7c72-4e3f-9b1a-922fdbf6ebdf", title: nil, created_at: "2014-10-01 10:30:12", updated_at: "2014-10-01 10:30:12">
2.1.2 :002 > _.id
=> "adf5efad-7c72-4e3f-9b1a-922fdbf6ebdf"
So we created our book with auto-generated id
as a UUID, great!
But what if I need just another field of uuid
type? We can do it too.
rails g migration add_uuid_to_books uuid:uuid
class AddUuidToBooks < ActiveRecord::Migration
def change
add_column :books, :uuid, :uuid, default: 'uuid_generate_v4()'
end
end
After migration we have:
2.1.2 :001 > Book.create
(0.1ms) BEGIN
SQL (0.2ms) INSERT INTO "books" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2014-10-01 10:39:19.646211"], ["updated_at", "2014-10-01 10:39:19.646211"]]
(0.9ms) COMMIT
=> #<Book id: "e15b0c03-3ff0-46c2-99b2-76406da80b3a", title: nil, created_at: "2014-10-01 10:39:19", updated_at: "2014-10-01 10:39:19", uuid: nil>
2.1.2 :002 > Book.first
Book Load (0.4ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT 1
=> #<Book id: "e15b0c03-3ff0-46c2-99b2-76406da80b3a", title: nil, created_at: "2014-10-01 10:39:19", updated_at: "2014-10-01 10:39:19", uuid: "0699c100-4c6e-4dc3-b72f-91bac8847304">
And we’re done. Note that UUID is accessible only after retrieving a record from the database (or reloading it in place), not immediately in brand new created object, because we get UUID generated from Postgres and not Rails itself.
Threats and inconveniences
To be honest I should mention about some drawbacks right now. Maybe they are not crucial, but it’s worth to be aware that they exists.
Some inconvenience may be referencing with a foreign key to associated model. We can’t just add reference column like:
rails g migration AddAuthorRefToBook
because it produces:
class AddAuthorRefToBook < ActiveRecord::Migration
def change
add_reference :books, :author, index: true
end
end
Which may seems OK at the first sight, but it’s actually a little bit tricky. We have the following models:
class Book < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :books
end
and we are trying to create an association:
2.1.2 :001 > Author.create.books.create
(0.1ms) BEGIN
SQL (0.8ms) INSERT INTO "authors" DEFAULT VALUES RETURNING "id"
(1.0ms) COMMIT
(0.1ms) BEGIN
SQL (0.3ms) INSERT INTO "books" ("author_id") VALUES ($1) RETURNING "id" [["author_id", 49624675]]
(0.3ms) COMMIT
=> #<Book id: "38c66078-9e03-45dc-8b78-408a1a41b55c", title: nil, uuid: nil, author_id: 49624675>
2.1.2 :002 > Book.first
Book Load (0.8ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT 1
=> #<Book id: "38c66078-9e03-45dc-8b78-408a1a41b55c", title: nil, uuid: "3caa9344-c7e3-4a9e-abb4-e44d1b857d25", author_id: 49624675>
2.1.2 :003 > Author.first
Author Load (0.4ms) SELECT "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT 1
=> #<Author id: "49624675-3386-4423-8cb6-70916972fe34", name: nil>
See what happened? If not, take a look:
2.1.2 :004 > Book.first.author
Book Load (0.3ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC LIMIT 1
Author Load (1.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 49624675]]
PG::InvalidTextRepresentation: ERROR: invalid input syntax for uuid: "49624675"
: SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1
ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input syntax for uuid: "49624675"
: SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1
What is wrong? add_reference
associates model by default integer ID, which is not present in our database. Here’s created schema:
create_table "books", id: :uuid, default: "uuid_generate_v4()", force: true do |t|
t.string "title"
t.uuid "uuid", default: "uuid_generate_v4()"
t.integer "author_id"
end
Instead we should have a string field for referencing UUID, so any time we connect two tables, we can make a proper association. Fortunately it’s a small change:
class AddAuthorRefToBook < ActiveRecord::Migration
def change
add_column :books, :author_id, :uuid
end
end
which creates:
create_table "books", id: :uuid, default: "uuid_generate_v4()", force: true do |t|
t.string "title"
t.uuid "uuid", default: "uuid_generate_v4()"
t.uuid "author_id"
end
and that is what we’re talking about.
2.1.2 :001 > Author.create.books.create.author
(0.1ms) BEGIN
SQL (1.0ms) INSERT INTO "authors" DEFAULT VALUES RETURNING "id"
(0.4ms) COMMIT
(0.1ms) BEGIN
SQL (0.5ms) INSERT INTO "books" ("author_id") VALUES ($1) RETURNING "id" [["author_id", "e6ebe66f-3c6e-4a1f-ae67-2c5ddfce46f7"]]
(0.2ms) COMMIT
=> #<Author id: "e6ebe66f-3c6e-4a1f-ae67-2c5ddfce46f7", name: nil>
How unique is universally unique identifier?
While the UUIDs are not guaranteed to be unique, the probability of a duplicate is extremely low. The UUID is generated using a cryptographically strong pseudo random number generator. There’s very slight chance to get the same result twice. Wikipedia provides some nice explanation of possible duplicates.
Conclusion
PostgreSQL offers many more extensions and types out of the box, that are compatible with Rails in easy way. What might be worth to check out are:
The rest will be covered in further blogposts very soon.
Would you like to continue learning more?
If you enjoyed the article, subscribe to our newsletter so that you are always the first one to get the knowledge that you might find useful in your everyday Rails programmer job.
Content is mostly focused on (but not limited to) Ruby, Rails, Web-development and refactoring Rails applications.