我如何编写一种可以动态生成范围的方法,该方法可用于 ruby​​ on rails 中的多个模型

问题描述

目前,现有的范围是这样的。

module TransactionScopes
      extend ActiveSupport::Concern
      included do
        scope :status,->(status) { where status: status }
        scope :portfolio_id,->(portfolio_id) { where portfolio_id: portfolio_id }
        scope :investor_external_reference_id,->(investor_external_reference_id) { where investor_external_reference_id: investor_external_reference_id }
        scope :portfolio_external_reference_id,->(portfolio_external_reference_id) { where portfolio_external_reference_id: portfolio_external_reference_id }
        scope :file_id,->(file_id) { where back_office_file_id: file_id }
        scope :oms_status,->(status) { where oms_status: status }
        scope :order_id,->(order_id) { where order_id: order_id }
        scope :order_status,->(order_status) { where order_status: order_status }
        scope :transaction_id,->(transaction_id) { where transaction_id: transaction_id }
end

我有更多具有相似范围的模型,我可以编写更通用的方式来避免这些重复的过程。

解决方法

我强烈建议您不要为每个属性添加范围。 where(...) 只有 5 个字符,为读者提供了额外的上下文。 Person.where(name: 'John Doe') 表示:在 Person 上执行查询 (where) 并返回符合条件 name: 'John Doe' 的集合。

如果您添加建议属性范围,该行将变为 Person.name('John Doe')。通过删除这是一个查询的上下文,读者必须“了解”每个属性名称也可以作为范围访问。

上面立即显示了另一个问题,即名称冲突。 Person.name 已被占用,并返回类名。因此,添加 scope :name,->(name) { where(name: name) } 将引发 ArgumentError

作用域可能很有用,但如果使用过多,它们会弄乱模型的类方法命名空间。

除此之外,这里有一些实际的解决方案。


您可以编写一个帮助程序,让您可以轻松地为属性创建作用域。然后遍历传递的属性并为它们动态创建作用域。

class ApplicationRecord < ActiveRecord::Base
  class << self

    private

    def attribute_scope_for(*attributes)
      attributes.each { |attr| scope attr,->(value) { where(attr => value) } }
    end
  end
end

然后在你的模型中调用这个助手。

class YourModel < ApplicationRecord
  attribute_scopes_for :status,:portfolio_id # ....
end

或者,如果您想为每个属性创建一个范围,您可以使用 attribute_names 动态收集它们。然后遍历它们并根据名称创建范围。

class ApplicationRecord < ActiveRecord::Base
  class << self

    private

    def enable_attribute_scopes
      attribute_names
        .reject { |attr| respond_to?(attr,true) }
        .each { |attr| scope attr,->(value) { where(attr => value) } }
    end
  end
end

在上面的代码段中,.reject { |attr| respond_to?(attr,true) } 是可选的,但会阻止创建与当前公共/私有类方法名称冲突的范围。这将跳过这些属性。您可以安全地省略这一行,但 scope 方法在传递危险范围名称时可能会引发 ArgumentError

现在剩下要做的就是在要启用属性范围的模型中调用 enable_attribute_scopes


以上应该让您了解如何处理事情,您甚至可以添加诸如 :except:only 之类的选项。如果类变得混乱,还可以选择将上面的代码提取到模块中,并在 extend AttributeScopeHelpers 中提取 ApplicationRecord

然而,就像我开始这个答案一样,我建议不要为每个属性添加范围。