模块中的元编程作用域导致 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
那么神奇,但魔法总是有代价的。
我有一个包含人员模型和字段模型的 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
那么神奇,但魔法总是有代价的。