Solutions for Slugs of All Sizes: Acts_as_url + To_param

来源:转载

Last week was my first week of working from home, which meant two things: spending a lot of time with my computer and limited time with other human beings and, more importantly, debugging things on my own without having anyone nearby to ask for help. The latter of the two actually ended up reaffirming the fact that I actually candebug a decent amount of things on my own if I just power through and am stubborn enough to not give up.

I also learned something interesting about the debugging process: while we’re learning, we often solve the same problem again and again. At least, this was the case for one of the features I was working on which involved using an object’s slug to generate a url. As I started thinking through how to approach solving this, I immediately had the feeling that I had done something similar in another project. Digging through another repository’s source code confirmed my suspicions, and I rediscovered the stringexgem and its multiple libraries, including acts_as_url!

All of this begs the question: why didn’t I remember that this gem existed — or that I had already used it? My guess is that it’s because I neither wrote about it nor understood how it worked until a few days ago. This week, it’s time to rectify that situation and dive into the acts_as_urllibrary and find a solution for all slug problems, once and for all!

The Other Kind of Slug

We all are familiar with the Rails mantra of convention over configuration, aka making the lives of developers easier by eliminating the need for them to make decisions about how to structure their code. Now, this design paradigm is pretty fantastic, particularly when we’re first learning a new framework. But eventually, there comes a time when we need to tweak our code’s conventions just a tiny little bit.

What’s an example of this? Well, take Rails convention of finding an object by it’s id. This standardization pops up all over the place, but the one that we’re particularly concerned with is the generation of a url. By default, Rails applications will build a URL path for the showaction of any given controller based on the primary key (aka the idcolumn in our database) of the object that we’re trying to “show”.

Let’s put this in context of our bookstore application. We have a bunch of Bookobjects, and we want to iterate through their titles and then link to their individual “show” pages. A very basic, not-at-all-fancy template might look something like this:

It’s important to note that we’re actually passing in the bookinstance here — an ActiveRecordobject, and notthe book’s id. Why is this important? Because there’s a method that Rails is using to convert the Bookobject into a URL in order to generate the correct address for our book_path. That’s why we need to pass it an ActiveRecordobject, because the method that’s being called expects an object and returns the idas parameters in the url. What does this look like, exactly? Well, right now our book_pathtakes a Bookinstance and creates a path that looks like this: localhost:3000/books/25.

Which is fine! Actually, it’s more than fine: it’s the expected behavior given our Rails mantra of convention over configuration. But, what if we actually wantto configure this a little bit more. What if, instead of using the primary key of our Bookinstances, we wanted to use the title of the book? It would be lovely if we could link someone to a particular book’s page with a more human-readable url (for example, something like awesomebookstore.com/books/the-bell-jarin production).

There’s a solution for this problem, and it’s called slugs. Slugs are a solution for semantic URL generation, which is also sometimes referred to as “RESTful” or “SEO-friendly” URL generation. As our application grows, not only would we want our URLs to be user-friendly, but we probably also will want them to be optimized for search engine results. So, we need to change our application’s configuration to use a slug.

Protip:if anyone ever pop quizzes you about where the term “slug” comes from, you can totally school them with the following interesting fact: a “slug” used to be a shorter namegiven to a newspaper article while it was in production; during the editing process, the article would be labeled by its slug, which would more specifically indicate the content of the story to the editors and reporters. The more you know, amirite?

There are obviously a lot of ways to approach this, but why reinvent the wheel by writing a bunch of methods that someone else has already written? Let’s make use of someone’s open source work and use the acts_as_urllibrary to get the job done for us!

Don’t Let Slugs Slow You Down

The acts_as_urllibrary is actually part of the stringexgem, which adds some useful extensions to Ruby’s Stringclass. After we add this gem to our Gemfile( gem "stringex") and run bundle install, we can get started doing a quick setup.

The documentation for this library is fairly straightforward, and a quick read-through gives us a good idea of what we need to do in order to make it work properly. The basic implementation of this library is four-fold:

