【问题标题】:Metaprogrammed Scope in Module causing NoMethodError模块中的元编程范围导致 NoMethodError
【发布时间】:2020-04-15 12:51:53
【问题描述】:

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

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

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

可以在运行时将字段添加到应用程序中,并且范围用于过滤人员表中的缺失记录。这一切都是使用 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

【问题讨论】:

  • 您所做的基本上是重新发明实体属性值(反)模式。由于类缓存,在运行时对类进行整个元编程的方法不太可能完全成功。感觉就像您试图将某些东西硬塞到一个不属于的关系数据库中,而像 mongodb 这样的文档存储会更合适。如果您决定要深入这个兔子洞,请使用单表继承来创建不同的字段类,而不是使用枚举和切换解决方案,这在单个类中发挥了很大作用。
  • 感谢您的评论。你是对的,这是 EAV,看看这个项目将来是否可以转移到 MongoDB 肯定是有意义的。就目前而言,这是不可能的。我可以看到 STI 会简化 Field 类中的事情
  • 我不完全确定我将如何处理这个问题,但是在实际设置内容或验证记录或查询时,您必须实际执行数据库查询和即时评估事物,而不是对类进行元编程。

标签: ruby-on-rails metaprogramming


【解决方案1】:

我将首先使用 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 表,其中包含规范化的字段类型,例如:

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

实际值存储在field_values EAV 表中:

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 那样神奇,但神奇总是要付出代价的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-18
    • 2011-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多