Slicing params hash for specific values
Summary
Given a Hash, what is the most efficient way to create a subset Hash based on a list of keys to use?
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
Details
The Sequel database toolkit allows one to create or update a model instance by passing in a Hash:
foo = Product.create( hash_of_column_values )
foo.update( another_hash )
The Sinatra web framework makes available a Hash named params
that includes form variables, querystring parameters and also route matches.
If I create a form holding only fields named the same as the database columns and post it to this route, everything works very conveniently:
post "/create_product" do
new_product = Product.create params
redirect "/product/#{new_product.id}"
end
However, this is both fragile and dangerous. It's dangerous because a malicious hacker could post a form with columns not intended to be changed and have them updated. It's fragile because using the same form with this route will not work:
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
So, for robustness and security I want to be able to write this:
post "/update_product/:foo" do |prod_id|
if product = Product[prod_id]
# Only update two specific fields
product.update(param开发者_运维技巧s.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
...instead of the more verbose and non-DRY option:
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
Update: Benchmarks
Here are the results of benchmarking the (current) implementations:
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)
The second answer by @sawa is the fastest of all, a hair in front of my tap
-based implementation (based on his first answer). Choosing to add the check for has_key?
adds very little time, and is still more than twice as fast as ActiveSupport.
Here is the benchmark code:
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
I would just use the slice method provided by active_support
require 'active_support/core_ext/hash/slice'
{a: 1, b: 2, c: 3}.slice(:a, :c) # => {a: 1, c: 3}
Of course, make sure to update your gemfile:
gem 'active_support'
I changed by mind. The previous one doesn't seem to be any good.
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
Sequel has built-in support for only picking specific columns when updating:
product.update_fields(params, [:name, :description])
That doesn't do exactly the same thing if :name or :description is not present in params, though. But assuming you are expecting the user to use your form, that shouldn't be an issue.
I could always expand update_fields to take an option hash with an option that will skip the value if not present in the hash. I just haven't received a request to do that yet.
Perhaps
class Hash
def slice *keys
select{|k| keys.member?(k)}
end
end
Or you could just copy ActiveSupport's Hash#slice
, it looks a bit more robust.
Here are my implementations; I will benchmark and accept faster (or sufficiently more elegant) solutions:
# 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
精彩评论