Нарезка хэша params для определенных значений - PullRequest
14 голосов
/ 13 апреля 2011

Резюме

Учитывая хэш, каков наиболее эффективный способ создания подмножества хэша на основе списка используемых ключей?

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 предоставляет хэш с именем 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

... вместо более многословного и неСУХОГО варианта:

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

Ответы [ 5 ]

19 голосов
/ 13 апреля 2011

Я бы просто использовал метод слайса, предоставленный 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'
4 голосов
/ 13 апреля 2011

Я передумал. Предыдущий, похоже, не очень хорош.

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 голосов
/ 14 апреля 2011

Sequel имеет встроенную поддержку для выбора только определенных столбцов при обновлении:

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

Это не делает то же самое, если: name или: description не присутствует в params. Но если вы ожидаете, что пользователь будет использовать вашу форму, это не должно быть проблемой.

Я всегда мог бы расширить поля update_fields, чтобы получить хеш опций с опцией, которая пропустит значение, если оно не присутствует в хэше. Я просто еще не получил запрос сделать это.

2 голосов
/ 13 апреля 2011

Возможно

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

Или вы можете просто скопировать Hash#slice ActiveSupport, он выглядит немного более надежным.

0 голосов
/ 13 апреля 2011

Вот мои реализации; Я буду оценивать и принимать более быстрые (или достаточно элегантные) решения:

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