【问题标题】:Rails 4: composed_of mapping to JSON store attributeRails 4:composed_of 映射到 JSON 存储属性
【发布时间】:2014-04-02 08:07:16
【问题描述】:

我设置了以下模型

# task.rb
class Task << AR
  # everything all task objects have in common
end

# login_request.rb
class Tasks::LoginRequest < Task
  store :data, accessors: [:email, :first_name, :last_name, :expires_at]

  composed_of :valid_until, class_name: 'DateTime', mapping: %w(expires_at to_s), constructor: Proc.new { |date| (date && date.to_datetime) || DateTime.now }, converter: Proc.new { |value| value.to_s.to_datetime }
end

我在表单中使用了datetime_select 助手:

# _form.html.haml
= f.datetime_select :valid_until

这很好用,但是当我在控制器中使用提交的表单数据调用 update 时,我收到以下错误消息:

1 error(s) on assignment of multiparameter attributes [error on assignment [2014, 4, 2, 9, 48] to valid_until (can't write unknown attribute 'expires_at')]

所以,我猜测更新后的方法试图直接操作attributes 哈希,但显然它找不到属性expires_at,因为它是JSON 列data 的简单访问器方法。

我知道我可以简单地将这个字段添加到数据库中,它可能会起作用 - 尽管那时不需要 composed_of 语句。但我宁愿不走这条路,因为不是每个任务都有expires_at 列。

我该如何克服这个错误?还是我错过了什么?

【问题讨论】:

    标签: ruby-on-rails-4 virtual-attribute


    【解决方案1】:

    目前 compose_of 不支持这种情况,因为它直接写入假定在数据库中的属性。我写了一个经过调整的 compose_of 版本(基于 Rails 4.0.2 版本)

    把它放在初始化文件夹中:

    #/initialize/support_store_in_composed_of.rb
    module ActiveRecord
      module Aggregations
        extend ActiveSupport::Concern
    
        def clear_aggregation_cache #:nodoc:
          @aggregation_cache.clear if persisted?
        end
    
        module ClassMethods
    
          def composed_of_with_store_support(part_id, options = {})
            options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter, :store)
    
            name        = part_id.id2name
            class_name  = options[:class_name]  || name.camelize
            mapping     = options[:mapping]     || [ name, name ]
            mapping     = [ mapping ] unless mapping.first.is_a?(Array)
            allow_nil   = options[:allow_nil]   || false
            constructor = options[:constructor] || :new
            converter   = options[:converter]
    
            reader_method(name, class_name, mapping, allow_nil, constructor, options[:store])
            writer_method(name, class_name, mapping, allow_nil, converter, options[:store])
    
            create_reflection(:composed_of, part_id, nil, options, self)
          end
    
          private
            def reader_method(name, class_name, mapping, allow_nil, constructor, store=nil)
              define_method(name) do
                if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
                  if store.present?
                    attrs = mapping.collect {|pair| send(pair.first)}
                  else
                    attrs = mapping.collect {|pair| read_attribute(pair.first)}
                  end
    
                  object = constructor.respond_to?(:call) ?
                  constructor.call(*attrs) :
                  class_name.constantize.send(constructor, *attrs)
    
                  @aggregation_cache[name] = object
                end
                @aggregation_cache[name]
              end
            end
    
            def writer_method(name, class_name, mapping, allow_nil, converter, store=nil)
    
              define_method("#{name}=") do |part|
                klass = class_name.constantize
                unless part.is_a?(klass) || converter.nil? || part.nil?
                  part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
                end
                if part.nil? && allow_nil
                  mapping.each { |pair| self[pair.first] = nil }
                  @aggregation_cache[name] = nil
                else
                  if store.present?      
                    mapping.each { |pair| send("#{pair.first}=", part.send(pair.last)) } 
                  else
                    mapping.each { |pair| self[pair.first] = part.send(pair.last) }
                  end
                  @aggregation_cache[name] = part.freeze
    
                end
              end
            end
        end
      end
    end
    

    像这样使用它可以解决您的问题。

    class Task < ActiveRecord::Base
    
      store :data, accessors: [:email, :first_name, :last_name, :expires_at]
    
    
      composed_of_with_store_support :valid_until,  class_name: 'DateTime', mapping: %w(expires_at to_s),
       constructor: Proc.new { |date| (date && date.to_datetime) || DateTime.now },
        converter: Proc.new { |value| value.to_s.to_datetime },
        store: true
    
    end
    

    【讨论】:

    • 感谢您的回复。我有 valid_until 直接作为存储访问器,但是我不能在其上使用 datetime_select 助手,因为魔法仍然有效,因为我得到了提到的多参数错误。而且我知道我可以创建一个手动包装器,但我想避免自己编程,因为所有功能基本上都在那里 - 但它没有按预期工作。
    • 我明白了。它现在不那样工作,所以我写了一个版本。
    猜你喜欢
    • 2017-02-07
    • 2019-07-05
    • 1970-01-01
    • 1970-01-01
    • 2019-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多