ActiveRecord::Base.transaction with Rails .save - ruby-on-rails

It is my understanding that wrapping .save! in ActiveRecord::Base.transaction will make sure all the models (User, Profile, and Setting) to save together or none at all.
However, I was also told that including .save! with all the models.save! methods will also do that. So essentially, both version 1 and 2 are the same. I have a feeling I am wrong, so what is the difference?
Thank you
Version 1
def save
if valid?
ActiveRecord::Base.transaction do
User.save!
Profile.save!
Setting.save!
end
else
false
end
end
Version 2
def save
if valid?
User.save!
Profile.save!
Setting.save!
else
false
end
end
Reference: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

In the first case if any of save! statement fails then all the previous saved models will be rolled back. For ex:
If setting.save! fails then setting.save!, user.save! and profile.save! will be rolled back.
But in second case if any save! statement fails then it will only rollback that statement and it will also raise an exception. For ex:
If setting.save! fails then only setting.save! will be rolled back.
Both statements will work same only in 1 case when the first statement fails 'user.save!' as exception will be raised and in second case subsequent statement will not be executed
The difference between save and save! is that the latter will raise an exception but both will not save the value of object to table if validations fail.

Related

update_attribute not doing anything after first_or_create if record does not exist previously

I do a first_or_create statement, followed by a update_attributes:
hangout = Hangout.where(tour: tour, guide: current_user).first_or_create
hangout.update_attributes(priority: current_user.priority)
If the record already existed, it updates the priority. If it doesn't exist previously, there is no update. Why?
Thanks!
update_attributes (aka update) returns a boolean indicating if there was an error, if you do not check it - use bang-version update! so that exception will not be ignored.
Most probably record does not get created due to validation. Also when you're updating new record just after create - better use first_or_create(priority: current_user.priority) or first_or_initialize(with subsequent update) to spare extra DB write.
def update_attributes!(attributes)
self.attributes = attributes
save!
end
update attribute with bang call the save with bang.
def save!(*args, &block)
create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self))
end
Inside the save! RecordNotSave error will be raise if it cannot save the record.
so you can customize the error handing from your code.
begin
hangout.update_attributes!(priority: current_user.priority)
rescue RecordNotSaved => e
#do the exception handling here
end

Is this an error-handling anti-pattern?

