如何在表单中编辑Rails序列化字段?

57

我在我的Rails项目中有一个数据模型,其中有一个序列化字段:

class Widget < ActiveRecord::Base
  serialize :options
end

选项字段可以包含可变的数据信息。例如,这是从模拟数据文件中获取的一个记录的选项字段:

  options:
    query_id: 2 
    axis_y: 'percent'
    axis_x: 'text'
    units: '%'
    css_class: 'occupancy'
    dom_hook: '#average-occupancy-by-day'
    table_scale: 1

我的问题是,在标准表单视图中,让用户编辑此信息的正确方法是什么?

如果您只是为选项字段使用一个简单的文本区域字段,则只会得到一个yaml dump表示,并且该数据将以字符串形式返回。

在Rails中,编辑序列化哈希字段的最佳/正确方法是什么?


你想要什么样的界面?是否应该为每个属性设置字段?你是否事先知道所有属性是什么? - BJ Clark
所有属性不会一开始就知道。有些是标准的,将始终存在,但其余的可以由用户定义。这是一个仅限管理员使用的界面,因此我比平常更信任用户输入。实际上,我只是使用了一个文本区域框,并让用户使用 YAML 标记输入键值对,一直都很好用。 - cpjolicoeur
8个回答

58

如果您预先知道选项键将是什么,您可以像这样声明特殊的getter和setter:

If you know what the option keys are going to be in advance, you can declare special getters and setters for them like so:


