Words and Code

One writer’s journey from words to code.

You’ve Got a Friend in Friendly_Id

#technicaltuesdays, rails, ruby


One of my favorite aspects of programming is the fact that there’s always more than one way to do something. In fact, I think this is probably why I consider the very act of writing and building software to be a far more intutive task, rather than a merely structured, logical, and rigid pursuit. The very notion of no “one single solution” to solving a problem is what makes programming both a critical thinking skill, but also a creative one.

Let me give you an example: a few months ago, I wrote about using slugs and the acts_as_url gem to create human-readable urls for an application I was working on. Rails has a handy to_param method that can be used in conjunction with this gem to generate a hyphen-separated string that can be used in the show route of a resource in place of the object’s numeric id, which is both unreadable and usually doesn’t provide any point of reference for the user. So, here we are, using slugs.

However, just because we did something one way initially doesn’t mean that our way is the only way to do it. Actually, I am certain that there are other solutions — and some of them might actually be more flexible than our approach! This was what I realized very quickly when I recently learned about another gem that solves the same problem of slugs in a different, and rather interesting way! In fact it took what I already knew about using and generating slugs in an url structure to another level by using text-based identifiers in place of ids. Basically, it allows us to query for objects by finding them using their slugs, rather than their ids. Doesn’t this make you super excited? Time to find out more about this approach and become friends with the friendly_id gem!

Making Friends With Friendly_Id


The friendly_id gem, created and maintained by Norman Clarke, describes itself as “the Swiss Army bulldozer of slugging and permalink plugins for ActiveRecord”. And that’s probably a pretty accurate name for all the things that this one single gem is capable of doing!

Before we can really explore all of its neat features, we need to do some initial setup. We’ll start by adding friendly_id to our Gemfile, and then running bundle:

gem 'friendly_id', '~> 5.1.0'

It’s worth mentioning here that if we’re running on Rails 4.0 or higher, we must use friendly_id 5.0.0 or greater.

Next, we know we’ll need to add a slug column to the table in our database that we want to implement friendly_id on. We’ll add a publishers table to our bookstore application, and assure that they always have a name and a slug column when they are committed to the database. We’ll also add a unique index to the slug column, since we’ll be using the slug in our urls, which means that no two publisher instances should have the same slug — and also because we’ll want to be able to look up publishers by their slug rather than by their id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CreatePublishers < ActiveRecord::Migration
  def self.up
      create_table :publishers do |t|
        t.string :name, null: false
        t.string :slug, null: false
      end

      add_index :publishers, :slug, unique: true
  end

  def self.down
      drop_table :publishers
  end
end

It’s worth nothing that if we were adding friendly_id to rows that already existed in our database, we would need to generate slugs for these preexisting objects. We have a few options on where to put this command — in a rake task or from the console, for example — but we’d have to find each of our objects and call save on them to generate their slugs. For example, if we were implementing friendly_id on our preexisting Author class, we would run this line:

Author.find_each(&:save)

Now, time to extend or include the FriendlyId module in our model; that’s right, it doesn’t matter which one you do, just as long as the model has access to the methods defined in the module:

1
2
3
4
5
class Publisher < ActiveRecord::Base
  extend FriendlyId
  
  validates :name, :slug, presence: true
end

Now comes the actual implementation! We need to use one method in particular in order to configure the way that the FriendlyId module will behave inside the context of the model. This method is aptly named as: friendly_id, which is essentially just the “base” method of the FriendlyId module:

1
2
3
4
5
6
class Publisher < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: :slugged

  validates :name, :slug, presence: true
end

This method sets the default configurations of what method (yes, a method and not a column in the database!) it should use when trying to find an object. It also allows you to pass an options hash, which is what we’re doing when we pass it use: :slugged. We’re effectively telling the friendly_id method to use the slugged addon.

So now that we’ve got the most basic implementation set up, what does this allow us to do, exactly? Well, given our current model, we can now find instance of our Publisher class by their name:

1
2
3
Publisher.friendly.find('random-house')
#=> finds and returns an instance of the Publisher class
with a slug 'random-house'

Cool! We’re doing almost what Rails’ ActiveRecord find method would do, but we’re now no longer finding by a numerical id, but a string identifer that actually means something to both us as programmers, and our users!

But what if we didn’t want to litter our codebase with friendly.find everywhere? There’s a solution for that, too. We just need to use another addon, which isn’t implemented by default, called finders:

1
2
3
4
5
class Publisher < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: [:slugged, :finders]
  validates :name, :slug, presence: true
end

This allows us to invoke find directly — but we have to be careful with this because it could conflict with other places where we are using ActiveRecord’s find(id) method. Now we can do something like this very easily:

1
2
3
Publisher.find('random-house')
#=> finds and returns an instance of the Publisher class
with a slug 'random-house'

As we continue to implement friendly_id on other models in our application, we’ll need to keep in mind that any classes that participate in single-table inheritence must extend FriendlyId in both the parent classes, and all its children classes as well.

But what else can this gem do? It’s time to finally start playing around with all of its functionality!

Where You Lead, Friendly_Id Will Follow


When the documentation called this gem the “Swiss Army bulldozer” of slug url generation, it wasn’t kidding! There really is a ton that we can do with the various modules and addons provided to us by friendly_id. We’ll explore just a handful of things that we can modify for different use cases.

Should Generate New Friendly Id?

One question we should answer off the bat is how exactly this gem actually decides to generate a slug. It turns out that the friendly_id gem has a should_generate_new_friendly_id? method, which determines when and whether to generate a new slug. A peek into the source code of this gem reveals that this method just checks whether there is a slug column defined, and the base method on the FriendlyId module configurations has been called or not:

