Associations

Defining Associations

Let's see if our associations are still there?

post = Post.find 1
#   Post Load (0.4ms)  SELECT  "posts".* FROM "posts"  ORDER BY "posts"."id" ASC LIMIT 1
# => #<Post:0x00000003e1b470
#  id: 1,
#  title: "Blog 1",
#  body: "Lorem ipsum dolor sit amet.",
#  author: "Brad",
#  created_at: Sun, 07 Dec 2014 00:00:00 UTC +00:00>

post.comments
# NoMethodError: undefined method `comments' for #<Post:0x00000003e1b470>

In Rails, defining associations is quite simple:

### app/models/post.rb ###

class Post < ActiveRecord::Base
  ....

  has_many :comments
end
### app/models/comment.rb ###

class Comment < ActiveRecord::Base
  ....

  belongs_to :post
end

These has_many and belong_to calls work just as expected, and return us to the functionality we had before:

### post comments ###

post = Post.find 1
#   Post Load (0.1ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1  [["id", 1]]
# => #<Post:0x000000085a9e68
#  id: 1,
#  title: "Blog 1",
#  body: "Lorem ipsum dolor sit amet.",
#  author: "Brad",
#  created_at: Sun, 07 Dec 2014 00:00:00 UTC +00:00>

post.comments
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
# => [#<Comment:0x00000004865b88
#   id: 1,
#   body: "Lorem ipsum dolor sit amet.",
#   author: "Matz",
#   post_id: 1,
#   created_at: Tue, 09 Dec 2014 00:00:00 UTC +00:00>,
#  #<Comment:0x00000004865318
#   id: 2,
#   body: "Lorem ipsum dolor sit amet.",
#   author: "DHH",
#   post_id: 1,
#   created_at: Tue, 09 Dec 2014 00:00:00 UTC +00:00>,
#  #<Comment:0x00000004864b20
#   id: 3,
#   body: "Lorem ipsum dolor sit amet.",
#   author: "tenderlove",
#   post_id: 1,
#   created_at: Tue, 09 Dec 2014 00:00:00 UTC +00:00>]

### post for a comment ###

comment = Comment.find 1
#  Comment Load (0.2ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT 1  [["id", 1]]
#=> #<Comment:0x0000000513fd30
# id: 1,
# body: "Lorem ipsum dolor sit amet.",
# author: "Matz",
# post_id: 1,
# created_at: Tue, 09 Dec 2014 00:00:00 UTC +00:00>

comment.post
#  Post Load (0.1ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1  [["id", 1]]
#=> #<Post:0x000000052ab7c8
# id: 1,
# title: "Blog 1",
# body: "Lorem ipsum dolor sit amet.",
# author: "Brad",
# created_at: Sun, 07 Dec 2014 00:00:00 UTC +00:00>

So again, ActiveRecord will handle our associations for us, but we have to define how our models are related.

How did this happen?

  • When you have a has_many :comments line in your model, Rails will define a method comments for you, very similar to the comments method we had before, to retrieve all the comments related to the post.

  • Similarly when you have a belongs_to :post line in your Comment model, Rails will define the method post to fetch you the post related to that comment.

More about Active Record Associations:

  • Associations may be separated into three main types.: One-to-One(1:1), One-To-Many(1:M), and Many-to-Many(M:M). In this book, we have mainly dealt with a One-To-Many association. In a blog that has posts and comments, a post may have many comments, but a comment cannot have many posts, so that is a one(post) to many(comments) association.
    If we could write the same comment on multiple posts at once, then we would have a many to many association. And if we could only post at most a single comment on a single post, then we would be dealing with a one to one association.
  • After you define the associations, Rails defines for you many convenient methods that you can use. See here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Here are some more examples with associations:

post = Post.first
#   Post Load (0.3ms)  SELECT  "posts".* FROM "posts"  ORDER BY "posts"."id" ASC LIMIT 1
# => #<Post:0x00000004cbefe0 ...>

post.comments.count
#    (0.5ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
# => 3

post.comments.exists?
#   Comment Exists (0.4ms)  SELECT  1 AS one FROM "comments" WHERE "comments"."post_id" = ? LIMIT 1  [["post_id", 1]]
# => true

