How to use ULID as primary key Rails
Tags: rails, postgres, tutorial, security
Published: 2020-05-21 19:54:29


### 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](https://en.wikipedia.org/wiki/Universally_unique_identifier) 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](https://github.com/ulid/spec), 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](https://github.com/rafaelsales/ulid) for ruby add it to your `Gemfile`

```ruby
#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.

```ruby
# 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.

```Ruby
# 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.

```ruby
# 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.

```ruby
# 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!

[<img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee" style="display: inline !important;">](https://www.buymeacoffee.com/renzodiaz)