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

How can i write a method that can generate scope dynamically which can be used for multiple models in 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 个字符,为 reader 提供了额外的上下文。 Person.where(name: 'John Doe') 说:在 Person 上执行查询 (where) 和 return 符合条件 name: 'John Doe'.

的集合

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

上面直接说明了另一个问题,就是名称冲突。 Person.name 已被占用,return 是 class 名称。所以添加 scope :name, ->(name) { where(name: name) } 会引发 ArgumentError.

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

完成上述内容后,这里有一些实际的解决方案。


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

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) } 是可选的,但可以防止创建与当前 public/private class 方法名称冲突的范围。这将跳过那些属性。您可以安全地省略此行,但是 scope 方法在传递危险范围名称时可能会引发 ArgumentError

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


以上应该让您了解如何处理事情,您甚至可以添加 :except:only 等选项。如果 class 变得混乱,还可以选择将上面的代码提取到模块和 extend AttributeScopeHelpersApplicationRecord

但是,就像我开始回答这个问题一样,我建议不要为每个属性添加范围。