An Introduction to Association Proxy

Posted by james
on Sunday, February 10
@post.comments << Comment.new(:title => 'something')
Comment.find(:all,  :conditions => ['post_id = ? AND title like ?', @post.id, 'something'])
Comment.count(:all, :conditions => ['post_id = ?', @post.id])

There is an easier way to do all of that. So, if you're still manipulating ActiveRecord associations by hand, stop; there is a better way.

AssociationProxy

When you request a collection of associated models, ActiveRecord extends that array with several helpful methods. You can treat that array like a model class, except all of the methods are automatically scoped to the association's parent. It responds to methods like find, create, new, count, etc, and, it's called AssociationProxy.

So, what if we wanted to create a comment, scoped to our post model?

@post.comments.create(params[:comment])

Making use of AssociationProxy, the code reads better, and requires fewer keystrokes. We can also use AssociationProxy to find elements in our collection.

@post.comments.find(:all, :conditions => {:title => 'title we are looking for'})

We can even use dynamic, attribute-based finders through AssociationProxy.

@post.comments.find_by_title 'title we are looking for'

It Gets Better

Since assoc proxy responds to many of the same methods as a model class, the two can be interchanged for many operations. In resource_controller, for example, nested controllers depend heavily on association proxies. Let me show you what I mean.

If we had, for example, a typical show action.

def show
  @comment = Comment.find(params[:id])
end

With very little effort, we can allow comments to be shown nested, or unnested.

def show
  @comment = model.find(params[:id])
end

private
  def model
    params[:post_id] ? Post.find(params[:post_id]).comments : Comment
  end

Since we know that AssociationProxy responds to other model class methods, we can base our create method on this technique, too.

def show
  @comment = model.find(params[:id])
end

def create
  @comment = model.new(params[:comment])
  if @comment.save
    redirect_to @comment
  else
    render :action => 'new'
  end
end

private
  def model
    params[:post_id] ? Post.find(params[:post_id]).comments : Comment
  end

In fact, this is nearly exactly how resource_controller is built.

So, association proxy is useful in simple and complex situations alike. It can save you a lot of code, and increase readability. It's a win-win really.

An Introduction to Ruby's Enumerable Module 4

Posted by james
on Sunday, January 06

If you're still writing for loops, stop; there's a better way. Exactly why merits some examples.

[1, 2, 3].each { |number| puts number }

That's a pretty straightforward one. It says: with each element in this array, execute the given block. But, what if we wanted to do something more complicated, like determine whether there were any even numbers in the array?

[1, 2, 3].any? { |number| number % 2 == 0 } # => true
[1, 3, 5].any? { |number| number % 2 == 0 } # => false

The any? method says: are there any elements in the array for which this block evaluates to true? We might also want to know whether all? of the elements were even.

[2, 4, 6].all? { |number| number % 2 == 0 } # => true
[1, 2, 3].all? { |number| number % 2 == 0 } # => false

We can see that making good use of Enumerable methods produces highly readable code. Before we move on, there are a couple more methods that are worth demonstrating.

We've already seen how we can easily look inside of an array, using any?, and all?. Conveniently, Enumerable provides similar methods for extracting objects. What if we wanted, for example, to retrieve all of the even numbers.

[1, 2, 3, 4].select { |number| number % 2 == 0 } # => [2, 4]

...or, the first even number...

[1, 2, 3, 4].detect { |number| number % 2 == 0 } # => 2

Let's pick up the pace.

Putting the Pieces Together

If I was writing some blogging software, I might want to display recent activity in the administration area. With a big array of notification objects, I can use select to easily divide them up for display, based on recency.

today     = @notifications.select { |notification| notification.created_at > 1.day.ago.to_date }
yesterday = @notifications.select { |notification| notification.created_at < 1.day.ago.to_date && notification.created_at > 2.days.ago.to_date }
earlier   = @notifications.select { |notification| notification.created_at < 2.days.ago.to_date }

I'm actually doing something very similar to this in an app I'm working on. Since I love fat models, I created some helper methods on AR::Base. They look (something) like this:

class ActiveRecord::Base
  def today?
    created_at > 1.day.ago.to_date
  end
  
  def yesterday?
    created_at < 1.day.ago.to_date && created_at > 2.days.ago.to_date
  end
  
  def earlier?
    created_at < 2.days.ago.to_date
  end
end

So, now, we can greatly simplify our use of the select method, making it far clearer what we're up to.

today     = @notifications.select(&:today?)
yesterday = @notifications.select(&:yesterday?)
earlier   = @notifications.select(&:earlier?)

...wait, what? The ampersand operator, as described by the PickAxe book:

If the last argument to a method is preceded by an ampersand, Ruby assumes that it is a Proc object. It removes it from the parameter list, converts the Proc object into a block, and associates it with the method

But, a symbol isn't a Proc object. Luckily, ruby calls to_proc on the object to the right of the ampersand, in order to try and convert it to one. Symbol#to_proc is defined in ActiveSupport; it creates a block that calls the method named by the symbol on the object passed to it. So, for the above examples, the blocks send today?, yesterday?, or earlier?, respectively, to the each object that is yielded. Written out by hand, the expanded blocks would look like this:

