Enregistrement de plusieurs objets en un seul appel dans les rails

92

J'ai une méthode dans les rails qui fait quelque chose comme ceci:

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

Le problème est que cela prend de plus en plus d'entités. Je soupçonne que c'est parce qu'il doit frapper la base de données pour chaque enregistrement. Puisqu'ils sont imbriqués, je sais que je ne peux pas sauver les enfants avant que les parents ne soient sauvés, mais j'aimerais sauver tous les parents à la fois, puis tous les enfants. Ce serait bien de faire quelque chose comme:

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)

Cela ferait tout cela en seulement deux hits de base de données. Existe-t-il un moyen simple de le faire dans les rails, ou suis-je obligé de le faire un à la fois?

captncraig
la source

Réponses:

65

Vous pouvez essayer d'utiliser Foo.create au lieu de Foo.new. Créer "Crée un objet (ou plusieurs objets) et l'enregistre dans la base de données, si les validations réussissent. L'objet résultant est renvoyé, que l'objet ait été enregistré avec succès dans la base de données ou non."

Vous pouvez créer plusieurs objets comme ceci:

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

Ensuite, pour chaque parent, vous pouvez également utiliser create pour ajouter à son association:

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

Je recommande de lire à la fois la documentation ActiveRecord et les guides Rails sur l' interface de requête ActiveRecord et les associations ActiveRecord . Ce dernier contient un guide de toutes les méthodes qu'une classe gagne lorsque vous déclarez une association.

Roadmaster
la source
77
Malheureusement, ActiveRecord générera une requête INSERT par modèle créé. L'OP veut un seul appel INSERT, ce qu'ActiveRecord ne fera pas.
François Beausoleil
Oui, j'espérais tout obtenir en un seul appel d'insertion, mais si activerecord n'est pas si intelligent, je suppose que ce n'est pas très facile.
captncraig
@ FrançoisBeausoleil est-ce que cela vous dérangerait de regarder la question stackoverflow.com/questions/15386450/… , serait-ce la raison pour laquelle je ne peux pas insérer plusieurs enregistrements en même temps?
Richlewis
3
Il est vrai que vous ne pouvez pas obtenir AR pour générer un INSERT ou UPDATE, mais avec ActiveRecord::Base.transaction { records.each(&:save) }ou similaire, vous pouvez au moins mettre tous les INSERT ou UPDATE dans une seule transaction.
yuval le
1
En fait, l'OP veut moins frapper la base de données, pour accélérer l'accès à la base de données, et ActiveRecord vous permet de le faire, en regroupant tous les appels en une seule transaction. (Voir la réponse de Harish, qui devrait être la réponse acceptée.) Ce qu'ActiveRecord ne vous laissera pas faire est de faire en sorte que la base de données crée une requête INSERT par transaction, mais cela n'a pas beaucoup d'importance, car la latence provient du fait de faire le réseau accès à la base de données, et non à l'intérieur de la base de données elle-même lorsqu'il effectue les requêtes INSERT.
Magne
98

Puisque vous devez effectuer plusieurs insertions, la base de données sera frappée plusieurs fois. Le retard dans votre cas est dû au fait que chaque sauvegarde est effectuée dans différentes transactions DB. Vous pouvez réduire la latence en regroupant toutes vos opérations dans une seule transaction.

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

Votre méthode de sauvegarde pourrait ressembler à ceci:

# 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

L' saveappel sur l'objet parent enregistre les objets enfants.

Harish Shetty
la source
3
Exactement ce que je cherchais. Accélère beaucoup mes graines. Merci :-)
Renra
13

insert_all (Rails 6+)

Rails 6a introduit une nouvelle méthode insert_all , qui insère plusieurs enregistrements dans la base de données en une seule SQL INSERTinstruction.

En outre, cette méthode n'instancie aucun modèle et n'appelle pas les rappels ou validations Active Record.

Alors,

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

il est nettement plus efficace que

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

si tout ce que vous voulez faire est d'insérer de nouveaux enregistrements.

Marian13
la source
1
J'ai hâte que nous mettions à jour notre application. Tant de choses sympas dans Rails 6.
Dan
une chose à noter est: insert_all ignore les callbacks et validations AR: edgeguides.rubyonrails.org
sujay
10

L'une des deux réponses trouvées ailleurs: par Beerlington . Ces deux sont votre meilleur pari pour la performance


Je pense que votre meilleur pari en termes de performances sera d'utiliser SQL et d'insérer en masse plusieurs lignes par requête. Si vous pouvez créer une instruction INSERT qui fait quelque chose comme:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Vous devriez pouvoir insérer des milliers de lignes dans une seule requête. Je n'ai pas essayé votre méthode mass_habtm, mais il semble que vous pourriez faire quelque chose comme:


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

De plus, si vous recherchez Bar par "some_attribute", assurez-vous que ce champ est indexé dans votre base de données.


OU

Vous pouvez toujours jeter un œil à activerecord-import. Il est vrai que cela ne fonctionne pas sans modèle, mais vous pouvez créer un modèle uniquement pour l'importation.


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

À votre santé

Nguyen Chien Cong
la source
Cela fonctionne très bien pour l'insertion, mais qu'en est-il de la mise à jour de plusieurs enregistrements en une seule transaction?
Avishai
2
Pour la mise à jour, vous devez utiliser upsert: github.com/seamusabshere/upsert . cheers
Nguyen Chien Cong
Très mauvaise idée avec requête SQL. Vous devez utiliser ActiveRecord et transaction.
Kerozu
Ce n'est pas une mauvaise idée. Si vous faites UNE insertion, cela réussira ou échouera, pas besoin de transaction, je suppose. Ou vous pouvez toujours envelopper cet insert dans un bloc de transaction.
Fernando Fabreti
c'est une mauvaise pratique des rails
Blair Anderson
1

vous devez utiliser ce joyau "FastInserter" -> https://github.com/joinhandshake/fast_inserter

et l'insertion d'un grand nombre et de milliers d'enregistrements est rapide car ce joyau ignore l'enregistrement actif et n'utilise qu'une seule requête brute SQL

val caro
la source
1
Bien que le lien vers la gemme puisse être utile, veuillez fournir du code que le demandeur pourrait utiliser à la place de son code actuel (voir la question).
trincot le
1
Les réponses doivent intégrer les informations essentielles . Veuillez modifier votre réponse et ajouter le lien là-bas, ainsi que les parties essentielles de celle-ci dans la réponse, afin qu'elle soit autonome.
trincot le