23

I have an array like this

a = []

a << B.new(:name => "c")
a << B.new(:name => "s")
a << B.new(:name => "e")
a << B.new(:name => "t")

How i can save it at once?

Luca Romagnoli
  • 12,145
  • 30
  • 95
  • 157

6 Answers6

54
B.transaction do
  a.each(&:save!)
end

This will create a transaction that loops through each element of the array and calls element.save on it.

You can read about ActiveRecord Transactions and the each method in the Rails and Ruby APIs.

Benjamin Manns
  • 9,028
  • 4
  • 37
  • 48
  • 1
    Attention! This will not work unless you use `save!`. It seems that transaction is only aborted if there is an exception. – Alexey May 14 '13 at 17:58
  • Now you need to catch `ActiveRecord::RecordInvalid` somewhere. – Alexey May 16 '13 at 18:03
29
a.each(&:save)

This will call B#save on each item in the array.

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • 11
    And wrap that in a B.transaction to get it all saved in one atomic operation. – Farrel Feb 18 '10 at 22:53
  • 6
    Never just call `save`, either check the return value (true oder false) or use `.save!` to let rails raise an exception when something is not okay! – reto May 16 '13 at 14:15
14

So I think we need a middle ground to Alexey's raising exceptions and aborting the transaction and Jordan's one-liner solution. May I propose:

B.transaction do
  success = a.map(&:save)
  unless success.all?
    errored = a.select{|b| !b.errors.blank?}
    # do something with the errored values
    raise ActiveRecord::Rollback
  end
end

This will give you a bit of both worlds: a transaction with rollback, knowledge of which records failed and even gives you access to the validation errors therein.

kayakyakr
  • 403
  • 5
  • 5
  • 2
    Instead of `!b.errors.blank?`, why not `b.errors.present?` – moveson Nov 29 '16 at 03:43
  • 1
    could also be `a.reject{|b| b.errors.blank?}`. Mostly because it was almost 3 years ago and it was an off-the-cuff answer. Also because Ruby, am I right? – kayakyakr Jan 20 '17 at 05:08
2

Wrapping save in transaction will not be enough: if a validation is not passed, there will be no exception raised and no rollback triggered.

I can suggest this:

B.transaction do
  a.each do |o|
    raise ActiveRecord::Rollback unless o.save
  end
end

Just doing B.transaction do a.each(&:save!) end is not an option either, because the transaction block will not rescue any exception other than ActiveRecord::Rollback, and the application would crash on failed validation.

I do not know how to check afterwards if the records have been saved.


Update. As someone has downrated my answer, i assume that the person was looking for a cut-and-paste solution :), so here is some (ugly :)) way to process fail/success value:

save_failed = nil
B.transaction do
  a.each do |o|
    unless o.save
      save_failed = true
      raise ActiveRecord::Rollback
    end
  end
end
if save_failed
  # ...
else
  # ...
end
Alexey
  • 3,843
  • 6
  • 30
  • 44
  • `save!` is perfect, the `transaction do` block will automatically revert all other changes. – reto May 16 '13 at 14:16
  • You would need to manually catch exceptions outside the transaction if a validation fails. – Alexey May 16 '13 at 18:02
  • All I'm saying is: you kinda have two choices a) handle the validation right after the `save` using the return value b) let the process/job crash using `save!`, there is (in almost all cases) no middle ground. A lone `save` (without handling its return value) is a big warning sign. The transaction just ensures that everything else gets restored too. – reto May 17 '13 at 08:42
  • To put it differently: Your example is almost ALWAYS never what you want. If ANY of your objects in `a` cannot be saved the whole bunch will not be saved. After the transaction block you don't know: a) if the objects have been saved, b) if not, which one caused the rollback c) what the problem was. I can't imagine a situation where I would want this behavior. – reto May 17 '13 at 08:45
  • Reverting all saves if one fails is the purpose of using a transaction. How to process fail/success is a good question to which i do not see a good answer, because `transaction` method does not seem to return this as a value. It looks to me like an `ActiveRecord` flaw. – Alexey May 17 '13 at 12:30
  • Benjamin Manns solution is completely correct, either you catch the 'error' at its source or you dont't catch it at all. – reto May 17 '13 at 15:26
  • He does not catch it in his code example, so it is not clear how it is intended to be caught. My impression was that the OP question is about `save`, not `save!`. This is a different attitude: failed validations are not treated as errors, but as a normal result of interaction with a user. They should not generate exceptions that need to be "caught", but simply raise a flag. I thought the question was about saving multiple records at once the same way as "nested attributes" are saved: all or nothing, no exceptions to catch. – Alexey May 17 '13 at 15:51
  • Alexey, in Benjamins example the code block will just fail/abort and everything will be restored to the old state. It will print out a verbose error message containing the problem. This is a perfectly fine solution. Somebody can afterwards check the log/email/job status to see what went wrong, fix the bug, do some pretests, whatever necessary. With your example it will be in an unknown state, without ANY way to determine what could have and should have gone wrong. Exceptions are rarely caught, in our projects exceptions are mostly used to stop the program in an unexpected situations. – reto May 17 '13 at 16:59
  • reto, this is far from fine solution when you, for example, simply want the user to fix a typo in one of the email addresses or whatever and resubmit the form. – Alexey May 17 '13 at 17:19
  • What do you mean by an "unknown state"? Either everything will be saved and i will know it by looking at `save_failed`, or nothing will be saved and i will know it too. – Alexey May 17 '13 at 17:26
2

I know this is an old question but I'm suprised no one thought of this:

B.transaction do
  broken = a.reject { |o| o.save }
  raise ActiveRecord::Rollback if broken.present?
end

if broken.present?
  # error message
end
d4rky
  • 469
  • 6
  • 13
  • You should use save! instead of the raise. Transaction will handle the rest. – D. Wonnink Mar 30 '16 at 13:38
  • @D.Wonnink yes but it will short-circuit further saves so you will only know about the first one that failed, not about all of them. This can make a world of difference when you have for example 30 objects saved in row, half of them failed and you have to show validation to user in a way that will not make him re-send the form 15 times to correct 15 errors one after another just to learn yet another thing is broken ;) – d4rky May 11 '16 at 07:19
0

In case you're looking for more efficient solution than save each row in the loop please look my answer here Ruby on Rails - Import Data from a CSV file

I'm suggesting to use gem activerecord-import there.

Yaroslav
  • 2,338
  • 26
  • 37