views:

753

answers:

4

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.

+1  A: 

You are correct. The article needs an id before this validation will work. One way around this is the save the article, like so:

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

If you are creating a new article with a comment in one method or action then I would recommend creating the article and saving it, then creating the comment, but wrapping the entire thing inside of a Article.transaction block so that you don't end up with any extra articles.

Nicholas Hubbard
Since I can only comment on my own answer. Daniel is correct you could use validates presence of article instead of article_id, but if you are using the article.comments.build this will still not work due to the builder that rails uses.
Nicholas Hubbard
A: 

Instead of validating the presence of the article's id you could validate the presence of the article.

validates_presence_of :article

Then when you are creating your comment:

article.comments.build :article => article
Daniel X Moore
This does not work, results in the same error
pjb3
A: 

If you're using Rails 2.3 you are using the new nested model stuff. I have noticed the same failures with validates_presence_of as you pointed out, or if :null => false was specified in the migration for that field.

If your intent is to create nested models, you should add accepts_nested_attributes_for :comments to article.rb. This would allow you to:

a = Article.new
a.comments_attributes = [{:body => "test"}]
a.save!  # creates a new Article and a new Comment with a body of "test"

What you have is the way it should work to me, but I'm seeing that it doesn't with Rails 2.3.2. I debugged a before_create in comment using your code and no article_id is supplied through the build (it is nil) so this is not going to work. You'd need to save the Article first as Nicholas pointed out, or remove validation.

Andy Atkinson
Yeah, I think the problem is that there is no article id yet, because the article has been saved, because this runs during validate. I believe the order of operations is:1. valid the article2. validate all the comments3. save the article 4. save the commentsSo step 2 fails, because there is no id yet.
pjb3
A: 

Hi,

I fixed this problem adding this follow line to my _comment.html.erb:

<%= form.hidden_field :article_id, :value => "NEW" if form.object.new_record? %>

Now, the validation works in stand alone form, and in multi form too.

daviscabral