模块中的元编程作用域导致 NoMethodError

Metaprogrammed Scope in Module causing NoMethodError

我有一个包含人员模型和字段模型的 HR 系统。 Person 有一些属性存储在常规数据库列中,还有一些可以动态添加。

字段 table 对人物 table 的每个数据库列都有一条记录。这些是系统必填字段。管理员可以在配置应用程序时添加任意数量的(非系统必需的)字段。他们还可以设置字段的属性,例如它们是否是强制性的。

对于非必填字段,管理员可能希望在用户主页上添加一个小部件,显示有多少人缺少此属性。例如,管理员可以添加一个 :personal_email 字段,以及一个显示有多少人没有输入该字段的小部件。

可以在运行时将字段添加到应用程序,范围用于过滤人员 table 以查找丢失的记录。这一切都是使用 PersonField 模块完成的。添加新字段并请求小部件时,应用程序会产生错误 NoMethodError: undefined method `missing_personal_email' for #<Person::ActiveRecord_Relation>

重新启动 rails 服务器时,错误没有出现。我认为这可能与 cache_classes 有关,但它发生在开发中,但它是错误的。我如何重构 PersonField 模块来避免这个问题?

class Person < ActiveRecord::Base
  include PersonField
end

class Field < ActiveRecord::Base   
    enum field_type: {:boolean => 1, :integer => 2, :string => 3, :date => 4, :time => 5, :datetime => 6, :float => 7, :decimal => 8, :reference => 9, :any => 10, :email => 11, :phone => 12, :text => 13, :currency => 14, :postcode => 15}
    enum widget:  { :not_set => 0, :missing => 1, :not_missing => 2 }

    scope :widget, -> { where.not(widget: 0) }
    scope :system_required, -> {where(system_required: 1)}
    scope :not_system_required, -> {where(system_required: 0)}
end

module PersonField
    included do
        typed_store :data do |s|
            Field.active.each do |f|
                case f.field_type.to_sym
                when :integer, :reference
                    s.integer f.name.to_sym
                when :string, :text, :email, :phone, :postcode
                    s.string f.name.to_sym
                when :datetime
                    s.datetime f.name.to_sym
                # etc for all field types
                else
                    s.any f.name.to_sym
                end
            end
        end
    end

    Field.active.system_required.widget.uniq.each do |f|  
       scope "#{f.widget}_#{f.name}", -> { where("people.#{f.name} IS NULL") } 
    end

    Field.active.not_system_required.widget.pluck(:name).uniq.each do |f|
       # EG for :personal_email field this gives the SQL condition: people.data NOT LIKE '%personal_email%' OR people.data LIKE '%personal_email: \n%' 
       scope "#{f.widget}_#{f.name}", -> { where("people.data NOT LIKE '%#{f.name}%' OR people.data LIKE '%#{f.name}: \n%'") }
    end
end

我将从使用 STI 设置 EAV 系统开始:

module DynamicFields
  # Represents the normalized A in EAV
  class FieldType
    self.abstract_class = true
    self.table_name = 'field_types'
  end

  def self.exist?(name)
     FieldType.exist?(name: name)
  end
end

module DynamicFields
  class StringType < FieldType
  end
end

module DynamicFields
  class IntegerType < FieldType
  end
end

# more types ...

module DynamicFields
  # This is the V in EAV
  class FieldValue
    self.table_name = 'field_values'
    belongs_to :person # this is the E in EAV
    belongs_to :field_type # this is the A in EAV
  end
end

class Person
  has_many :field_types, 
    class_name: 'DynamicFields::FieldType'
  has_many :field_values, 
    class_name: 'DynamicFields::FieldValue'
end

这里发生了一些事情,但你基本上有一个 field_types table 和规范化的字段类型,例如:

id | type            | name            | required
1  | StringType      | display_name    | false
2  | IntegerType     | age             | false

实际值存储在 field_values EAV table:

id | field_type_id  | person_id    | value (JSON)   
1  | 1              | 1            | "Mr Loverman"
2  | 2              | 2            | 21 

你用 missing_personal_email 做的事情实际上非常像 Rails Dynamic Finders,最终从框架中被砍掉了,可以通过 method_missing:

实现
module DynamicFields
  module MissingFinders
    # name is the name of the method that was called
    def method_missing(method_name, *args, **kwargs, &block)
      return super unless method_name.start_with?('missing_')  
      self.joins(field_values: :field_type)
          .where(field_values: { 
             value: nil, 
             field_type: {
               method_name.strip('missing_')
             }
          }
      )
    end
  end
end

它是否真的是个好主意值得怀疑,因为它引入了许多潜在的错误和性能问题。我只想编写一个采用参数的普通方法:

module DynamicFields
  module Scopes
    def missing_field(*fields)
       where(field_values: { 
         value: nil, 
         field_type: {
           name: fields
         }
       })
    end
  end
end

是的Person.missing_field(:personal_email)它不像Person.missing_personal_email那么神奇,但魔法总是有代价的。