We need a column in our database that will map to the attribute used in generating our url. We need to call the acts_as_urlmethod in our model using the attribute name that we want to use for generating our url. We need to override Rails’ to_parammethod (Confused? Hang tight, we’ll get there in a second!) We need to find_byour new url attribute inside of our controllers.

Let’s take it step by step. First, we’ll write a migration that will add a slugcolumn to our booksdatabase. This is going to be the column that will map to a slugattribute on our Bookobjects:

The default behavior of this method expects that we have a column and attribute called urlon our object. Since we aren’t using the default attribute name, we need to specify the name of the attribute that we’re using to store the generated url string. Thankfully, acts_as_urltakes a bunch of options, including url_attribute, which is what we’re using here. There are some other useful options worth checking outin the documentation, including scope, limit, truncate_words, and blacklist.

Next, we’ll need to override Rail’s to_parammethod in order to actually useour generated url attribute. Basically, we’ll just want to write our own to_parammethod and return our slugattribute from inside of it.

And finally, we need to make sure that we’re finding our object using the appropriate attribute from within the context of our controller. The documentation suggests we use the find_by_urlmethod, but we could also use the find_bymethod in our controller as well.

Nothing changes about how our controller works and we can still do all the fancy things we were doing before, like use a decorator (Remember those?). The only thing that happens now is that our original book_pathhelper will now use the bookinstance we passed it to generate a url with a slug instead of the primary key!

Success! We’ve done it! Actually, we’ve almostdone it. One tiny little thing that I always forget is all of the books that already exist in our database. What about them? They all have a slugattribute, sure, and a slugcolumn – but there’s a slight problem: the column is empty! So we can’t find_bythe slugattribute for those books, can we? In fact, if we try to call to_paramon any of our preexisting Bookinstances right now, all we’ll get is nil!

Now all of the Bookinstances that had empty slugattributes have ben initialized, and we’re good to go! Right? Wrong. Because I haven’t explained the whole to_paramsituation yet, and I promised that I would get to it. Now’s the time to figure out the magic behind that!

Rails Non-Sluggish Solution: to_param

The Rails solution to generating params for an object’s url path comes from its elegant to_parammethod. By default, this method just calls to_son a Plain Old Ruby Object, and converts it to an instance of Ruby’s Stringclass. However, there are plenty of places where Rails itself overridesthis method (which explains why we also have to do it in the context of our own controller)!

In fact, the Rails documentation even explains when and how to go about redefining the implementation of this method:

“Notably, the Rails routing system calls to_paramon models to get a value for the :idplaceholder. ActiveRecord::Base#to_paramreturns the idof a model, but you can redefine that method in your models.”

class User def to_param "#{id}-#{name.parameterize}" endend

Of course, we could have easily redefined this in the context of each of our models, but using the acts_as_urllibrary reduces the amount of duplicated code that we need in each of our models, and is pretty sophisticated in that it allows us to use differentattributes across different models to generate our url path.

Interestingly, the source codefor Rails’ to_parammethod reveals some elegant checks as well. This method first checks whether the object as been persisted to the database, and then returns a string representing the object’s key. We can actually see how the to_keymethod is being called from insideof to_param, and how the default return value of an unpersisted object’s param will be nil. This is the magic that goes on under the hood when we were trying to find the slugattributes for all of those Bookinstances before we called initialize_urlson them!

So, even though slugs have a reputationof being slow, now we know how to speed through this problem with an elegant and quick solution! I can’t say the same for this poor guy, though:

tl;dr? The acts_as_urllibrary expects an urlattribute on a model, and uses that to generate the path for an object. You need to override Rails’ to_parammethod that, by default, will use the idof an object to generate its path. This awesome gist by Jeff Casimir is the best write-up on slugs and Rails’ url generation out there. Give it a read!The original to_param method used to be defined inside of ActiveRecord::Base, but has since moved to the ActiveModel::Conversion module, which handles default conversions, including to_model, to_key, and to_partial_path. Read more about how these methods work in the Conversion module documentation.