Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you validate the presence of a belongs to association with Rails?

Say I have a basic Rails app with a basic one-to-many relationship where each comment belongs to an article:

$ rails blog
$ cd blog
$ script/generate model article name:string
$ script/generate model comment article:belongs_to body:text

Now I add in the code to create the associations, but I also want to be sure that when I create a comment, it always has an article:

class Article < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :article
  validates_presence_of :article_id
end

So now let's say I'd like to create an article with a comment all at once:

$ rake db:migrate
$ script/console

If you do this:

>> article = Article.new
=> #<Article id: nil, name: nil, created_at: nil, updated_at: nil>
>> article.comments.build
=> #<Comment id: nil, article_id: nil, body: nil, created_at: nil, updated_at: nil>
>> article.save!

You'll get this error:

ActiveRecord::RecordInvalid: Validation failed: Comments is invalid

Which makes sense, because the comment has no page_id yet.

>> article.comments.first.errors.on(:article_id)
=> "can't be blank"

So if I remove the validates_presence_of :article_id from comment.rb, then I could do the save, but that would also allow you to create comments without an article id. What's the typical way of handling this?

UPDATE: Based on Nicholas' suggestion, here's a implementation of save_with_comments that works but is ugly:

def save_with_comments
  save_with_comments!
rescue
  false
end

def save_with_comments!
  transaction do
    comments = self.comments.dup
    self.comments = []
    save!
    comments.each do |c|
      c.article = self
      c.save!
    end
  end
  true
end

Not sure I want add something like this for every one-to-many association. Andy is probably correct in that is just best to avoid trying to do a cascading save and use the nested attributes solution. I'll leave this open for a while to see if anyone has any other suggestions.

like image 470
pjb3 Avatar asked Jun 22 '09 02:06

pjb3


1 Answers

I've also been investigating this topic and here is my summary:

The root cause why this doesn't work OOTB (at least when using validates_presence_of :article and not validates_presence_of :article_id) is the fact that rails doesn't use an identity map internally and therefore will not by itself know that article.comments[x].article == article

I have found three workarounds to make it work with a little effort:

  1. Save the article before creating the comments (rails will automatically pass the article id that was generated during the save to each newly created comments; see Nicholas Hubbard's response)
  2. Explicitly set the article on the comment after creating it (see W. Andrew Loe III's response)
  3. Use inverse_of:
    class Article < ActiveRecord::Base
      has_many :comments, :inverse_of => :article
    end

This last solution was bot yet mentioned in this article but seems to be rails' quick fix solution for the lack of an identity map. It also looks the least intrusive one of the three to me.

like image 121
Niek Bartholomeus Avatar answered Sep 24 '22 05:09

Niek Bartholomeus