When I am building a Rails app that I expect to be public facing in some sort of capacity, I don’t want to be displaying auto-incrementing, integer based IDs in my URLs. Not only does it visually look better, in my opinion. It is a security enhancement by removing the predictability of record discovery via the URL. Though, as ingrained as auto-incremented integer primary keys are in ActiveRecord, I wouldn’t want to rock the boat too much. Some suggest using UUIDs but I find them to be too long for URLs in my taste. So, I set out on how to use Hashes as IDs in my URL and would like to share that with you.
This tutorial is going to assume you already have existing models, however, you will simply just need to add a `hash_id:string` column to your new models. Additionally, you will need to install the friendly_id gem.
# Gemfilegem 'friendly_id', '~> 5.1.0'
…and of course
bundle install
Perfect, we have the gem needed to do the hashed ID lookups on our models. Next, we need to include the functionality into our models. I chose to create a model concern that will be able to be included in any model we want the Hashed ID functionality on…
# app/models/concerns/friendlyable.rb
module Friendlyableextend ActiveSupport::Concern
included doextend ::FriendlyIdbefore_create :set_hash_idfriendly_id :hash_idend
def set_hash_idhash_id = nilloop dohash_id = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])break unless self.class.name.constantize.where(:hash_id => hash_id).exists?endself.hash_id = hash_idend
end
Let’s go over that file. The first two lines are standard model concern boilerplate.
included doextend ::FriendlyIdbefore_create :set_hash_idfriendly_id :hash_idend
The include block is basically allowing you to insert lines into your model file as if you were to put them there manually. Thus, we’re adding FriendlyId functionality, adding a before_create callback, and lastly telling FriendlyId we’re using the hash_id column has the FriendlyId lookup column.
def set_hash_idhash_id = nilloop dohash_id = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])break unless self.class.name.constantize.where(:hash_id => hash_id).exists?endself.hash_id = hash_idend
In the second half of the file, we’re defining the method called from the before_create hook. Essentially, we’re creating a loop to create a URL safe hash, and set the hash_id attribute if that hash_id does not collide with any existing records. If it’s successful, we break the loop and set the attribute for ActiveRecord to save.
Now that we have the model concern ready to go, it’s really easy to include it in a model:
class User < ApplicationRecordinclude Friendlyable
...
end
…and that’s it! Let’s add the migration to finish this off.
class AddHashIdToUsers < ActiveRecord::Migration[5.0]def upadd_column :users, :hash_id, :string, index: true
User.all.each{|m| m.set_hash_id; m.save}end
def downremove_column :users, :hash_id, :stringend
end
The migration will add the column, make it indexed, and then update any existing records to have a hash_id.
The last piece here will be looking up records via FriendlyId, which is a simple update to any finds in your app:
User.friendly.find(params[:id])
…which will use the primary ‘id’ key or your ‘hash_id’ to look up records. There you have it! From this point forward you can be using URLs like http://localhost:3000/users/90upoijsz in your Rails application.
PS- This concept and many otherw are used in my new book for Building a SaaS Ruby on Rails 5(https://BuildASaaSAppinRails.com) and it’s on presale now.