class Widget < ActiveRecord::Base
  serialize :options

  def self.serialized_attr_accessor(*args)
    args.each do |method_name|
      eval "
        def #{method_name}
          (self.options || {})[:#{method_name}]
        end
        def #{method_name}=(value)
          self.options ||= {}
          self.options[:#{method_name}] = value
        end
        attr_accessible :#{method_name}
      "
    end
  end

  serialized_attr_accessor :query_id, :axis_y, :axis_x, :units
end

这个好处在于它将选项数组的组件作为属性公开,这使您可以像这样使用Rails表单助手:

#haml
- form_for @widget do |f|
  = f.text_field :axis_y
  = f.text_field :axis_x
  = f.text_field :unit

谢谢,但我事先不知道所有选项的值。其中一些将始终存在,但其余部分可以由用户定义。 - cpjolicoeur
是的,动态创建这些访问器会很棒。 - Artur79
1
在 eval 结尾添加 attr_accessible 方法名是一个不错的选择。 - Artur79
解决方案是否也会在保存时更新哈希值?即使不是所有键都处于形式上?它不会删除现有记录吗? - aldrien.h

40

我也曾遇到同样的问题,但尽量不要过度设计。问题在于,虽然你可以将序列化的哈希传递给fields_for函数,但fields_for函数会认为它是一个选项哈希(而不是你的对象),并将表单对象设置为nil。这意味着,尽管你可以编辑值,但在编辑后它们不会出现。这可能是Rails的一个bug或者是未预料到的行为,并且也许在将来会得到修复。

然而,现在很容易解决这个问题(虽然我花了整个上午才找到解决方法)。

你可以保留你的模型不变,在视图中需要将fields for的对象作为一个open struct传递。它将正确设置记录对象(所以f2.object会返回你的选项),并且其次它允许text_field builder从你的对象/参数中访问值。

由于我包含了" || {}",它也适用于新建/创建表单。

= form_for @widget do |f|
  = f.fields_for :options, OpenStruct.new(f.object.options || {}) do |f2|
    = f2.text_field :axis_y
    = f2.text_field :axis_x
    = f2.text_field :unit

祝您拥有愉快的一天


1
干得好。这个提示今晚节省了我一些时间。我没有想到对象是哈希会被解释为选项哈希的事实,但如果确实是这样,那就很有道理。 - Legion
1
我非常喜欢这个解决方案,我已经将其添加到我的模型中以完成它:def options OpenStruct.new self[:options] || {} end,这样当读取属性时,你总是可以得到OpenStruct。 - Tim Baas
2
我认为有一个小错误:你应该在f2上调用text_field,而不是f - Sidhannowe
谢谢 Sidhannowe 的提示,我已经修复了 ^_^ - Christian Butzke
解决方案是否也更新了保存的哈希值? - aldrien.h

29

emh已经接近成功了。我认为Rails应该会将值返回到表单字段中,但实际上不会。因此,您可以在“:value =>”参数中手动输入每个字段的值。它看起来不太流畅,但它有效。

以下是完整步骤:

class Widget < ActiveRecord::Base
    serialize :options, Hash
end

<%= form_for :widget, @widget, :url => {:action => "update"}, :html => {:method => :put} do |f| %>
<%= f.error_messages %>
    <%= f.fields_for :options do |o| %>
        <%= o.text_field :axis_x, :size => 10, :value => @widget.options["axis_x"] %>
        <%= o.text_field :axis_y, :size => 10, :value => @widget.options["axis_y"] %>
    <% end %>
<% end %>

"fields_for"中添加的任何字段都将显示在序列化哈希中。您可以随意添加或删除字段。它们将作为属性传递给“options”哈希并存储为YAML。


我正在尝试按照您所说的去做,但似乎对我不起作用:https://dev59.com/hGw15IYBdhLWcg3wbLLU - Nicolas Raoul

5

我一直在遇到一个非常相似的问题。我在这里找到的解决方案对我非常有帮助。感谢 @austinfromboston,@Christian-Butske,@sbzoom以及其他所有人。然而,我认为这些答案可能已经过时了。以下是针对Rails 5和ruby 2.3对我有效的方法:

在表单中:

<%= f.label :options %>
<%= f.fields_for :options do |o| %>
  <%= o.label :axis_y %>
  <%= o.text_field :axis_y %>
  <%= o.label :axis_x %>
  <%= o.text_field :axis_x %>
  ...
<% end %>

然后在控制器中,我需要更新强参数,就像这样:

def widget_params
    params.require(:widget).permit(:any, :regular, :parameters, :options => [:axis_y, :axis_x, ...])
end

似乎将序列化哈希参数放在参数列表的末尾非常重要。否则,Rails将期望下一个参数也是序列化哈希。

在视图中,我使用了一些简单的if/then逻辑来仅在哈希不为空时显示它,并仅显示值不为nil的键/值对。


1
这适用于存储值,但是当您打开“编辑”表单时,您看不到保存的值。 - Yshmarov
1
@YaroShm 一段时间没遇到这个问题了。我记得 strong parameters 稍微有点棘手,并且过去一年中 Rails 的更新可能已经改变了必要的语法。它可能需要一些仔细的调试和幸运的测试才能使其正常工作。当你成功后,请在此处发布解决方案。这将帮助未来遇到此类问题的其他开发人员。祝好运! - JDenman6

4
我曾经遇到过同样的问题,经过一些研究,我发现可以使用Rails的store_accessor解决,使序列化列的键可作为属性访问。
通过这种方法,我们可以访问序列化列的“嵌套”属性...
# post.rb
class Post < ApplicationRecord
  serialize :options
  store_accessor :options, :value1, :value2, :value3
end

# set / get values
post = Post.new
post.value1 = "foo"
post.value1
#=> "foo"
post.options['value1']
#=> "foo"

# strong parameters in posts_controller.rb
params.require(:post).permit(:value1, :value2, :value3)

# form.html.erb
<%= form_with model: @post, local: true do |f| %>
  <%= f.label :value1 %>
  <%= f.text_field :value1 %>
  # …
<% end %>

太棒了,这正是我在寻找的。现在,根据文档,您只需使用 store :options, :value1, :value2 https://api.rubyonrails.org/classes/ActiveRecord/Store.html - Stefan Huska

3

不需要setter/getters,我只是在模型中定义了:

serialize :content_hash, Hash

然后在视图中,我使用simple_form(但与普通的Rails类似):

  = f.simple_fields_for :content_hash do |chf|
    - @model_instance.content_hash.each_pair do |k,v|
      =chf.input k.to_sym, :as => :string, :input_html => {:value => v}

我的最后一个问题是如何让用户添加新的键/值对。


大部分都能正常工作,只是我正在处理一个深层哈希表,但只有根级别最终成为哈希表。 - cpuguy83

2

我建议您使用简单的方法,因为每次用户保存表单时,您都会得到字符串。因此,您可以使用例如before过滤器,并解析这些数据,如下所示:

before_save do
  widget.options = YAML.parse(widget.options).to_ruby
end 

当然,如果这是正确的YAML格式,您应该添加验证。但它应该能够正常工作。

1

我正在尝试做类似的事情,我找到了这个有点可行:

<%= form_for @search do |f| %>
    <%= f.fields_for :params, @search.params do |p| %>
        <%= p.select "property_id", [[ "All", 0 ]] + PropertyType.all.collect { |pt| [ pt.value, pt.id ] } %>

        <%= p.text_field :min_square_footage, :size => 10, :placeholder => "Min" %>
        <%= p.text_field :max_square_footage, :size => 10, :placeholder => "Max" %>
    <% end %>
<% end %>

除了表单在渲染时未填充表单字段外,一切正常。当表单被提交时,值可以很好地传递,我可以执行以下操作:

@search = Search.new(params[:search])

所以它“半个”工作...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接