如何避免 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 => 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 << reader # blog_readers.position = 1;
blog.readers << reader # blog_readers.position = 2
由于第二个 blog_readers 的位置不同,uniq 设置不会将其视为现有条目并允许创建它
如果你有一个默认范围来为你的博客排序,你需要取消它的范围(否则 DISTINCT 会失败),你可以使用这个:has_many :blogs, -> unscope(:order).uniq , through: :blog_readers
更新 @marksiemers 对 Rails 5.2 的回答 has_many :blogs, -> 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 RSpec 测试 has_many :through 关系