Renzo Diaz
Posted on May 21, 2020
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!
Posted on May 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.