如何避免 has_many :through 关系中的重复?

Posted

技术标签:

【中文标题】如何避免 has_many :through 关系中的重复?【英文标题】:how to avoid duplicates in a has_many :through relationship? 【发布时间】:2010-09-23 20:49:07 【问题描述】:

我怎样才能实现以下目标?我有两个模型(博客和读者)和一个 JOIN 表,它允许我在它们之间建立 N:M 关系:

class Blog < ActiveRecord::Base
  has_many :blogs_readers, :dependent => :destroy
  has_many :readers, :through => :blogs_readers
end

class Reader < ActiveRecord::Base
  has_many :blogs_readers, :dependent => :destroy
  has_many :blogs, :through => :blogs_readers
end

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader
end

我现在想做的是将读者添加到不同的博客。但是,条件是我只能将读者添加到博客一次。所以BlogsReaders 表中不能有任何重复项(相同的readerID,相同的blogID)。我怎样才能做到这一点?

第二个问题是,我如何获得读者尚未订阅的博客列表(例如,填写下拉选择列表,然后可以使用该列表将读者添加到另一个博客)?

【问题讨论】:

【参考方案1】:

Rails 中内置的更简单的解决方案:

 class Blog < ActiveRecord::Base
     has_many :blogs_readers, :dependent => :destroy
     has_many :readers, :through => :blogs_readers, :uniq => true
    end

    class Reader < ActiveRecord::Base
     has_many :blogs_readers, :dependent => :destroy
     has_many :blogs, :through => :blogs_readers, :uniq => true
    end

    class BlogsReaders < ActiveRecord::Base
      belongs_to :blog
      belongs_to :reader
    end

注意将:uniq =&gt; true 选项添加到has_many 调用中。

此外,您可能需要考虑在 Blog 和 Reader 之间使用 has_and_belongs_to_many,除非您希望在连接模型上具有其他一些属性(目前您没有)。该方法还有一个:uniq 选项。

请注意,这不会阻止您在表中创建条目,但它确实可以确保当您查询集合时,您只会获得每个对象中的一个。

更新

在 Rails 4 中,这样做的方法是通过范围块。上述更改为。

class Blog < ActiveRecord::Base
 has_many :blogs_readers, dependent:  :destroy
 has_many :readers,  ->  uniq , through: :blogs_readers
end

class Reader < ActiveRecord::Base
 has_many :blogs_readers, dependent: :destroy
 has_many :blogs, ->  uniq , through: :blogs_readers
end

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader
end

Rails 5 更新

在作用域块中使用uniq 会导致错误NoMethodError: undefined method 'extensions' for []:Array。请改用distinct

class Blog < ActiveRecord::Base
 has_many :blogs_readers, dependent:  :destroy
 has_many :readers,  ->  distinct , through: :blogs_readers
end

class Reader < ActiveRecord::Base
 has_many :blogs_readers, dependent: :destroy
 has_many :blogs, ->  distinct , through: :blogs_readers
end

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader
end

【讨论】:

如果您的连接模型有任何其他字段,我认为这种方法存在问题。例如,一个位置字段,以便每个子项都可以定位在其父项中。 blog.readers &lt;&lt; reader # blog_readers.position = 1; blog.readers &lt;&lt; reader # blog_readers.position = 2 由于第二个 blog_readers 的位置不同,uniq 设置不会将其视为现有条目并允许创建它 如果你有一个默认范围来为你的博客排序,你需要取消它的范围(否则 DISTINCT 会失败),你可以使用这个:has_many :blogs, -&gt; unscope(:order).uniq , through: :blog_readers 更新 @marksiemers 对 Rails 5.2 的回答 has_many :blogs, -&gt; unscope(:order).distinct , through: :blog_readers【参考方案2】:

这应该解决您的第一个问题:

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader

  validates_uniqueness_of :reader_id, :scope => :blog_id
end

【讨论】:

我一直在努力解决这个问题,但我从来没有想过!很好的解决方案!谢谢! 请在此处仔细阅读并发和完整性apidock.com/rails/ActiveRecord/Validations/ClassMethods/… 我认为这在 Rails 5 中运行良好(无论如何对我有用)【参考方案3】:

Rails 5.1 方式

class Blog < ActiveRecord::Base
 has_many :blogs_readers, dependent:  :destroy
 has_many :readers,  ->  distinct , through: :blogs_readers
end

class Reader < ActiveRecord::Base
 has_many :blogs_readers, dependent: :destroy
 has_many :blogs, ->  distinct , through: :blogs_readers
end

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader
end

【讨论】:

推理:github.com/rails/rails/pull/9683 和 github.com/rails/rails/commit/… @pastullo 但它仍然在中间表 blog_readers 中插入数据。如何预防?【参考方案4】:

怎么样:

Blog.find(:all,
          :conditions => ['id NOT IN (?)', the_reader.blog_ids])

Rails 通过关联方法为我们处理 id 的收集! :)

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

【讨论】:

另外,我想提一下,这可能是更好的方法,因为接受的答案从行中选择所有数据(例如,the_reader.blogs),而我的答案只选择来自行(例如,the_reader.blog_ids)。这是一个巨大的性能打击! 这是一个更好的解决方案,应该是正确的答案。谢谢乔希。【参考方案5】:

此链接上的答案显示了如何覆盖“Rails idiom to avoid duplicates in has_many :through

【讨论】:

【参考方案6】:

目前的最佳答案是在 proc 中使用 uniq

class Blog < ActiveRecord::Base
 has_many :blogs_readers, dependent:  :destroy
 has_many :readers,  ->  uniq , through: :blogs_readers
end

然而,这会将关系踢到一个数组中,并且可能会破坏期望对关系执行操作的东西,而不是数组。

如果您使用distinct,它会将其保留为关系:

class Blog < ActiveRecord::Base
 has_many :blogs_readers, dependent:  :destroy
 has_many :readers,  ->  distinct , through: :blogs_readers
end

【讨论】:

【参考方案7】:

我想有人会给出比这更好的答案。

the_reader = Reader.find(:first, :include => :blogs)

Blog.find(:all, 
          :conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])

[编辑]

请参阅下面 Josh 的回答。这是要走的路。 (我知道那里有更好的方法;)

【讨论】:

您也可以使用 find_by_sql 在一个语句中执行此操作。【参考方案8】:

我为 Rails 6 执行以下操作

class BlogsReaders < ActiveRecord::Base
  belongs_to :blog
  belongs_to :reader

  validates :blog_id, uniqueness:  scope: :reader_id 
end

不要忘记创建数据库约束以防止违反唯一性。

【讨论】:

【参考方案9】:

最简单的方法是将关系序列化成数组:

class Blog < ActiveRecord::Base
  has_many :blogs_readers, :dependent => :destroy
  has_many :readers, :through => :blogs_readers
  serialize :reader_ids, Array
end

然后在为读者分配值时,您将它们应用为

blog.reader_ids = [1,2,3,4]

以这种方式分配关系时,会自动删除重复项。

【讨论】:

以上是关于如何避免 has_many :through 关系中的重复?的主要内容,如果未能解决你的问题,请参考以下文章

有关如何使用has_many:through关系正确设置验证的指导?

Rails 成语避免在 has_many 中重复:通过

Rails RSpec 测试 has_many :through 关系

设置多态 has_many :through 关系

ActiveRecord、has_many :through 和多态关联

使用 has_many :through 和 build