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:
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
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.