对特定值的参数哈希进行切片

发布于 2024-11-01 02:28:55 字数 2835 浏览 1 评论 0原文

总结

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

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

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(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

...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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

九歌凝 2024-11-08 02:28:55

我只会使用 active_support 提供的 slice 方法

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

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

gem 'active_support'

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'
身边 2024-11-08 02:28:55

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

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

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
独孤求败 2024-11-08 02:28:55

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

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

但是,如果 :name 或 :description 不存在于参数中,则不会执行完全相同的操作。但假设您希望用户使用您的表单,那么这应该不是问题。

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

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.

乖乖公主 2024-11-08 02:28:55

也许

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

或者您可以复制 ActiveSupport 的 Hash#slice,看起来更健壮一点。

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.

唱一曲作罢 2024-11-08 02:28:55

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

# 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

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文