【问题标题】:Correct way to handle multiparameter attributes corresponding to virtual attributes处理对应于虚拟属性的多参数属性的正确方法
【发布时间】:2013-07-05 06:33:55
【问题描述】:

我有一个带有 birthdate 属性的模型的 Rails 应用程序。这对应于我的数据库中使用 ActiveRecord date 类型定义的列。有了这个,我可以使用date_select 表单辅助方法在我的视图中将其呈现为三选输入。然后将与该字段对应的表单参数序列化回控制器为birthdate(1i)birthdate(2i)birthdate(3i)。因此,我可以在我的模型上的控制器中使用标准 update_attributes 方法来更新模型上的所有字段。

我现在正在尝试使用 attr_encrypted gem 加密这个字段。虽然 gem 支持编组(这很好),但不再有名称为 birthdate 类型为 date 的真实列 - 相反,attr_encrypted 将值公开为 virtual 属性 @987654336 @ 由真实的 encrypted_birthdate 列支持。这意味着update_attributes 无法执行之前的多参数属性分配来填充和保存此列。相反,我得到一个 MultiparameterAssignmentErrors 错误,这是由于调用内部 column_for_attribute 方法返回此列的 nil(来自 execute_callstack_for_multiparameter_attributes 内的某处)。

我目前正在解决这个问题,如下所示:

我在app/models/person.rb中的模特:

class Person < ActiveRecord::Base
  attr_encrypted :birthdate
end

我的控制器在app/controllers/people_controller.rb:

class PeopleController < ApplicationController
  def update

    # This is the bit I would like to avoid having to do.
    params[:person] = munge_params(params[:person])

    respond_to do |format|
      if @person.update_attributes(params[:person])
        format.html { redirect_to @person, notice: 'Person was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: "edit" }
        format.json { render json: @person.errors, status: :unprocessable_entity }
      end
    end
  end

  private

  def munge_params(params)
    # This separates the "birthdate" parameters from the other parameters in the request.
    birthdate_params, munged_params = extract_multiparameter_attribute(params, :birthdate)

    # Now we place a scalar "birthdate" where the multiparameter attribute used to be.
    munged_params['birthdate'] = Date.new(
      birthdate_params[1],
      birthdate_params[2],
      birthdate_params[3]
    )

    munged_params
  end

  def extract_multiparameter_attribute(params, name)
    # This is sample code for demonstration purposes only and currently
    # only handles the integer "i" type.
    regex = /^#{Regexp.quote(name.to_s)}\((\d+)i)\)$/
    attribute_params, other_params = params.segment { |k, v| k =~ regex }
    attribute_params2 = Hash[attribute_params.collect do |key, value|
      key =~ regex or raise RuntimeError.new("Invalid key \"#{key}\"")
      index = Integer($1)
      [index, Integer(value)]
    end]
    [attribute_params2, other_params]
  end

  def segment(hash, &discriminator)
    hash.to_a.partition(&discriminator).map do |a|
      a.each_with_object(Hash.new) { |e, h| h[e.first] = e.last }
    end
  end
end

我的看法app/views/people/_form.html.erb:

<%= form_for @person do |f| %>
    <%= f.label :birthdate %>
    <%= f.date_select :birthdate %>

    <% f.submit %>
<% end %>

在不必像这样引入对 params 数组的临时修改的情况下,处理此类属性的正确方法是什么?

更新: 看起来this 可能指的是相关问题。还有this

另一个更新:

这是我目前的解决方案,基于 Chris Heald 的回答。这段代码应该添加到Person模型类中:

class EncryptedAttributeClassWrapper
  attr_reader :klass
  def initialize(klass); @klass = klass; end
end

# TODO: Modify attr_encrypted to take a :class option in order
# to populate this hash.
ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS = {
  :birthdate => EncryptedAttributeClassWrapper.new(Date)
}

def column_for_attribute(attribute)
  attribute_sym = attribute.to_sym
  if encrypted = self.class.encrypted_attributes[attribute_sym]
    column_info = ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS[attribute_sym]
    column_info ||= super encrypted[:attribute]
    column_info
  else
    super
  end
end

此解决方案按原样工作,但如果 attr_encrypted 采用 :class 选项来动态构造 ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS 哈希值会更好。我将研究如何扩展/monkeypatch attr_encrypted 来做到这一点。要点在这里:https://gist.github.com/rcook/5992293

【问题讨论】:

    标签: ruby-on-rails ruby


    【解决方案1】:

    您可以对模型进行monkeypatch 以通过column_for_attribute 调用。我没有对此进行测试,但它应该会导致 birthday 字段上的反射,而不是返回 encrypted_birthday 字段的反射,这应该会导致多参数属性正确分配(因为 AR 将能够推断字段类型):

    def column_for_attribute(attribute)
      if encrypted = encrypted_attributes[attribute.to_sym]
        super encrypted[:attribute]
      else
        super
      end
    end
    

    我们正在根据this line 修补column_for_attribute,以便AR 可以推断出列的正确类型。它需要弄清楚“生日”的参数应该是什么类型的DateTime,并且不能从虚拟属性中推断出来。将反射映射到实际列应该可以解决这个问题。

    【讨论】:

    • 谢谢,克里斯。我已将您的建议标记为解决方案。您将在四天内获得赏金!我已经更新了我的原始问题,以包括我当前基于您的工作解决方案。由于attr_encrypted 不幸地将所有加密列视为字符串,因此您所说的解决方案并不完全有效:额外的工作是将列的预期数据类型存储在其他地方。这就是ENCRYPTED_ATTRIBUTE_CLASS_WRAPPERS 在我的变体中所做的。理想情况下,attr_encrypted 会解决这个问题。
    • 酷,很高兴你把它整理好了。也许对attr_encrypted gem 的拉取请求是有序的?
    • 是的。在我们说话的时候,我正在处理 attr_encrypted 的扩展!
    • 我已经为 Rails > 4.2 更新了这个答案,因为 Rails 在分配多参数属性时不再使用 column_for_attribute。 Richard Cook 的要点适用于 Rails gist.github.com/stevehodges/dde0da195da29300e9a8bb3cfc337eb4
    猜你喜欢
    • 2010-09-20
    • 1970-01-01
    • 1970-01-01
    • 2014-10-18
    • 1970-01-01
    • 1970-01-01
    • 2020-11-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多