【问题标题】:Slicing params hash for specific values为特定值切片参数哈希
【发布时间】:2011-04-13 17:10:39
【问题描述】:

总结

给定一个哈希,根据要使用的键列表创建子集哈希的最有效方法是什么?

h1 = { a:1, b:2, c:3 }        # Given a hash...
p foo( h1, :a, :c, :d )       # ...create a method that...
#=> { :a=>1, :c=>3, :d=>nil } # ...returns specified keys...
#=> { :a=>1, :c=>3 }          # ...or perhaps only keys that exist

详情

Sequel 数据库工具包允许人们通过传入哈希来创建或更新模型实例:

foo = Product.create( hash_of_column_values )
foo.update( another_hash )

Sinatra Web 框架提供了一个名为 params 的哈希,其中包括表单变量、查询字符串参数以及路由匹配。

如果我创建一个只包含与数据库列名称相同的字段并将其发布到此路由的表单,一切都会非常方便:

post "/create_product" do
  new_product = Product.create params
  redirect "/product/#{new_product.id}"
end

但是,这既脆弱又危险。这很危险,因为恶意黑客可能会发布一个包含不打算更改的列的表单并让它们更新。它很脆弱,因为在这条路线上使用相同的表格是行不通的:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    product.update(params)
    #=> <Sequel::Error: method foo= doesn't exist or access is restricted to it>
  end
end

因此,为了稳健性和安全性,我希望能够编写以下代码:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    # Only update two specific fields
    product.update(params.slice(:name,:description))
    # The above assumes a Hash (or Sinatra params) monkeypatch
    # I will also accept standalone helper methods that perform the same
  end
end

...而不是更冗长和非 DRY 选项:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    # Only update two specific fields
    product.update({
      name:params[:name],
      description:params[:description]
    })
  end
end

更新:基准

以下是对(当前)实现进行基准测试的结果:

                    user     system      total        real
sawa2           0.250000   0.000000   0.250000 (  0.269027)
phrogz2         0.280000   0.000000   0.280000 (  0.275027)
sawa1           0.297000   0.000000   0.297000 (  0.293029)
phrogz3         0.296000   0.000000   0.296000 (  0.307031)
phrogz1         0.328000   0.000000   0.328000 (  0.319032)
activesupport   0.639000   0.000000   0.639000 (  0.657066)
mladen          1.716000   0.000000   1.716000 (  1.725172)

@sawa 的第二个答案是最快的,在我基于tap 的实现之前有一根头发(基于他的第一个答案)。选择为has_key? 添加检查只需要很少的时间,而且速度仍然是 ActiveSupport 的两倍多。

这是基准代码:

h1 = Hash[ ('a'..'z').zip(1..26) ]
keys = %w[a z c d g A x]
n = 60000

require 'benchmark'
Benchmark.bmbm do |x|
  %w[ sawa2 phrogz2 sawa1 phrogz3 phrogz1 activesupport mladen ].each do |m|
    x.report(m){ n.times{ h1.send(m,*keys) } }
  end
end

【问题讨论】:

  • 您在顶部的示例似乎与细节不一致?在示例中,您展示了如果选择原始哈希中不存在的键,您应该在新哈希中获得 nil 值。在您的 Sequel 示例中,您似乎不需要创建新的哈希,而实际上只是一个子集。真正的需求是什么?
  • @Wes 我认为这两者并非不兼容。实际情况是,我永远不会(故意)要求原始版本中不存在的密钥。我在摘要中包含了:d,以明确指定应如何处理边缘情况。但是,我也可以接受不包含任何丢失但请求的密钥的解决方案。 (事实上​​,Mladen 的回答和 ActiveSupport 都没有包含任何原件中没有的键。)
  • 哇,很高兴听到结果。这里有一个可能的教训;一个天真的实现比过多地使用 Rubyish 方式并充分使用它的功能要快吗?希望 ruby​​ 实现更快。
  • 前 5 个基准之间的差异没有统计学意义。也就是说,它们的速度基本相同。
  • 仅供 9 年后阅读本文的任何人参考,Ruby 现在已经内置了这个 ^^ ruby-doc.org/core-2.5.0/Hash.html#method-i-slice

