As promised last week, today is dedicated to delicious Rails! This post is the first of several in which I'll talk about how the various feature implementations in Sutro, the blog engine that runs this site. In particular, today will focus on what it means to add tags to articles, and how to associate the models underneath.

Models and Associations

Rails associations are a set of methods that enable us to tie conceptually related models together and describe their relationships. For example, a Car belongs to a Person, and a Customer has many Orders. The association keywords "belongs to" and "has many" are several of the built in associations that Rails provides. We'll see that by specifying the relationships between two models (posts and tags here), we gain access to high-level helper methods that make our lives infinitely easier.

Our first step is the most important - determing the right relationship to use between posts and tags. The complete list of Rails model associations, as well as an explanation of their purpose and usage can be found at the RailsGuide here. So which ones are right for our needs? Well, clearly a post can have many tags. But remember, the relationship must be defined both ways. In that case, a post has_many :tags and a tag belongs_to :post relationship seems like the obvious choice. After some thought though, you'll realize that's not right. A post can have many tags, but a tag can also belong to many posts. For example, if a post is tagged with "ruby", the "ruby" tag is not locked to that post. Many posts can be tagged with the same name. So the association we're looking for is has_and_belongs_to_many. A post has and belongs to many tags, and a tag has and belongs to many posts. This is a mouthful, and gets a bit complicated to put in place, but ultimately, it allows us to call the methods @post.tags and @tag.posts, which will be enormously useful. Let's move on to generating our models and migrations!

Fortunately, our tag model is extremely simple. All we care about is the tag name. Let's generate a model to represent our tags - run rails generate model tags to create the tag model, as well a migration to initialize the corresponding table.

# db/migrate/(timestamp)_create_tags.rb
def change
  create_table :tags do |t|
    t.string "name"
    t.timestamps
  end
end

That's all we have to do in our migration. Make sure to run a rake db:migrate afterwards. Now let's write the actual associations between our two models!

# app/models/tag.rb
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

# app/models/post.rb
class Post < ActiveRecord::Base
  has_and_belongs_to_many :tags
end

We're not quite done yet. If you were paying attention, you'll notice we haven't placed any foreign keys anywhere - nothing in either table references the other. For example, let's look at a customer/order relationship. A customer has many orders, and an order belongs to a single customer. The traditional approach would be to place a customer_id field in the Order table, so that an order now references a Customer by foreign key lookup. But a many-to-many relationship is more complicated. Since we defined that a post has and belongs to many tags, and that a tag has and belongs to many posts, we need an intermediate table to keep track of the associations called a join table. This is a table with two columns only - post_id and tag_id. The join table holds no data about either model - purely association data for lookups.

Creating the join table

While join tables may be a tricky concept to master in your head, the implementation is fortunately fairly simple and Rails handles most of the heavy lifting for us. For example, it's Rails convention to define the table name in terms of the two model names in alphabetical order. So, our table joining Posts and Tags will be called posts_tags. To implement it, all we have to do is generate the appropriate migration using rails generate migration CreatePostTagJoinTable.

# db/migrate/(timestamp)_create_post_tag_join_table.rb
def change
  create_table :posts_tags, :id => false do |t|
    t.integer :post_id
    t.integer :tag_id
  end
end

Note that we're specifying :id => false here. Rails automatically adds an id primary key to tables, but since this a pure join table we don't need it. Be sure to run a rake db:migrate again!

That's all we need to worry about in terms of migrations. Now we have to create an interface for our tags, and the ability to actually add a tag to a post.

has_and_belongs_to_many or has_many :through?

Before I continue, I want to explain a design decision here. If you have experience with Rails, you'll know that we had the option of doing has_many :through instead of has_and_belongs_to_many. For those unfamiliar with the association, has_many :through creates an intermediary model for the relationship. For example, consider a system that tracks appointments for physicians and patients. A physician has many patients, and a patient (potentially) has many physicians, but we need information about the relationship itself as well, such as the appointment time. In this case, we'd want to create the intermediary model Appointments and change our associations so that a physician has_many :patients, :through => :appointments and a patient has_many :physicians, :through => :appointments. Again, Appointments acts as a join table tracking the relationship, but now we have access to other information about the relationship itself. Other tutorials I've seen have implemented the post-tag association using has_many :through and created an intermediary model called Taggings. This doesn't appeal to me - a direct relationship seems to make more sense, so I've chosen to implement it using has_and_belongs_to_many. For more information, I highly recommend the RailsGuide on Associations or the ActiveRecord Associations ClassMethods Documentation.

