切片参数哈希特定值

摘要

给定Hash,基于要使用的键列表创建子集Hash的最有效方法是什么?

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数据库工具包允许通过传入Hash来创建或更新模型实例:

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

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

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

 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) #=>  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?的检查has_key? 增加的时间非常少,并且仍然是ActiveSupport的两倍多。

这是基准代码:

 h1 = Hash[ ('a'..'z').zip(1..26) ] keys = %w[azcdg 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 

我改变了想法。 前一个似乎没有任何好处。

 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 

我只想使用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' 

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

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

如果:name或:描述在params中不存在,那么这并不完全相同。 但假设您希望用户使用您的表单,那应该不是问题。

我总是可以扩展update_fields以获取一个选项哈希,该选项将跳过该值(如果哈希中不存在)。 我还没有收到要求这样做的请求。

也许

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

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

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

 # 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