1
2
3
4
def should_generate_new_friendly_id?
  send(friendly_id_config.slug_column).nil? &&
      !send(friendly_id_config.base).nil?
end

The documentation points out that it is totally fine to override this method in our models — for example, if we only wanted our slugs to be generated once upon creation, and never updated.

Duplicate Ids

By default, friendly_id expects the slug values that we told it to use in our model to be unique. It also helps that we assured that this is the case by creating a unique index on our slug column. But, what happens if an admin (or even a user) tries to create an object that would create a duplicate friendly_id? Well, the gem handles this case in a pretty cool way: it just appends a UUID to the generated slug to ensure that it will be a unique value:

1
2
3
4
5
publisher_a = Publisher.create(name: "Harper Collins")
publisher_b = Publisher.create(name: "Harper Collins")

publisher_a.friendly_id #=> "harper-collins"
publisher_b.friendly_id #=> "harper-collins-102-c9f6749b-daez-4586-a21x-waz87ak16oe2"

Pretty awesome, right? It really does seem like this gem is a developer’s best friend!

Slug Candidates

As nice as it is that we have the functionality to append an UUID sequence to prevent non-unique slugs, it would also be nice if we had some control over how to modify a potential clash in friendly_id identifiers. Well, our wish is this gem’s command! We can use a lovely “candidates” feature (new in version 5.0 of this gem!) to set up a list of alternate slugs that we can use to distinguish records in place of sequence.

We’ll first add two required columns to our publishers database, city and country:

1
2
3
4
5
6
class AddCityAndCountryToPublishers < ActiveRecord::Migration
  def change
      add_column :publishers, :city, :string, null: false
      add_column :publishers, :country, :string, null: false
  end
end

After we run rake db:migrate, and make sure that these values are all populate in our pre-existing records, we’ll tell the friendly_id base method to use the slug_candidates method, which is going to be a set of instructions on how to construct the slug for any given instance of our Publisher class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Publisher < ActiveRecord::Base
  extend FriendlyId
  friendly_id :slug_candidates, use: :slugged

  validates :name, :slug, :city, :country, presence: true

  def slug_candidates
      [
          :name,
          [:name, :city],
          [:name, :city, :country]
      ]
  end
end

You’ll remember that I mentioned that friendly_id uses a method, and not a column in the database to generate a slug — well, this is exactly why it does that: so that we can override a method very easily! Now the friendly_id base method will use first the name attribute, then the name and city attributes, followed by the name, city, and country attributes.

1
2
3
4
5
publisher_a = Publisher.create(name: "Harper Collins", city: 'New York City')
publisher_b = Publisher.create(name: "Harper Collins", city: 'San Francisco')

publisher_a.friendly_id #=> "harper-collins-new-york-city"
publisher_b.friendly_id #=> "harper-collins-san-francisco"

It’s worth noting that our method doesn’t have to be named slug_candidates in the context of our class: this is just the base method of our FriendlyId module, which means that we can mame it anything we want, so long as we pass it to our friendly_id method when we tell it what we want to use to generate our ids for finding our objects. The nice thing about using an array of symbols (as opposed to string literals or procs and lambdas), is that the FriendlyId module will invoke a method of the same exact name on our Publisher model, which can be helpful if we have a city and country attribute on each of our publisher instances, which maps to a column in our database.

Of course, if we do happen to have an edge case where two instances of a Publisher have the exact same name, city, and country, the friendly_id gem can handle this situation as well! What will it do, exactly? Here’s what the documentation says:

“If none of the [slug] candidates can generate a unique slug, then FriendlyId will append a UUID to the first candidate as a last resort.”

Nice! So we can always depend on our slugs being unique in some way or another — in the worst case (which probably won’t even happen that often!), it’ll just add a UUID at the end of the slug that matches another one, making it unique.

Saving Old Friends

We’ve been working mostly with the slugged addon, but there are also quite a few other addons available to us. One of the most interesting ones is the history addon, which allows us to save different versions of an instance’s slug attributes!

For example, if we had Article instances that might allow for their titles to be updated by admins, we wouldn’t want all of our urls to be broken when an admin changed a title on an article, right? Well, this addon helps us prevent that.

In order for us to implement this module, we need add a table to your database to store the slug records. Luckily, friendly_id has a generator for this:

1
2
rails generate friendly_id
rake db:migrate

Now we just need to specify that our base method needs to use the history addon:

1
2
3
4
class Article < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: :history
end

And now in our controller we can do something like this!

1
2
3
4
5
6
7
8
9
10
class ArticlesController < ApplicationController
  before_filter :find_article

  def find_article
      @article = Article.find params[:id]

      # If an old id is used to find the record,
      # we can handle a can redirect accordingly!
  end
end

There are so many interesting use cases for this gem, and it turns out that it does a lot of the stuff we already know about under the hood. One quick example: it uses Rails’ ActiveSupport parameterize method, which is actually used by to_param — and which we have already explored on our own!

So, there’s never just one way to do anything. As long as we’re willing to learn the fundamentals of how to solve a problem in one way, we can explore all the different solutions that people have already come up with. And when it comes to generating urls and handling strange situations with slugs, we’ve got it covered with our new best friend, the friendly_id gem.


tl;dr?

  • The friendly_id gem is a way to find objects and generate urls using strings instead of numerical ids. The documentation for this gem is fantastic, check it out!
  • Here’s a very simple railscast that implements friendly_id in its most basic capacity.
  • Curious about how that friendly_id base method works? Check out the source code.