Сохранение нескольких объектов за один вызов в рельсах

93

У меня есть метод в рельсах, который делает что-то вроде этого:

a = Foo.new("bar")
a.save

b = Foo.new("baz")
b.save

...
x = Foo.new("123", :parent_id => a.id)
x.save

...
z = Foo.new("zxy", :parent_id => b.id)
z.save

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

a = Foo.new("bar")
b = Foo.new("baz")
...
saveall(a,b,...)

x = Foo.new("123", :parent_id => a.id)
...
z = Foo.new("zxy", :parent_id => b.id)
saveall(x,...,z)

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

Captncraig
источник

Ответы:

69

Вы можете попробовать использовать Foo.create вместо Foo.new. Create «Создает объект (или несколько объектов) и сохраняет его в базе данных, если проверки пройдены. Полученный объект возвращается независимо от того, был ли объект успешно сохранен в базе данных или нет».

Вы можете создать несколько таких объектов:

# Create an Array of new objects
  parents = Foo.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])

Затем для каждого родителя вы также можете использовать create, чтобы добавить к его ассоциации:

parents.each do |parent|
  parent.children.create (:child_name => 'abc')
end

Я рекомендую прочитать как документацию ActiveRecord и Rails Guides на ActiveRecord интерфейс запросов и ActiveRecord ассоциаций . Последний содержит руководство по всем методам, которые получает класс при объявлении ассоциации.

Roadmaster
источник
78
К сожалению, ActiveRecord будет генерировать один запрос INSERT для каждой созданной модели. OP хочет один вызов INSERT, чего ActiveRecord не сделает.
François Beausoleil,
Да, я надеялся получить все это за один вызов вставки, но если activerecord не так умен, думаю, это не очень просто.
captncraig
@ FrançoisBeausoleil, не могли бы вы взглянуть на вопрос stackoverflow.com/questions/15386450/… , поэтому я не могу вставить несколько записей одновременно?
Richlewis
3
Это правда, что вы не можете заставить AR генерировать один INSERT или UPDATE, но с помощью ActiveRecord::Base.transaction { records.each(&:save) }или подобным вы можете, по крайней мере, поместить все INSERT или UPDATE в одну транзакцию.
юваль 01
1
На самом деле OP хочет меньше попадать в базу данных, чтобы ускорить доступ к БД, а ActiveRecord позволяет вам это сделать, объединяя все вызовы в одну транзакцию. (См. Ответ Хариша, который должен быть принятым ответом.) ActiveRecord не позволит вам сделать так, чтобы БД создавала один запрос INSERT для каждой транзакции, но это не имеет большого значения, поскольку задержка возникает из-за работы в сети. доступ к БД, а не внутри самой БД, когда он выполняет запросы INSERT.
Magne
99

Поскольку вам нужно выполнить несколько вставок, база данных будет поражена несколько раз. Задержка в вашем случае связана с тем, что каждое сохранение выполняется в разных транзакциях БД. Вы можете уменьшить задержку, заключив все свои операции в одну транзакцию.

class Foo
  belongs_to  :parent,   :class_name => "Foo"
  has_many    :children, :class_name => "Foo", :foreign_key=> "parent_id"
end

Ваш метод сохранения может выглядеть так:

# build the parent and the children
a = Foo.new(:name => "bar")
a.children.build(:name => "123")

b = Foo.new("baz")
b.children.build(:name => "zxy")

#save parents and their children in one transaction
Foo.transaction do
  a.save!
  b.save!
end

saveВызов на родительский объект сохраняет дочерние объекты.

Хариш Шетти
источник
3
Именно то, что я искал. Ускоряет мои семена. Спасибо :-)
Renra 01
16

insert_all (Rails 6+)

Rails 6представил новый метод insert_all , который вставляет несколько записей в базу данных одним SQL INSERTоператором.

Кроме того, этот метод не создает экземпляров каких-либо моделей и не вызывает обратные вызовы или проверки Active Record.

Так,

Foo.insert_all([
  { first_name: 'Jamie' },
  { first_name: 'Jeremy' }
])

это значительно эффективнее, чем

Foo.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])

если все, что вы хотите сделать, это вставить новые записи.

Мариан13
источник
1
Не могу дождаться, пока мы обновим наше приложение. Так много крутых вещей в Rails 6.
Дэн
1
одно замечание: insert_all пропускает обратные вызовы и проверки AR: edgeguides.rubyonrails.org/…
суджай
11

Один из двух ответов, найденных где-то еще: Бирлингтоном . Эти двое - лучший выбор для производительности


Я думаю, что ваш лучший выбор с точки зрения производительности - использовать SQL и массовую вставку нескольких строк на запрос. Если вы можете создать оператор INSERT, который делает что-то вроде:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Вы должны иметь возможность вставлять тысячи строк в один запрос. Я не пробовал ваш метод mass_habtm, но похоже, что вы можете что-то вроде:


bars = Bar.find_all_by_some_attribute(:a) 
foo = Foo.create
values = bars.map {|bar| "(#{foo.id},#{bar.id})"}.join(",") 
connection.execute("INSERT INTO foos_bars (foo_id, bar_id) VALUES
#{values}")

Кроме того, если вы ищете Bar по «some_attribute», убедитесь, что это поле проиндексировано в вашей базе данных.


ИЛИ

Вы все еще можете взглянуть на activerecord-import. Это правильно, что без модели это не работает, но вы можете создать модель только для импорта.


FooBar.import [:foo_id, :bar_id], [[1,2], [1,3]]

Ура

Нгуен Чиен Конг
источник
Это отлично подходит для вставки, но как насчет обновления нескольких записей в одной транзакции?
Avishai
2
Для обновления следует использовать upsert: github.com/seamusabshere/upsert . ура
Нгуен Чиен Конг
Очень плохая идея с sql-запросом. Вы должны использовать ActiveRecord и транзакцию.
Kerozu
Это неплохая идея. Если вы делаете ОДНУ вставку, она либо удастся, либо не удастся, я думаю, нет необходимости в транзакции. Или вы всегда можете обернуть эту ОДНУ вставку в блок транзакции.
Фернандо Фабрети
это плохая практика на рельсах
Блэр Андерсон
0

вам нужно использовать этот гем "FastInserter" -> https://github.com/joinhandshake/fast_inserter

и вставка большого количества и тысяч записей выполняется быстро, потому что этот гем пропускает активную запись и использует только один необработанный запрос sql

Валь Каро
источник
1
Хотя ссылка на гем может быть полезна, предоставьте код, который спрашивающий может использовать вместо своего текущего кода (см. Вопрос).
trincot
1
Ответы должны иметь необходимую информацию , внедренную . Отредактируйте свой ответ и добавьте туда ссылку, а также добавьте его основные части в ответ, чтобы он был самодостаточным.
trincot
-2

Вам не нужен драгоценный камень, чтобы быстро и только один раз попасть в БД!

Джекрг разработал это для нас: https://gist.github.com/jackrg/76ade1724bd816292e4e

Фернандо Фабрети
источник
Любое подобное решение для Mongodb?
Breno