Как я могу запускать обновления в пакетах в Rails 3/4?

Мне нужно массовое обновление многих тысяч записей, и я хотел бы обрабатывать обновления в пакетах. Во-первых, я попробовал:

Foo.where(bar: 'bar').find_in_batches.update_all(bar: 'baz')

... который я надеялся создать SQL, например:

"UPDATE foo SET bar = 'baz' where bar='bar' AND id > (whatever id is passed in by find_in_batches)"

Это не работает, потому что find_in_batches возвращает массив, а update_all - отношение ActiveRecord.

Вот что я пробовал дальше:

Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
  ids = foos.map(&:id)
  Foo.where(id: ids).update_all(bar: 'baz')
end

Это работает, но, очевидно, выполняется выбор, за которым следует обновление, а не одно обновление, основанное на моих условиях "where". Есть ли способ очистить это, так что выбор и обновление не должны быть отдельными запросами?

Ответы

Ответ 1

В Rails 5 для решения этой проблемы существует новый удобный метод ActiveRecord::Relation#in_batches:

Foo.in_batches.update_all(bar: 'baz')

Подробнее о документации.

Ответ 2

Я тоже удивлен, что нет более простого способа сделать это... но я придумал такой подход:

batch_size = 1000
0.step(Foo.count, batch_size).each do |offset|
  Foo.where(bar: 'bar').order(:id)
                       .offset(offset)
                       .limit(batch_size)
                       .update_all(bar: 'baz')
end

В основном это будет:

  • Создайте массив смещений между 0 и Foo.count, шаг за шагом batch_size каждый раз. Например, если Foo.count == 10500 вы получите: [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
  • Пронумеруйте эти числа и используйте их как СМЕЩЕНИЕ в SQL-запросе, обязательно заказывайте id и ограничивая batch_size.
  • Обновить не более batch_size записи, чей "индекс" больше, чем offset.

Это в основном ручной способ выполнить то, что вы сказали, на что надеетесь в сгенерированном SQL. Слишком плохо, что это невозможно сделать уже по стандартным библиотечным методам... хотя я уверен, что вы могли бы создать свой собственный.

Ответ 3

Ответ pdobb на правильном пути, но не работал у меня в Rails 3.2.21 из-за этой проблемы ActiveRecord, не разобрав OFFSET с вызовами UPDATE:

https://github.com/rails/rails/issues/10849

Я изменил код соответствующим образом, и он отлично работал для одновременного задания значения по умолчанию в моей таблице Postgres:

batch_size = 1000
0.step(Foo.count, batch_size).each do |offset|
  Foo.where('id > ? AND id <= ?', offset, offset + batch_size).
      order(:id).
      update_all(foo: 'bar')
end

Ответ 4

Это на 2 года позже, но ответы здесь: a) очень медленны для больших наборов данных и b) игнорировать встроенные возможности рельсов (http://api.rubyonrails.org/classes/ActiveRecord/Batches.html).

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

используйте метод итератора "find_each":

Foo.where(a: b).find_each do |bar|
   bar.x = y
   bar.save
end

Это имеет дополнительное преимущество при использовании обратных вызовов модели при каждом сохранении. Если вам не нужны обратные вызовы, попробуйте:

Foo.where(a: b).find_in_batches do |array_of_foo|
  ids = array_of_foo.collect &:id
  Foo.where(id: ids).update_all(x: y)
end

Ответ 6

У вас не было возможности проверить это, но вы могли бы использовать AREL и дополнительный запрос.

Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
  Foo.where( Foo.arel_table[ :id ].in( foos.to_arel ) ).update_all(bar: 'baz')
end