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.
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.
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
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.
You can call it a tagging, although there's no convention for naming. If you and other can understand it, it is fine.
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) %>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With