不幸的是,多参数分配是 ActiveRecord 中的一种实现,而不是 ActiveModel。因此,Mongoid 必须有自己的实现,但他们放弃了对该功能的支持,将其留给 ActiveSupport 和 ActiveModel 来弥补。好吧,看看 Rails 源代码,它仍然在 ActiveRecord 中。
幸运的是,您可以在 process_attributes 方法中挂钩您自己的实现,该方法在 Mongoid 对象上分配属性时调用,例如在创建或更新操作期间。
要对其进行测试,只需创建一个 config/initializer/multi_parameter_attributes.rb 并添加以下代码,该代码会将必要的功能添加到 Mongoid::Document 模块中:
module Mongoid
module MultiParameterAttributes
module Errors
class AttributeAssignmentError < Mongoid::Errors::MongoidError
attr_reader :exception, :attribute
def initialize(message, exception, attribute)
@exception = exception
@attribute = attribute
@message = message
end
end
class MultiparameterAssignmentErrors < Mongoid::Errors::MongoidError
attr_reader :errors
def initialize(errors)
@errors = errors
end
end
end
def process_attributes(attrs = nil)
if attrs
errors = []
attributes = attrs.class.new
attributes.permit! if attrs.respond_to?(:permitted?) && attrs.permitted?
multi_parameter_attributes = {}
attrs.each_pair do |key, value|
if key =~ /\A([^\(]+)\((\d+)([if])\)$/
key, index = $1, $2.to_i
(multi_parameter_attributes[key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}")
else
attributes[key] = value
end
end
multi_parameter_attributes.each_pair do |key, values|
begin
values = (values.keys.min..values.keys.max).map { |i| values[i] }
field = self.class.fields[database_field_name(key)]
attributes[key] = instantiate_object(field, values)
rescue => e
errors << Errors::AttributeAssignmentError.new(
"error on assignment #{values.inspect} to #{key}", e, key
)
end
end
unless errors.empty?
raise Errors::MultiparameterAssignmentErrors.new(errors),
"#{errors.size} error(s) on assignment of multiparameter attributes"
end
super attributes
else
super
end
end
protected
def instantiate_object(field, values_with_empty_parameters)
return nil if values_with_empty_parameters.all? { |v| v.nil? }
values = values_with_empty_parameters.collect { |v| v.nil? ? 1 : v }
klass = field.type
if klass == DateTime || klass == Date || klass == Time
field.mongoize(values)
elsif klass
klass.new(*values)
else
values
end
end
end
module Document
include MultiParameterAttributes
end
end
那么这段代码有什么作用呢?我们创建了一个数据结构 multi_parameter_attributes,它将存储与以下正则表达式模式匹配的任何属性:/\A([^(]+)((\d+)([if]))$/。\A 匹配字符串的开头. 通常你习惯看到 ^ 来匹配字符串的开头,但是 \A 和它的对应 \Z 将匹配而不考虑换行符。我们有 3 个捕获组。第一个 ([^(]+) 将匹配所有字符不是左括号。在字符串“starttime(1i)”中,它将捕获“starttime”。第二个捕获组 (\d+) 将捕获数字。所以“starttime(1i)”中的“1” . 第三个捕获组 ([if]) 将捕获 i 或 f。 i 指的是整数值。
现在通常一个日期时间字段有很多部分,如下所示:
starttime(1i) => 2019
starttime(2i) => 6
starttime(3i) => 28
starttime(4i) => 19
starttime(5i) => 18
因此,我们正在遍历属性哈希,以便将我们的数据结构构建到 multi_parameter_attributes 中:
attrs.each_pair do |key, value|
...
end
请记住,我们在正则表达式中使用了捕获组。稍后我们可以使用 Ruby 的 $1、$2 等全局变量来引用捕获的组。key 是属性的名称,例如 starttime。index 指的是日期时间中属性的部分,比如年、月、日等。而$3保存了第三个捕获组中的i,因为我们要取字符串值并将其转换为整数:
key, index = $1, $2.to_i
(multi_parameter_attributes[key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}")
最终,我们得到了一个很好的数据结构,如下所示:
{ starttime: { 1 => 2019, 2 => 6, 3 => 28, 4 => 19, 5 => 18 } }
现在我们做一些智能的事情来获取实际的日期部分:
values = (values.keys.min..values.keys.max).map { |i| values[i] }
这会给我们:
[2019, 6, 28, 19, 18]
好吧,现在我们有了我们想要的日期。剩下的就是使用 Mongoid API 生成一个字段对象来存储日期。