Ответ 1
В Rails 5 для решения этой проблемы существует новый удобный метод ActiveRecord::Relation#in_batches
:
Foo.in_batches.update_all(bar: 'baz')
Подробнее о документации.
Мне нужно массовое обновление многих тысяч записей, и я хотел бы обрабатывать обновления в пакетах. Во-первых, я попробовал:
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". Есть ли способ очистить это, так что выбор и обновление не должны быть отдельными запросами?
В Rails 5 для решения этой проблемы существует новый удобный метод ActiveRecord::Relation#in_batches
:
Foo.in_batches.update_all(bar: 'baz')
Подробнее о документации.
Я тоже удивлен, что нет более простого способа сделать это... но я придумал такой подход:
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]
id
и ограничивая batch_size
.batch_size
записи, чей "индекс" больше, чем offset
.Это в основном ручной способ выполнить то, что вы сказали, на что надеетесь в сгенерированном SQL. Слишком плохо, что это невозможно сделать уже по стандартным библиотечным методам... хотя я уверен, что вы могли бы создать свой собственный.
Ответ 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
Это на 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
Я написал небольшой метод для вызова update_all пакетами:
https://gist.github.com/VarunNatraaj/420c638d544be59eef85
Надеюсь, это полезно!:)
У вас не было возможности проверить это, но вы могли бы использовать 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