Rails has_many 由于 id = null 无法以嵌套形式保存

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

使用 Rails 4.1.13 和 Ruby 2.0.0(尽管我在 Ralis 4.0 和 Ruby 1.9.3 上遇到了同样的问题。我已经阅读了很多关于这个的文章特殊问题,无法理解为什么我的解决方案(看起来与 this 完全一样)不起作用,所以请帮助我。

我有两个模型 BlogPost 和 Tag。一篇博文可以有很多标签,一个标签可以有很多博文。我通过第三个模型 BlogPostRelation 连接它们。因此,这是我的基本设置:

# 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

我有一个 BlogPost 表单,使用 Formtastic,我在其中为 BlogPost 创建复选框使用:

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

我遇到的问题是在添加标签之前未保存 BlogPost,这导致 blog_post_id 不存在(实际上不存在)的验证失败:

  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

似乎解决方案应该是使用 inverse_of,坦率地说,我没有 100% 理解它。还应该提到的是,我不是 100% 确定如何使用 accepts_nested_attributes_for 来解决此类问题。我已经尝试了所有不同的设置,但据我所知,它们唯一应该出现在连接模型 BlogPostRelation 中,如下所示:

# 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

这也不管用,我现在完全不知道该怎么做。

  1. 你的模型结构没问题。
  2. 创建 post 后,有一种巧妙的方法可以将标签添加到 post。你只需要使用模型方法来做 that.You 不需要 inverse_of 。方法如下:

    在您看来,添加自定义属性 (all_tags)。

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

    您需要在控制器中允许该参数。 在您的 Post 模型中添加以下三种方法:

    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. 您使用的 nested_attributes_for 是正确的,但在这种情况下,您的模型只有一个名称和一个 belongs_to 列,因此使用它是一种矫枉过正。

  4. 虽然没有命名约定,但您可以将其称为标记。如果你和其他人能看懂就可以了。

您真正想要使用的是 has_and_belongs_to_many (HABTM) 关联:http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association

然而,这假设您不想对关系模型做任何事情(blog_post_tag_relations 在您的情况下)

您只需要以下模型和关联:

class BlogPost < ActiveRecord::Base
  has_and_belongs_to_many :tags
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :blog_posts
end

然后您必须将联接 table blog_post_tag_relations 重命名为 blog_posts_tags,这是两个模型的字母复数组合。 Rails 自动在后台无缝地 up/uses 看起来 table。它只会有关系的 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

那么您的表单就可以正常工作了:

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

这里的部分问题是您正在验证 ID。如果 id 未知,Rails 无法验证 blog_post_id 是否存在, 但是 它可以验证 blog_post 是否存在。

所以部分答案至少是验证关联实例的存在,而不是 id。

将验证更改为:

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

我也总是指定 inverse_of,但我不确定这是这个问题的一部分。

尝试

validates :blog_post, :presence => true
validates :blog_post_id, :presence => true, allow_nil: true #can ignore this