标签: ruby hash sinatra sequel


【解决方案1】:

我只会使用active_support提供的切片方法

require 'active_support/core_ext/hash/slice'
{a: 1, b: 2, c: 3}.slice(:a, :c)                  # => {a: 1, c: 3}

当然,请确保更新您的 gemfile:

gem 'active_support'

【讨论】:

  • +1 我不知道您可以从 ActiveSupport 中挑选个别方法。有关对此方法进行基准测试的结果,请参阅上面更新的问题。
【解决方案2】:

我改变了主意。上一个好像不太好。

class Hash
  def slice1(*keys)
    keys.each_with_object({}){|k, h| h[k] = self[k]}
  end
  def slice2(*keys)
    h = {}
    keys.each{|k| h[k] = self[k]}
    h
  end
end

【讨论】:

    【解决方案3】:

    Sequel 内置支持仅在更新时选择特定列:

    product.update_fields(params, [:name, :description])
    

    但是,如果 :name 或 :description 不存在于 params 中,则不会做完全相同的事情。但假设您希望用户使用您的表单,这应该不是问题。

    我总是可以扩展 update_fields 以获取带有选项的选项散列,如果散列中不存在该值,则该选项将跳过该值。我只是还没有收到这样做的请求。

    【讨论】:

    • 我不知道。非常好。这仍然不能满足Product.create() 的需求,对吗?
    • 啊,好点子。请注意,我昨晚刚刚遇到了一个案例,我正在处理复选框,并且我 确实 在请求哈希中不存在的字段时明确希望包含 nil 值。我绝对不会请求跳过非当前值的功能。 :)
    • FWIW,我只需要 Sequel 切片的另一种情况:model.add_associateditem( existing_item.slice( hash_of_fields_without_id ) )
    【解决方案4】:

    也许

    class Hash
      def slice *keys
        select{|k| keys.member?(k)}
      end
    end
    

    或者你可以直接复制 ActiveSupport 的Hash#slice,它看起来更健壮一些。

    【讨论】:

      【解决方案5】:

      这是我的实现;我将基准测试并接受更快(或更优雅)的解决方案:

      # Implementation 1
      class Hash
        def slice(*keys)
          Hash[keys.zip(values_at *keys)]
        end
      end
      
      # Implementation 2
      class Hash
        def slice(*keys)
          {}.tap{ |h| keys.each{ |k| h[k]=self[k] } }
        end
      end
      
      # Implementation 3 - silently ignore keys not in the original
      class Hash
        def slice(*keys)
          {}.tap{ |h| keys.each{ |k| h[k]=self[k] if has_key?(k) } }
        end
      end
      

      【讨论】:

      • 为什么不使用来自 ActiveSupport 的 Hash#only?
      • @ReinHeinrichs 因为我使用的是 Sinatra,而不是 Rails,而且我的应用程序中没有包含 ActiveSupport 的臃肿。另外,因为我不知道。 :) 谢谢,我会调查的。
      • 不包括散列中不存在的键会导致将某些表列设置为 NULL 吗?我相信你必须在你的应用中检查has_key?
      • @Mladen 是的,它会的。我很纠结这是否可取。例如,未选中的 HTML 复选框将不会发送键值对。在这种情况下,我可能会要求在哈希中合理地不存在的列,并希望得到nil。如您所见,我在上面使用has_key? 的版本编辑了我的答案,以备不时之需。
      • 数据库表可以为其列提供默认值(不一定是 NULL),所以如果参数不存在就很好。
      猜你喜欢
      • 2013-08-10
      • 2010-11-10
      • 2010-12-19
      • 1970-01-01
      • 1970-01-01
      • 2019-08-07
      • 2018-11-03
      • 2020-12-26
      • 2019-02-12
      相关资源
      最近更新 更多