today     = @notifications.select { |notification| notification.today?  }
yesterday = @notifications.select { |notification| notification.yesterday? }
earlier   = @notifications.select { |notification| notification.earlier? }

For additional to_proc coolness, it's worth mentioning Reg Braithwaite's String#to_proc (or get the code). I won't go on any further about that today, though.

Let's look at one last example — this time from real software. In order to add support for polymorphic, deep nesting in resource_controller, I had to write an algorithm that would determine which of the possible sets of parent resources were present at the invocation of a controller action. This is done by testing to see which of the params (parent_type_id) are present. So, if I say belongs_to [:project, :message], [:project, :ticket] in my comments controller, I need to determine: for which of those two possible sets are both necessary params present? Since belongs_to is just a simple accessor, the algorithm is easy to write.

belongs_to.detect { |set| set.all? { |parent_type| params["#{parent_type}_id".to_sym].nil? } }

Recap

Enumerable's biggest advantage over the for loop, as we've seen, is clarity. Using Enumerable's looping power tools, like any?, all?, and friends makes your code read semantically. Especially combined with Symbol#to_proc, code "...seems to describe what I want done instead of how I want the computer to do it.", to quote Raganwald.

Finally, in order to take your Enumerable usage to the next level, there are a few more methods you should have in your toolbox.

I'd also recommend keeping the Enumerable, and Array documentation bookmarked, so that you can quickly refer to them. As a bonus, most of these methods exist in prototype.js, too; just take a look at the documentation.

Promiscuity for Fun and Profit 1

Posted by james
on Wednesday, August 29
Sometimes, we advanced rails users take our knowledge of the framework for granted, have unfair expectations for less advanced rails coders. The worst examples of such unreasonable demands can be found on a daily basis in the IRC channel (#rubyonrails on Freenode, for the uninitiated). And, while the IRC channel can certainly be harsh, even those of us who don't throw around the term "noob" like it's going out of style sometimes (often) complain more than we help. As one of my peers likes to remind me, I'm present in that category more often than I'd like to admit (if I didn't have to). As an aside: While Gary is often the one I'm ranting to, his code is never the subject of the rant. I'd really like to turn my negative energy in to something positive. This will be the first of many articles that seek to add to the pool of resources on rails best practices, shortcuts, tips, and tricks. I hope you like it.

Today's Topic: Polymorphic Associations

It was a check-in today that sparked the round of complaining that sparked the round of Gary telling me to shut up that sparked the desire to write this article. The check-in added a bunch of tables to a project, with a schema that looked (something) like this:
create_table :cities do |t|
  t.column :name, :string
  ...
end

create_table :city_aliases do |t|
  t.column :city_id, :integer
  t.column :alias, :string
end

create_table :provinces do |t|
  t.column :name, :string
  ...
end

create_table :province_aliases do |t|
  t.column :province_id, :integer
  t.column :alias, :string
end
Well, it's definitely not the worst schema ever, but it can be improved drastically with a polymorphic association. What the heck is that, you ask?

Promiscuous Models

Essentially, a polymorphic association is a model that can belong_to any other table. Polymorphic associations are the way that a lot of popular plugins work. Ever wonder how acts_as_taggable just magically adds tagging to any model, without any migration? Well, it's super easy. Just replace your duplicate tables with one of these babies:
create_table :name_aliases do |t|
  t.column :aliasable_type, :string
  t.column :aliasable_id, :integer
  t.column :name, :string
end
You'll need a model, too:
class NameAlias < ActiveRecord::Base
  belongs_to :aliasable, :polymorphic => true
end
Note: I chose the name NameAlias because alias is a reserved word in ruby, and it may cause you some problems if you try to name a model that.

Going back to our example, we can redefine our models as follows:
class City < ActiveRecord::Base
  has_many :name_aliases, :as => :aliasable
end

class Province < ActiveRecord::Base
  has_many :name_aliases, :as => :aliasable
end

Now What?

A polymorphic association works just like any normal association. That means you can use all of your favorite AssociationProxy methods, and everything.
City.find_by_name("montreal").name_aliases.create :name => "mtl"
...will work just as it always has.

How'd you do that?

When you tell rails that :polymorphic => true, in your belongs_to options, it knows that it needs to store the id and the type of the associated class. You give the association a generic name like aliasable or taggable, to let rails know what the fields are named in the database. For example, if you call your association aliasable, you need to have the fields aliasable_type and aliasable_id. Make sure aliasable_type is a long enough string column to store the name of your longest model in it. In production, both of these fields should probably be indexed. When you associate a class with your polymorphic model, you have to tell it to associate as your generic association name, instead of its own, to let rails know how to join the tables up properly: :as => :aliasable.

Name Aliases that Rock

Because this example is a particularly useful one, it's worth showing you a method that I write often. It will work with any association, but I often use it with aliases. It adds an extra finder, which allows you to find_by_name_alias.
class Person
  has_many :name_aliases, :as => :aliasable
    
  def self.find_by_name_alias(name)
      Person.find(:all, :include => :name_aliases, :conditions => ["name_aliases.name = ?", name])
  end
end

Feedback, please!

So, there you have it: polymorphic associations. This is where you come in. I have some questions:
  • How'd you like it?
  • What could be improved?
  • Should I use a different format (like a screencast)?
  • Are there any topics you'd like to see?
Thanks for reading.