Salvar vários objetos em uma única chamada nos trilhos


92

Eu tenho um método em trilhos que está fazendo algo assim:

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

O problema é que isso leva mais e mais tempo quanto mais entidades eu adiciono. Suspeito que seja porque ele precisa acessar o banco de dados para cada registro. Como eles estão aninhados, sei que não posso salvar os filhos antes que os pais sejam salvos, mas gostaria de salvar todos os pais de uma vez e, em seguida, todos os filhos. Seria bom fazer algo como:

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)

Isso faria tudo em apenas dois acessos ao banco de dados. Existe uma maneira fácil de fazer isso em trilhos ou estou preso fazendo um de cada vez?

Respostas:


65

Você pode tentar usar Foo.create em vez de Foo.new. Create "Cria um objeto (ou vários objetos) e salva-o no banco de dados, se as validações passarem. O objeto resultante é retornado independentemente de o objeto ter sido salvo com sucesso no banco de dados ou não."

Você pode criar vários objetos como este:

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

Então, para cada pai, você também pode usar criar para adicionar à sua associação:

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

Eu recomendo a leitura da documentação do ActiveRecord e dos Guias do Rails sobre a interface de consulta do ActiveRecord e associações do ActiveRecord . O último contém um guia de todos os métodos que uma classe ganha quando você declara uma associação.


77
Infelizmente, ActiveRecord irá gerar uma consulta INSERT por modelo criado. O OP deseja uma única chamada INSERT, o que ActiveRecord não fará.
François Beausoleil

Sim, eu esperava obter tudo em uma chamada de inserção, mas se o activerecord não for tão inteligente, acho que não é muito fácil.
captncraig

@ FrançoisBeausoleil, você se importaria de olhar a questão stackoverflow.com/questions/15386450/… , seria por isso que não consigo inserir vários registros ao mesmo tempo?
Richlewis

3
É verdade que você não pode fazer o AR gerar um INSERT ou UPDATE, mas com ActiveRecord::Base.transaction { records.each(&:save) }ou similar você pode pelo menos colocar todos os INSERTs ou UPDATEs em uma única transação.
yuval 01 de

1
Na verdade, o OP quer acertar menos o banco de dados, para acelerar o acesso ao banco de dados, e o ActiveRecord realmente permite que você faça isso, agrupando todas as chamadas em uma transação. (Veja a resposta de Harish, que deve ser a resposta aceita.) O que ActiveRecord não permite que você faça é fazer o banco de dados criar uma consulta INSERT por transação, mas isso não importa muito, já que a latência vem de fazer a rede acesso ao BD, e não dentro do próprio BD quando ele faz as consultas INSERT.
Magne

98

Como você precisa realizar várias inserções, o banco de dados será acessado várias vezes. O atraso no seu caso é porque cada salvamento é feito em diferentes transações do banco de dados. Você pode reduzir a latência encerrando todas as suas operações em uma transação.

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

Seu método de salvamento pode ser parecido com este:

# 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

A savechamada no objeto pai salva os objetos filho.


3
É mesmo o que eu procurava. Acelera muito minhas sementes. Obrigado :-)
Renra 01/07/2014

12

insert_all (Rails 6+)

Rails 6introduziu um novo método insert_all , que insere vários registros no banco de dados em uma única SQL INSERTinstrução.

Além disso, esse método não instancia nenhum modelo e não chama retornos de chamada ou validações do Active Record.

Assim,

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

é significativamente mais eficiente do que

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

se tudo o que você deseja fazer é inserir novos registros.


1
Eu não posso esperar até que atualizemos nosso aplicativo. Tantas coisas legais no Rails 6.
Dan

uma coisa a ser observada é: insert_all ignora os callbacks e validações AR: edgeguides.rubyonrails.org/…
sujay

10

Uma das duas respostas encontradas em outro lugar: por Beerlington . Esses dois são a sua melhor aposta para o desempenho


Acho que sua melhor aposta em termos de desempenho será usar SQL e inserir várias linhas em massa por consulta. Se você puder construir uma instrução INSERT que faça algo como:

INSERT INTO foos_bars (foo_id, bar_id) VALORES (1,1), (1,2), (1,3) .... Você deve ser capaz de inserir milhares de linhas em uma única consulta. Não tentei seu método mass_habtm, mas parece que você poderia fazer algo como:


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}")

Além disso, se você estiver pesquisando Bar por "algum_atributo", certifique-se de ter esse campo indexado em seu banco de dados.


OU

Você ainda pode dar uma olhada em activerecord-import. É certo que não funciona sem um modelo, mas você pode criar um modelo apenas para a importação.


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

Felicidades


Isso funciona muito bem para inserir, mas que tal atualizar vários registros em uma transação?
Avishai

2
Para atualizar, você deve usar upsert: github.com/seamusabshere/upsert . saúde
Nguyen Chien Cong

Idéia muito ruim com consulta sql. Você deve usar ActiveRecord e transação.
Kerozu de

Não é uma má ideia. Se você estiver fazendo UMA inserção, ela terá sucesso ou falhará, sem necessidade de transação, eu acho. Ou você pode sempre embrulhar UMA inserção em um bloco de transação.
Fernando Fabreti

esta é uma má prática de trilhos
Blair Anderson

1

você precisa usar esta gema "FastInserter" -> https://github.com/joinhandshake/fast_inserter

e inserir um grande número e milhares de registros é rápido porque esta gema pula o registro ativo e usa apenas uma única consulta bruta sql


1
Embora o link para a gema possa ser útil, forneça algum código que o Asker possa usar em vez do código atual (consulte a pergunta).
trincot


1
As respostas precisam ter as informações essenciais incorporadas . Edite sua resposta e adicione o link lá, e também adicione as partes essenciais dela dentro da resposta, de modo que seja independente.
trincot

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.