Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails has_many through cannot save in nested form due to id = null

Using Rails 4.1.13 and Ruby 2.0.0 (although I had the same problem with Ralis 4.0 and Ruby 1.9.3. I have read numerous articles about this particular issue and cannot understand why my solution (which seems exactly like this) does not work, so please help me out.

I have two models BlogPost and Tag. A BlogPost can have many Tags and one Tag can have many BlogPosts. I connect them through a third model BlogPostRelation. Thus, this is my basic setup:

# blog_post.rb
has_many :blog_post_tag_relations, dependent: :destroy
has_many :tags, :through => :blog_post_tag_relations
accepts_nested_attributes_for :blog_post_tag_relations, :tags

# tag.rb
has_many :blog_post_tag_relations, dependent: :destroy
has_many :blog_posts, :through => :blog_post_tag_relations

# blog_post_tag_relation.rb
belongs_to :tag
belongs_to :blog_post  
validates_uniqueness_of :tag_id, :scope => [:blog_post_id]
validates :blog_post_id, :presence => true
validates :tag_id, :presence => true    
accepts_nested_attributes_for :tag, :blog_post

I have a form for BlogPost, using Formtastic, where I create checkboxes for the BlogPost using:

<%= f.input :blog_title %>
<%= f.input :tags, as: :check_boxes, :collection => tags.order(:name) %>

The problem I have is that BlogPost is not saved before the Tags are added which causes an validation failure of blog_post_id not being present (which it isn't):

  Tag Load (1.6ms)  SELECT "tags".* FROM "tags"  WHERE "tags"."id" IN (678, 56)
   (0.9ms)  BEGIN
  BlogPost Exists (1.6ms)  SELECT  1 AS one FROM "blog_posts"  WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
  BlogPostTagRelation Exists (1.2ms)  SELECT  1 AS one FROM "blog_post_tag_relations"  WHERE ("blog_post_tag_relations"."tag_id" = 678 AND "blog_post_tag_relations"."blog_post_id" IS NULL) LIMIT 1
  CACHE (0.0ms)  SELECT  1 AS one FROM "blog_posts"  WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
  BlogPostTagRelation Exists (1.1ms)  SELECT  1 AS one FROM "blog_post_tag_relations"  WHERE ("blog_post_tag_relations"."tag_id" = 56 AND "blog_post_tag_relations"."blog_post_id" IS NULL) LIMIT 1
  CACHE (0.0ms)  SELECT  1 AS one FROM "blog_posts"  WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
   (0.8ms)  ROLLBACK

It seems like the solution should be to use inverse_of, which I frankly don't understand to 100%. It should also be mentioned that I am not 100% sure on how to use accepts_nested_attributes_for either for this type of issue. I have tried all different setups but as far as I understand the only place they should be is in the join model, BlogPostRelation, like this:

# blog_post_tag_relation.rb
belongs_to :tag, :inverse_of => :blog_post_tag_relations
belongs_to :blog_post, :inverse_of => :blog_post_tag_relations

validates_uniqueness_of :tag_id, :scope => [:blog_post_id]
validates :blog_post_id, :presence => true
validates :tag_id, :presence => true    

accepts_nested_attributes_for :tag, :blog_post

This doesn't work either and I am completely lost now in what to do.

  • Most important: What should I do?
  • Is inverse_of the solution to this problem? If so, how should I use it?
  • Am I using accepts_nested_attributes_for correctly?
  • Does it have to do with the naming of BlogPostTagRelation (should it have been called BlogPostTag instead?
like image 283
Christoffer Avatar asked Sep 07 '15 22:09

Christoffer


3 Answers

Part of the problem here is that you are validating on ids. Rails cannot validate that blog_post_id is present if the id is not known, but it can validate that blog_post is present.

So part of the answer at least is to validate the presence of the associated instance, not the id.

Change the validations to:

validates :blog_post, :presence => true
validates :tag      , :presence => true    

I would always specify inverse_of as well, but I'm not sure it is part of this problem.

like image 93
David Aldridge Avatar answered Nov 10 '22 15:11

David Aldridge


  1. Your model structure is okay.
  2. There's one nifty way you can add tags to your posts after the post is created. You just need to use a model method for doing that.You do not need inverse_of . Here is how:

    In your view, add a custom attribute (all_tags).

    <%= f.text_field :all_tags, placeholder: "Tags separated with comma" %>
    

    You need to permit the parameter in the controller. In your Post model add these three methods:

    def all_tags=(names)
      self.tags = names.split(",").map do |name|
      Tag.where(name: name.strip).first_or_create!
     end
    end
       #used when the post is being created. Splits the tags and creates   entries of them if they do not exist. `self.tags` ensures that they tags will be related to the post.
    
    def all_tags
     self.tags.map(&:name).join(", ")
    end
    
     #Returns all the post tags separated by comma
    
    def self.tagged_with(name)
      Tag.find_by_name!(name).posts
    end
     #Returns all the posts who also contain the tag from the current post.
    

    Here's a full implementation

  3. You are using nested_attributes_for correctly, but in this case, you are having models who just have a name and a belongs_to column, so using this is an overkill.

  4. You can call it a tagging, although there's no convention for naming. If you and other can understand it, it is fine.

like image 2
Hristo Georgiev Avatar answered Nov 10 '22 15:11

Hristo Georgiev


What you really want to use is a has_and_belongs_to_many (HABTM) association: http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association

This however assumes you do not want to do anything with the Relationship Model (blog_post_tag_relations in your case)

You would need only the following models and associations:

class BlogPost < ActiveRecord::Base
  has_and_belongs_to_many :tags
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :blog_posts
end

You would then have to rename your join table blog_post_tag_relations to blog_posts_tags, the alphabetical plural combination of the two models. Rails automagically looks up/uses that table seamlessly in the background. It will only have the relationship's foreign_keys:

create_table :blog_posts_tags, id: false do |t|
  t.belongs_to :blog_post, index: true
  t.belongs_to :tag, index: true
end

Then your form just works:

<%= f.input :blog_title %>
<%= f.input :tags, as: :check_boxes, :collection => tags.order(:name) %>
like image 2
omarvelous Avatar answered Nov 10 '22 15:11

omarvelous