【问题标题】:how to avoid duplicates in a has_many :through relationship?如何避免 has_many :through 关系中的重复?
【发布时间】: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)。我怎样才能做到这一点?

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

【问题讨论】:

    标签: ruby-on-rails ruby duplicates has-many-through has-many


    【解决方案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
    

    【讨论】:

    【解决方案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
    

    【讨论】:

    【解决方案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]
          

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-07-03
            • 1970-01-01
            • 2017-01-03
            • 2012-07-21
            • 2011-04-27
            • 1970-01-01
            相关资源
            最近更新 更多