仅针对特定模型覆盖ActiveRecord :: Relation :: CollectionProxy

时间:2019-04-12 12:12:28

标签: ruby activerecord ruby-on-rails-5 query-optimization

当前,我正在尝试最小化rails应用程序中SQL查询的数量。并且使用范围进行记录过滤,发现在我的服务器日志中,该范围触发了SQL查询,尽管它显示了CACHE(0.00毫秒),即使它正在使用记录的属性过滤记录。 (例如,使用obj.status进行过滤)

每次调用

时,我都试图在模型中使用class方法
def pdf_files
  all.select(&:pdf?)
end

范围,

scope :pdf_files, -> { all.select(&:pdf?) }

它触发sql查询。

我尝试覆盖ActiveRecord :: Associations :: CollectionProxy以包括此方法,

class ActiveRecord::Associations::CollectionProxy
  def pdf_files
    to_a.select(&:pdf?)
  end
end

有效,但是也可以从其他模型中调用此方法。

我尝试使用块来通过诸如此类的额外方法扩展您的关联。

has_many :files, do
  def pdf_files
    to_a.select(&:pdf?)
  end
end

这对我来说有效,它选择了没有任何SQL查询的文件。

但事实是。我需要仅适用于File模型的这些方法,因为与文件模型相关联的所有其他模型都需要这些方法,并且我不想破坏 @ email.files.pdf 这样的rails约定。

还发现在Rails控制台中, 当我输入File :: ProxyCollection时,它给我ActiveRecord :: Associations :: CollectionProxy。但是不知道如何添加方法。

我希望能够调用 @ email.files.pdf?,@ email.files.doc?,以过滤结果,而无需执行任何查询,即使它正在调用CACHE SQL查询。

模型定义

class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true
  scope :document_files, -> { all.select(&:document?) }
  scope :ebook_files, -> { all.select(&:ebook?) }

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

class TestEmail < ApplicationRecord
  has_many :test_files, as: :fileable
end

class TestPost < ApplicationRecord
  has_many :test_files, as: :fileable
end

在控制台中

email = TestEmail.includes(:test_files).first

document_files = email.test_files.document_files

针对document_files范围执行的查询为 enter image description here

但是如果我做类似的事情,

class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

class TestEmail < ApplicationRecord
  has_many :test_files, as: :fileable do
    def document_files
      to_a.select(&:document?)
    end
  end
end

class TestPost < ApplicationRecord
  has_many :test_files, as: :fileable do
    def document_files
      to_a.select(&:document?)
    end
  end
end

加载结果而无需任何查询 enter image description here

1 个答案:

答案 0 :(得分:1)

所以您有一个多态关联,并且您希望以尽可能少的查询来获取关联的数据,对吗?

对于我来说,任务本身(“最小化查询数量”)只有在合理的限制下才有意义。摆脱N + 1总是一件好事,但是尝试在1个查询中执行所有工作或诸如此类的操作不一定是个好主意-复杂的查询可能比随后的几个琐碎的查询要慢。此外,复杂的棘手查询往往容易出错。

现在让我们回到您的代码。首先,这个想法

scope :document_files, -> { all.select(&:document?) }

对我来说看起来很糟糕,原因有两个:

  1. 所有内容都已加载到内存中,如果表很大,会很痛。有时这是必要的权衡,但是在这种情况下(不需要任何繁重的处理,只需过滤数据即可)

  2. 此“作用域”不是常规的作用域-调用它会为您提供Array而不是AR关系,因此不能像适当的AR作用域那样链接它。它只是一个有误导性的代码,有气味。只是试一下。像TestFile.document_files.where(<some_extra_conditions>)一样,您会得到NoMethodError-可能不是人们从简单易懂的代码中所期望的结果...

在您的情况下,使用关联扩展也没有用-它使您的代码看起来更神秘,但并没有真正给您带来任何好处。假设它以某种特殊的方式工作是错误的-实际上,它的工作与您在关联代理上显式调用to_a.select...完全一样。

我建议一些更简单,更惯用的东西:

class TestFile < ApplicationRecord
  belongs_to :fileable, polymorphic: true, optional: true
  scope :document_files, -> { where(file_type: :document) }
  scope :ebook_files, -> { where(file_type: :ebook) }

  enum file_type: [:document, :ebook, :paper, :article, :picture]
end

...

然后,在适当的地方使用预加载,您可以获得较少数量的查询(无N + 1)。

是否想进一步优化?嗯,有很多方法可以使用更底层的API来执行此操作:例如,您可以将connection#select_all与任意复杂的查询一起使用,然后从ActiveRecord::Result手动实例化必要的记录,而无需进行更多查询...但是AR是一个自以为是的OR​​M框架,如果您要“与之抗争”,您可能很快就会得到混乱且不可维护的代码。

如果您确实需要更灵活的东西,建议您尝试其他选择(续集,ROM)...