24

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?

6 Answers 6

55
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.

Sign up to request clarification or add additional context in comments.

2 Comments

Attention! This will not work unless you use save!. It seems that transaction is only aborted if there is an exception.
Now you need to catch ActiveRecord::RecordInvalid somewhere.
29
a.each(&:save)

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

2 Comments

And wrap that in a B.transaction to get it all saved in one atomic operation.
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!
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.

2 Comments

Instead of !b.errors.blank?, why not b.errors.present?
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?
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

10 Comments

save! is perfect, the transaction do block will automatically revert all other changes.
You would need to manually catch exceptions outside the transaction if a validation fails.
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.
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.
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.
|
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

2 Comments

You should use save! instead of the raise. Transaction will handle the rest.
@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 ;)
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.