LaunchSchool - An Online School for Developers /

Blog

Ruby on Rails ActiveRecord Associations - the Lesser Known Parts

ActiveRecord associations are part of a Rails developer’s basic toolbox. But not all are aware of some of the extra freatures that are available when defining associations.

Customizing Queries

Perhaps you’ve developed a blogging site that allows comments. Some comments may be inappropriate (it is the Internet, after all) so you want to ensure comments are only shown if they’ve been approved by your site moderator. You’ve added a boolean field to your comments table, :approved.

Since for each blog post you only want to bring back a list of approved comments (only the ones the moderators have flagged as approved) you can customize the query

1
2
3
4
# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
  ...

You now have an association my_post.approved_comments that you can use wherever you need to retrieve a post’s vetted comments.

Extensions

It may be that you want to have options to display all comments on a post, or restrict the comments to just those created today. You can add an extension, which is to say a custom method for this particular association.

1
2
3
4
5
6
7
8
# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :comments do
    def today
      where("created_at >= ?", Time.zone.now.beginning_of_day)
    end
  end
  ...

Now, in addition to being able to call my_post.comments you can refine the records retrieved by calling my_post.comments.today

Callbacks

It’s possible to call methods automatically before and after records are added or removed.

Suppose you have a BuildingProject which has Workers… every time you add a new Worker to a BuildingProject you want to calculate the changes to the project budget and the changes to the completion date, perhaps based on the type of worker, experience of worker… a complicated method! And one you can call automatically after a worker is added.

1
2
3
4
5
6
7
8
# app/models/building_project.rb
class BuildingProject < ActiveRecord::Base
  has_many :workers, after_add: :recalculate_project_status
  ...
  def recalculate_project_status(newly_added_worker)
    ...
  end
  ...

Inverse Of

inverse_of is a handy way to flag to Ruby that associated objects can be accessed from either direction. my_post.comments and my_comment.post. Of course, by specifying has_many :comments in the Post class and belongs_to :post in the Comment class, you have already defined these two associations… but the inverse_of makes it explicit to rails that the objects in the relations are indeed the same objects.

You specify inverse_of as follows:

1
2
3
4
# app/models/post.rb
class Post < ActiveRecord.Base
 has_many :comments, inverse_of: post
 ...

Now WITHOUT specify8ng inverse_of, if you do this…

1
2
3
4
5
6
7
post = Post.first
post.update_attribute(:importance, false)
comment = post.comments.first
working_post = comment.post
working_post.update_attribute(:importance, true)
post.importance?
=> false

Huh! It still thinks post is unimportant… this is because comment.post did a separate database query and got a new and separate instance of the same Post record, and the original post record doesn’t know that the database record has been changed.

Now WITH inverse_of, you follow the exact same steps…

1
2
3
4
5
6
7
post = Post.first
post.update_attribute(:importance, false)
comment = post.comments.first
working_post = comment.post
working_post.update_attribute(:importance, true)
post.importance?
=> true

Much better! Because of the inverse_of the comment.post did NOT do a database query, instead it used the same instance of the post object that was already in memory. So changes to working_post are automatically shown in post at the same time, avoiding the unexpected results.