Post.last.comments.exists?
#   Post Load (0.4ms)  SELECT  "posts".* FROM "posts"  ORDER BY "posts"."id" DESC LIMIT 1
#   Comment Exists (0.2ms)  SELECT  1 AS one FROM "comments" WHERE "comments"."post_id" = ? LIMIT 1  [["post_id", 5]]
# => false

# and for posts, we can instantiate new comments like so:
comment = post.comments.build
# => #<Comment:0x0000000483f730
#  id: nil,
#  body: nil,
#  author: nil,
#  post_id: 1,
#  created_at: nil,
#  updated_at: nil>

# notice that the `post_id` was properly set
comment.post_id
# => 1

# and we can reference back from the post
comment.post
# => #<Post:0x00000004cbefe0
#  id: 1,
#  title: "Blog 1",
#  body: "Lorem ipsum dolor sit amet.",
#  author: "Kevin",
#  created_at: Sun, 07 Dec 2014 00:00:00 UTC +00:00,
#  updated_at: Sun, 07 Dec 2014 00:00:00 UTC +00:00>

# and just as there is `.build`, there is also
# `.create` and `.create!` methods for associations
post.comments.create!({})
#    (0.1ms)  begin transaction
#    (0.0ms)  rollback transaction
# ActiveRecord::RecordInvalid: Validation failed: Body can't be blank, Author can't be blank

Associations in Controllers

Now, let's use Active Record association methods in the two controllers we've been working with, PostsController and CommentsController. We'll want to use these methods going forward instead of the custom ones that we have made so far.

Listed below are the actions we need to change so that our code works with Active Record.

# app/controllers/posts_controller OLD

def update
  @post.set_attributes(params[:post])
  if @post.save
    redirect_to posts_path
  else
    render 'edit'
  end
end

# app/controllers/posts_controller NEW

def update
  if @post.update_attributes(params[:post])
    redirect_to posts_path
  else
    render 'edit'
  end
end

There aren't too many changes needed in the PostsController. set_attributes becomes the Active Record method, update_attributes. This method works in the same manner, taking a hash of attributes we wish to change, and updating the current object accordingly. The difference is that a call to save isn't needed. update_attributes sets the attributes and saves the changes to the database all in one command.

# app/controllers/comments_controller OLD

# ...

def create
  @comment = @post.build_comment(params[:comment])
  if @comment.save
    # redirect for success
    flash[:success] = "You have successfully created the comment."
    redirect_to post_path(@post.id)
  else
    # render form again with errors for failure
    flash.now[:error] = "Comment couldn't be created. Please check the errors."
    render 'posts/show'
  end
end

def destroy
  @post.delete_comment(params[:id])

  redirect_to post_path(@post.id)
end

# ...
# app/controllers/comments_controller NEW

# ...

def create
  @comment = @post.comments.build(params[:comment])
  if @comment.save
    # redirect for success
    flash[:success] = "You have successfully created the comment."
    redirect_to post_path(@post)
  else
    # render form again with errors for failure
    flash.now[:error] = "Comment couldn't be created. Please check the errors."
    render 'posts/show'
  end
end

def destroy
  @post.comments.delete(params[:id])

  redirect_to post_path(@post)
end

# ...

There's a bit more work to be done on the CommentsController. For the create action we have to change @post.build_comment to @post.comments.build. They both take the same argument, but build_comment isn't a method that exists in Rails. What we use above is the Active Record associations method, build, which allows us to create a new Active Record object of the same type as the collection it is -- Comment in this case.

We perform a similar change in the destroy action. The Active Record method, delete takes the same arguments as our delete_comment method. The difference between the two is that we have to call delete on a collection of comments, and then supply the id(s) for the comment(s) we wish to delete from the database.

One final thing to notice is that we're now only passing in the model object @post, we're not passing in its id to our path helpers. Rails allows us to write this shorter code. If we only pass in the Active Record model, then Rails will inspect it, determine its id and add that to the URL we wish to navigate to. This allows our path helper to accurately navigate to the correct post page after we create and destroy a comment.