A colleague recently told me this is an anti-pattern (record is an ActiveRecord):
begin
record.save!
do_something_else
rescue => e
puts "Unable to save"
end
...and I should do this instead:
if record.save
do_something_else
else
puts "Unable to save"
end
His argument is that I'm using an exception for flow control (which I agree is bad) but I believe this is a typical error-handling pattern.
Thoughts?
This is an antipattern, because there are better ways to check the validity of a record than running save! (which raises an error if it's not valid).
This is probably what your colleague was getting at:
if record.save
do_something_else
else
puts record.errors.full_messages
end
You see, there is just no benefit here of using the error as control flow because there are less roundabout ways to do it.
You can also run the validations independently of the save attempt. The errors.full_messages array (of strings) is populated when record.valid? gets called. record.save calls record.valid? internally.
if record.valid?
record.save # or save!; by this point you can assume the record is valid
else
puts record.errors.full_messages
end

atomic transaction not working - ruby rails

So I am trying to achieve atomicitiy in while doing my saves (which are essentially updates on each rows).
params[:player_types].each do |p_type_params|
if p_type_params[:id]
player = #player_types.find(p_type_params[:id])
player.assign_attributes(p_type_params)
#player_types << player
end
end
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
errors = true
end
end
end
Inspite of saving withing the transaction block, I can see partial saves also i.e. one of the rows get updated and the erroneous one does not (obviously) where as I would have wanted the updated row to be rolled back since there was atleast one row that could not be updated due to an error. Is my interpretation of transaction block correct in this case? How do I achieve an atomic save in my case?
EDIT: The model validates for uniqueness of one of the columns, which would be the reason for failing to update in the Database at this point.
You need to raise an error inside your transaction block to abort the transaction; setting errors doesn't impact the transaction.
For instance:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
raise "save failed!"
end
end
end
The more conventional way of doing this, of course, is to use save! which raises an exception for you when it fails:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
player_type.save!
end
end
If you really need to "DO Something" on failure (besides aborting the transaction), you'll have to use the first method.

Why does persisted? return true after ActiveRecord::Rollback?

Example:
BillingProfile.transaction do
if #billing_profile.save
unless SomeService.do_something # returns false and rollback occurs
raise ActiveRecord::Rollback
end
end
end
#billing_profile.persisted? # Still return true, despite rollback
#billing_profile.id # Is set, despite rollback
Why wouldn't the state of #billing_profile reflect that the record was rolled back?
This is a problem since the record cannot be created after it has been rolled back.
Turns out this was a bug in ActiveRecord (Rails 4): https://github.com/rails/rails/issues/13744
It has now been fixed.
I got interested in how transactions work. Your specific scenario is explained in the docs. Quoting the docs
transaction calls can be nested. By default, this makes all database
statements in the nested transaction block become part of the parent
transaction. For example, the following behavior may be surprising:
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
creates both “Kotori” and “Nemu”. Reason is the ActiveRecord::Rollback
exception in the nested block does not issue a ROLLBACK. Since these
exceptions are captured in transaction blocks, the parent block does
not see it and the real transaction is committed.
In order to get a ROLLBACK for the nested transaction you may ask for
a real sub-transaction by passing requires_new: true. If anything goes
wrong, the database rolls back to the beginning of the sub-transaction
without rolling back the parent transaction. If we add it to the
previous example:
User.transaction do
User.create(username: 'Kotori')
User.transaction(requires_new: true) do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
only “Kotori” is created. This works on MySQL and PostgreSQL. SQLite3
version >= '3.6.8' also supports it.
Most databases don't support true nested transactions. At the time of
writing, the only database that we're aware of that supports true
nested transactions, is MS-SQL. Because of this, Active Record
emulates nested transactions by using savepoints on MySQL and
PostgreSQL. See dev.mysql.com/doc/refman/5.6/en/savepoint.html for
more information about savepoints.

Return false and rollback in after_save callback

In ActiveRecord model after_save callback I need to ROLLBACK transaction and return false.
def after_save_callback
if mycondition?
raise ActiveRecord::Rollback
end
end
This callback rolls-back transaction but mymodel.save! returns true. How to make it return false and rollback?
If you want to abort a save in an after_save callback, you should
raise ActiveRecord::RecordInvalid.new(self)
rather than
raise ActiveRecord::Rollback
This will not only roll back the transaction (callbacks always happen inside a possibly-implicit transaction as part of the save or create) but also cause save to return false.
Here is an article with more details: http://tech.taskrabbit.com/blog/2013/05/23/rollback-after-save/
def around_save
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback # this will actually ROLLBACK
yield # calls the actual save method
raise ActiveRecord::Rollback # this will cause a COMMIT!!! because it affect only this internal transaction.
# OTHER ACTIONS NOT EXECUTED BUT BEING A INTERNAL TRANSACTION, THE PARENT WILL COMMIT, because parent hasn't failed.
end
end
So... I think around_save come already on a transaction block, so you don't need to add that extra ActiveRecord::Base.transaction do block because rollbacks doesnt propagate up
So if you want to rollback before or after yield, you need to remove that internal transaction.
def around_save
#ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback # this will actually ROLLBACK
yield # calls the actual save method
raise ActiveRecord::Rollback # this will actually ROLLBACK
# end
end
EDIT: Reading what I wrote... now seem difficult to understand. The point is: if you are gonna use aroud_save don't wrapp again with ActiveRecord::Base.transaction (do like in the last example) because rails will wrap the call to around_save with is own ActiveRecord::Base.transaction so when you raise ActiveRecord::Rollback you are only rolling back the most internal transaction, so you can end with extrange results and partial saves (like in the first example whic is FAIL).
I don't think you can do this with after_save You should be looking at around_save instead:
def around_save
ActiveRecord::Base.transaction do
yield # calls the actual save method
raise ActiveRecord::Rollback if my_condition?
end
end

Resources