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:
- Vanity URLs, e.g. trying to open a user’s profile at
/u/amingilani
- Obfuscated URLs, e.g. trying to make the URL difficult to guess such as
/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:
- We’ll add a unique
slug
column to our database table - We’ll generate a unique slug on creation (only for the 2nd use-case)
- We’ll use the slugs in URLs
Adding a slug
column to our database
We’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
Generating a unique slug on creation
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
Use the slug in URLS
This will have to be set in multiple places:
- use the
:slug
param in routes - lookup records using the
:slug
param - Override the
Model#to_param
method to return theslug
attribute instead ofid
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
Why my way is the best
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.