DEV Community

Renzo Diaz
Renzo Diaz

Posted on • Edited on

How to use ULID as primary key Rails

A short story

Ruby on Rails is a powerful framework it is easy to create APIs and fast website development, one thing that came when I was developing an API was how to change the Integer auto-increment primary key because having a predictable Id can be risky in terms of security if you share it for a client-size application, the user can guess ids and modify the database in the worst of the case, at that moment I had to research some alternatives and I found UUID a non-sequential data type that can be easily be implemented in Rails, so I've tried it and everything was ok until I had to sort ASC and DESC, I realize that the methods Model.first and Model.last doesn't work as expected because Rails by default uses them with the integer sequential id type, so as UUID is non-sequential it generates a random Hash like this 123e4567-e89b-12d3-a456-426614174000 as it doesn't have a sequence Rails can't figure out how to sort the data, then I researched a little bit more to see if there is any other solution and I found that we can use self.implicit_order_column = "created_at" in the Model, with this, those methods work ok it sorts depending on the DateTime created and I was happy. Not for a long time, when another problem came up! When I used seeds to fill a bulk of fake data into the database, all the data had the same DateTime and the sorting failed again, it was the same issue as the initial one. I've decided to look for another alternative and I found ULID, using this I had everything I've needed the sorting works as expected, it isn't predictable and the store in memory is pretty good. I'm still using it and I don't have any issues.

How to implement ULID in Rails?

There is already a gem for ruby add it to your Gemfile

#Gemfile
...
  gem 'ulid'
end

Then run bundle install to install it. Then update your migration or if you are creating a new one should look like this.

# migration
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
   # id: false, to not use id int as default
   create_table :users, id: false do |t|
     # here we create our id
     t.binary :id, limit: 16, primary_key: true

     t.string :given_name
     t.string :family_name
     t.string :email, unique: true, index: true
     t.string :username, unique: true, index: true
     t.string :password_digest
     ...
     t.timestamps
   end
  end
end

First, we disable the autogenerated id by putting id: false and create our own id t.binary :id, limit: 16, primary_key: true what we need to do is fill the id before create, we can do it on each Model but my approach was to create a concern to just include it in the application_record.rb file so it will apply for all the models.

# app/models/concenrs/ulid_pk.rb
require 'ulid'

module UlidPk
  extend ActiveSupport::Concern

  included do
    before_create :set_ulid
  end

  def set_ulid
    self.id = ULID.generate
  end
end

Then in the application_record.rb include it.

# app/models/concenrs/ulid_pk.rb
class ApplicationRecord < ActiveRecord::Base
  include UlidPk
  self.abstract_class = true
end

That's it, now it will autogenerate the id before create.

Note

if you need to create an association you should put the type on it.

# migration
class CreateEvents < ActiveRecord::Migration[6.0]
  def change
   # id: false, to not use id int as default
   create_table :users, id: false do |t|
     # here we create our id
     t.binary :id, limit: 16, primary_key: true

     t.string :name
     t.string :description
     ...
     # Specify the type: binary
     t.references :user, type: :binary, foreign_key: true, index: true

     t.timestamps
   end
  end
end

Hope this can help you, I'll try to make it more autogenerated when I get the chance to avoid put type: :binary or id: false manually. If you have any question feel free to reach me out. Cheers!

Buy me a coffee

Top comments (1)

Collapse
 
samuelodan profile image
Samuel O'Daniels

This was helpful. Thanks.