I’ve seen a lot of examples of this online, and it took a while for me to get it right, so I decided to document it.
The two cases I’ve used this for:
/u/amingilani
/transactions/601585f7–0f4a-41e8-bd04-b2eb24262fb4
Both cases differ only by the fact that the slug is randomly generated in the latter.
Quick definitions:
Slug
: part of the URL to identify the recordPrimary key
: a unique identifier for database recordsUUID
: Universal unique identifier, a 128 bit generated identifier that depending on the implementation is either guaranteed or is very likely to be unique.Overview:
slug
column to our database tableslug
column to our databaseWe’ll need to ensure that our slug is unique, and always present. You can name it “username” or anything more semantic if it’s meant to be part of the model.
class AddSlugToRecommendations < ActiveRecord::Migration[5.1]def changeadd_column :transactions, :slug, :string, null: falseadd_index :transactions, :slug, unique: trueendend
We’ll use the securerandom
library that ships with rails to generate a unique UUID. If the UUID (by whatever sorcery) already exists, a new one will be generated instead.
This is only important, if we’re trying to obfuscate our URLS. Ignore this step if you’re letting users set something like username
class Transaction < ApplicationRecordbefore_create :set_slug
private
def set_slugloop doself.slug = SecureRandom.uuidbreak unless Transaction.where(slug: slug).exists?endendend
This will have to be set in multiple places:
:slug
param in routes:slug
paramModel#to_param
method to return theslug
attribute instead of id
Tell your routes to use the slug param:
Rails.application.routes.draw doresources :transactions, param: :slugend
Now your routes will look like this:
edit_transaction GET /transactions/:slug/edit(.:format)transaction GET /transactions/:slug(.:format)PATCH /transactions/:slug(.:format)PUT /transactions/:slug(.:format)DELETE /transactions/:slug(.:format)
For your controller to lookup records by the slug, stop using the id
and use the slug
param instead:
class TransactionsController < ApplicationControllerbefore_action :set_transaction, only: [:show, :edit, :destroy]
private
def set_transaction@transaction = Transaction.find_by slug: params[:slug]endend
Let’s override the Model#to_param
method so that form_for @transaction
works:
class Transaction < ApplicationRecord...def to_paramslugend...end
Unless you’re creating a distributed application with distributed database nodes that need a collision free method to generate primary keys, you don’t need randomly generated ids. You’re just looking for a quick and easy way to hack URLS, which is why this method is awesome.
Using a string as a primary key is significantly slower than an integer during lookups, so every Model#children
call will be slower.
Using a slug for URL lookups means the slow performance will be limited to URL lookups, which is what you wanted in the first place.