UUID is an alternative primary key type for SQL database. It offers a number of benefits over the primary key in the standard integer style. Rails 6 released a new beta that introduced a new feature in ActiveRecord, making it easier to work with UUID primary keys. In this tutorial, we will dive into UUID with all their disadvantages and advantages.
Advantages of using UUID over integers
The UUID is a random string in a predefined format of the form like this:
1 2 | ccbb63c0-a8cd-47b7-8445-5d85e9c80977 |
UUID is superior to integer-based primary keys in many aspects. However, a warning might be the size of database indexes, but for tables without big data, you won’t notice the difference between integers and UUIDs.
Displaying non-public information in the URL
Primary key values are commonly used publicly in URLs and API network logs. In turn, people can estimate the total number of application resources and the growth rate of the application.
Do you really want to disclose how many users are signing up for your service or how many products you are selling with public URLs like:
1 2 3 | /orders/2234/checkout /users/287/profile |
This problem can be mitigated by adding slugs, but these are only unique keys that overlap with additional maintenance requirements.
Switching to UUID results in URLs cannot reveal any confidential information:
1 2 3 | /orders/cc7a4c8b-1a90-4287-a983-3f6e10bd88d4/checkout /users/6b6cabb3-e37d-4dd1-ae18-a4eb893b07ae/profile |
Violation of access scope
It would be quite difficult to properly access resources in web applications with unusual business logic. Rails makes it so easy as
1 2 3 4 5 6 7 8 | <span class="token keyword">class</span> <span class="token class-name">InvoicesController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">def</span> show <span class="token constant">Invoice</span> <span class="token punctuation">.</span> <span class="token function">find</span> <span class="token punctuation">(</span> params <span class="token punctuation">.</span> <span class="token function">fetch</span> <span class="token punctuation">(</span> <span class="token symbol">:id</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Instead of:
1 2 3 4 5 6 7 8 | class InvoicesController < ApplicationController ... def show current_user.invoices.find(params.fetch(:id)) end end |
This example may seem obvious, but in other applications – when a user has different roles and complex logic for who can access something, it is not always possible to prevent it. completely similar errors.
In the above example, if the invoice ID is of the UUID type, the attacker will not be able to sequentially scan integer ID values looking for security holes. This simple change makes a series of potential security flaws extremely difficult to exploit.
Anyway, I assert that using UUID will free you from restricting access to resources in your web application. However, it can save you in case similar security holes are detected in your project.
Independent of Frontend
The UUID primary keys allow frontend applications to independently create new objects, along with IDs, without having to talk to the backend. A unique ID can be generated with JavaScript code and the ability to duplicate existing objects is negligible.
This approach opens up a range of possibilities for frontend developers, for example, to create objects along with their links without calling the API.
Use UUID in Ruby on Rails application
You can create UUID with Ruby by:
1 2 3 4 | <span class="token keyword">require</span> <span class="token string">"securerandom"</span> <span class="token constant">SecureRandom</span> <span class="token punctuation">.</span> uuid <span class="token operator">=</span> <span class="token operator">></span> <span class="token string">"b436517a-e294-4211-8312-8576933f2db1"</span> |
To enable UUID in PostgreSQL, you need to create the following migration:
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">EnableUUID</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token keyword">def</span> change enable_extension <span class="token string">"pgcrypto"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Do not forget to run
1 2 | rails db:migrate |
You can now configure new tables to use UUIDs as their primary keys:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">class</span> <span class="token class-name">CreateUsers</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:users</span> <span class="token punctuation">,</span> id <span class="token punctuation">:</span> <span class="token symbol">:uuid</span> <span class="token keyword">do</span> <span class="token operator">|</span> t <span class="token operator">|</span> t <span class="token punctuation">.</span> string <span class="token symbol">:name</span> t <span class="token punctuation">.</span> timestamps <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Remember to set the correct foreign key data type on relational models. For this model case
1 2 3 4 5 6 | <span class="token comment"># app/models/user.rb</span> <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> has_many <span class="token symbol">:comments</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 | <span class="token comment"># app/models/comments.rb</span> <span class="token keyword">class</span> <span class="token class-name">Comment</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> <span class="token keyword">end</span> |
The migration to create comments looks like this
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">CreateComments</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:comments</span> <span class="token punctuation">,</span> id <span class="token punctuation">:</span> <span class="token symbol">:uuid</span> <span class="token keyword">do</span> <span class="token operator">|</span> t <span class="token operator">|</span> t <span class="token punctuation">.</span> string <span class="token symbol">:content</span> t <span class="token punctuation">.</span> uuid <span class="token symbol">:user_id</span> t <span class="token punctuation">.</span> timestamps <span class="token keyword">end</span> add_index <span class="token symbol">:comments</span> <span class="token punctuation">,</span> <span class="token symbol">:user_id</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
If you want all your future models to use UUIDs for primary keys by default, you need to add the following file.
1 2 3 4 5 6 | <span class="token comment"># config/initializers/generators.rb</span> <span class="token constant">Rails</span> <span class="token punctuation">.</span> application <span class="token punctuation">.</span> config <span class="token punctuation">.</span> generators <span class="token keyword">do</span> <span class="token operator">|</span> g <span class="token operator">|</span> g <span class="token punctuation">.</span> orm <span class="token symbol">:active_record</span> <span class="token punctuation">,</span> primary_key_type <span class="token punctuation">:</span> <span class="token symbol">:uuid</span> <span class="token keyword">end</span> |
How do I convert the primary key of a table from an integer to a UUID?
Changing the type of the primary key is not easy. First, start by running a similar migration, which will create a new uuid
column. Then rename the old id
column to integer_id
, un-set it as the primary key for the new uuid
column after renaming to id
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">class</span> <span class="token class-name">AddUUIDToUsers</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token keyword">def</span> up add_column <span class="token symbol">:users</span> <span class="token punctuation">,</span> <span class="token symbol">:uuid</span> <span class="token punctuation">,</span> <span class="token symbol">:uuid</span> <span class="token punctuation">,</span> default <span class="token punctuation">:</span> <span class="token string">"gen_random_uuid()"</span> <span class="token punctuation">,</span> null <span class="token punctuation">:</span> <span class="token keyword">false</span> rename_column <span class="token symbol">:users</span> <span class="token punctuation">,</span> <span class="token symbol">:id</span> <span class="token punctuation">,</span> <span class="token symbol">:integer_id</span> rename_column <span class="token symbol">:users</span> <span class="token punctuation">,</span> <span class="token symbol">:uuid</span> <span class="token punctuation">,</span> <span class="token symbol">:id</span> execute <span class="token string">"ALTER TABLE users drop constraint users_pkey;"</span> execute <span class="token string">"ALTER TABLE users ADD PRIMARY KEY (id);"</span> <span class="token comment"># Optinally you remove auto-incremented</span> <span class="token comment"># default value for integer_id column</span> execute <span class="token string">"ALTER TABLE ONLY users ALTER COLUMN integer_id DROP DEFAULT;"</span> change_column_null <span class="token symbol">:users</span> <span class="token punctuation">,</span> <span class="token symbol">:integer_id</span> <span class="token punctuation">,</span> <span class="token keyword">true</span> execute <span class="token string">"DROP SEQUENCE IF EXISTS users_id_seq"</span> <span class="token keyword">end</span> <span class="token keyword">def</span> down <span class="token keyword">raise</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">IrreversibleMigration</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
I will not go into details about how to migrate associations because it will be different for the use cases. You need to follow the same steps to add a new UUID type column and based on the keyword value in addition to the old integer, you must reassign the correct UUID keys. This can be time consuming.
UUID arrangement problem
Prior to Rails 6, trying UUIDs in your application could be a bit frustrating. Obviously first
and last
– the ActiveRecord::Relation
methods no longer work as expected, returning a seemingly random object from a collection.
Let’s look at an SQL query created by running User.last
1 2 | <span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> users <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> id <span class="token keyword">DESC</span> <span class="token keyword">LIMIT</span> <span class="token number">1</span> |
Integer primary keys are generated sequentially. We can safely assume that the most recently created object will have the highest ID value.
In contrast, due to the completely random nature of the UUID, it is generated in a non-sequential order. PostgreSQL can still organize them by algorithm determination. That means a single UUID from the table will always have the first position when sorting. Unfortunately, it has nothing to do with it being compared to other UUID values from the same table.
It results in a seemingly erroneous behavior of the first
and last
methods before Rails 6 because by default, they implicitly arrange relationships by ID values.
Changes in Rails 6
Rails 6 introduced a new implicit_order_value
configuration option for the ApplicationRecord
classes. You can use it like this:
1 2 3 4 5 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> implicit_order_column <span class="token operator">=</span> <span class="token string">"created_at"</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> |
With this setting, running User.last
now creates the following query
1 2 | <span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> users <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> created_at <span class="token keyword">DESC</span> <span class="token keyword">LIMIT</span> <span class="token number">1</span> |
The method then works as expected, even if it is using a non-sequential UUID for the primary key.
Using implicit_order_column
may cause a potential error. In case of multiple identical created_at values, running the above query will return unknown results. The timestamp values in Rails are accurate to milliseconds, so there may not be more than one object with the same creation time. But creating a series of objects is easy to happen.
Author’s contribution
The author of this post created a pull request related to the above mentioned problem and has been merged with Rails and will be released directly in version 6.0.2.
It modifies the implicit_order_column
behavior to subordinate results of the query by primary key if it is available. It ensures the result of determination regardless of the potential duplicate values in the implicit order column. An SQL query created by User.last
now looks like this:
1 2 | <span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> users <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> created_at <span class="token keyword">DESC</span> <span class="token punctuation">,</span> id <span class="token keyword">DESC</span> <span class="token keyword">LIMIT</span> <span class="token number">1</span> |
Use custom implicit arrangement in old Rails
You are stuck in an older version of Rails, but do you want to start using implicit_order_column
right now? You can check out the author’s new gem that supports this feature. It is a bit difficult, but it is using it without problems in the Abot project based on the author’s Rails 5.
summary
Switching to UUID as the default primary key type in your Rails application is worth a look. I didn’t think of a single case, but could only use the integer type as the primary key instead of UUID. When creating a new model, you cannot imagine all the possible business logic requirements that it will handle. Using UUID in the first place can help you reduce the cumbersome migration in the future.
Article translated from source . Thank you for reading.