Adding tags to posts

Before writing out any code, it's critical to sit back and imagine how you want the end result to work. I think of it in terms of a story - here, "when writing a post, I want to input the tags as a comma-separated list and have Rails do the rest of the work". This is possible, but takes a little bit of extra work on our part since we're no longer doing direct input as in a post title, for example. Let's write the corresponding code in our view first.

# app/views/admin/posts/new.html.erb
<%= form_for(:post, :url=>{:action => 'create'}) do |f| %>  
  <%= f.label :title %>
  <%= f.text_field :title %>

  <%= f.label :body, "Post" %>
  <%= f.text_area :body %>

  <%= f.label :tag_list, "Tags (optional)" %>
  <%= f.text_field :tag_list %>   

  <%= f.submit "Publish" %>           
  <%= link_to "Cancel", dashboard_path %>
<% end %>

At this point, you're like to run into an error if you go to the page - after all, our Post model has no attribute tag_list. We have to define methods to accept this tag list and transform it into a list of Tags associated with this post. Let's go into post.rb and define the appropriate methods.

# app/models/post.rb
def tag_list
  self.tags.map {|t| t.name }.join(", ")
end

With that defined, our new view displays fine, and our edit view displays the tags in a nicely comma separated list. But this doesn't actually associate any tags with a post yet. To do that, we have to define the special tag_list= method back in post.rb.

# app/models/post.rb
def tag_list=(tag_list)
  self.tags.clear # For the update method, just in case we're changing tags
  
  # Split the tags into an array, strip whitespace , and convert to lowercase
  tags = tag_list.split(",").collect{|s| s.strip.downcase}
 
  # For each tag, find or create by name, and associate with the post
  tags.each do |tag_name|
    tag = Tag.find_or_create_by_name(tag_name)
    tag.name = tag_name      
    self.tags << tag # Append the tag to the post
  end
end

We're making progress! We can now create posts with associated tags in a successful many to many relationship. Of course, we can't stop here. Tags aren't useful unless you have the ability to filter posts by their tags.

Filtering posts by tag

While it might seem like a good idea to generate a separate controller to handle our tags, I'm going to forgo that step. For now, it's somewhat of a lazy design decision, but we don't need that level of functionality and instead can just build our filtering into our post controller's list action. Let's do routes first.

# config/routes.rb
match "blog/tag/:tag"  => "posts#list", :as => "tag"

That gives us a simple named path that matches any tag string, such as /blog/tag/ruby and dispatches it to the list action on the posts controller. In the post controller itself, all we have to do is add a little bit of logic to determine whether or not we're filtering by a tag or just looking at the main blog index.

# app/controllers/posts_controller.rb
def list
    
  if !params[:tag].nil? # Are we looking at a tag?
    @tag = Tag.find_by_name(params[:tag])
    if @tag.nil?
      # Redirect if user is trying to look at a non-existent tag
      redirect_to blog_path
    else           
      @posts = @tag.posts.order("id DESC")
    end
  else
    # No tag - display posts as normal      
    @posts = Post.all.order("id DESC")
  end
                     
end

The actual Sutro implementation is slightly different (I check for status and do some pagination), but I've left those off for brevity. The full controller source is available on GitHub here.

Since we're lumping this in with the existing action, let's add a notice in the view so there's some indication that you're viewing by tag. My solution, although simple, was this:

# app/views/posts/list.html.erb
<% unless @tag.nil? %>
  <h1>Tagged: <%= @tag.name %></h1>
<% end %>

This creates an h1 at the top of the page displaying the current tag (if it exists)

Finishing touches

At this point, we've done all the real work for our tag implementation and now we're just working on presentational stuff. To close, this is how the tag 'pills' are displayed at the bottom of each post. A method called render_tags is defined in a helper that takes an array of Tag objects (as outputted by @post.tags or similar), and outputs a raw HTML string containing formatted links to each tag's respective path.

# app/helpers/post_helper.rb
def render_tags(tag_list)
  raw tags.map { |t| link_to(t.name, tag_path(t.name), :class=> "tag") }
    .join("")
end

Note that the Sutro implementation differs in that I use the same method for the public 'pill' display, and the backend dashboard which is a comma-separated listing. The full code is available here.

And we're done! We've succesfully created a many to many association between two models, the ability to enter tags with a post, and format them in a nice display. Rails provides powerful helpers to work with complex associations, and this is only a peek at what's possible. If you enjoyed what you read, or want to chide me on my lazy coding habits, make sure to inform me in the comments. Until next time!

Tags: railsactiverecordassociations