I've never done that before and I don't know if it is possible. I've got two process which I thing should most likely be wrapped in a database transaction to have the guarantee that either all changes in the database from the transaction will be made or no changes will be made.
process.campaign_code.update(state: 'used')
Campaign.find(process.campaign_code.campaign_id).increment!(:used_campaign_codes_amount, 1)
Try ActiveRecord::Base.transaction:
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
ActiveRecord::Base.transaction do
process.campaign_code.update!(state: 'used')
Campaign.find(process.campaign_code.campaign_id).increment!(:used_campaign_codes_amount, 1)
end
If any of the commands in the transaction throw an error, all SQL transactions in the block will be reverted.
EDIT: It's also worth changing update to update! to ensure an error is thrown if the update is unsuccessful, otherwise it can fail without throwing an error and the block will continue.
Related
As my understanding, if we have any code inside this transaction and when it happens any error with (save!, ...) in that block the entire code will revert, here the problem is if any timeout (rack timeout = 12) happens in this block.
def create
ActiveRecord::Base.transaction do
// timeout happens
end
end
How can we rollback a code with a transaction when a Rack::Timeout occurs?
When a Rack timeout happens, any transaction in process will be rolled back, but transactions that have already been committed will, of course remain committed. You should not have to worry about it.
Once you start a database transaction, it will eventually either be committed or rolled back. Those are the only two possibilities for ending a transaction. When you commit the transaction, you are saying that you want those changes to be saved regardless of what happens next. If you do not commit the transaction, the database will automatically rollback the transaction once it gets into any state where the transaction cannot move forward, such as a broken network connection.
ActiveRecord automatically commits the transaction when the ActiveRecord::Base.transaction do block exits normally. Abnormal exits may cause ActiveRecord to issue a ROLLBACK command to the database, which is efficient and good practice and returns the connection to a ready state, but it is not strictly necessary, because unless the transaction is explicitly committed, the database will eventually automatically roll it back.
If you look at ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction on line 270 and 283 Rails is rescuing Exception. Rescuing Exception will catch anything and everything including kill commands and should generally be avoided. It is used in this case to ensure that no matter what is raised (including Rack::Timeout) the transaction will rollback.
You need to explicitly specify an error class, then it will be rescued and ActiveRecord will rollback the transaction.
e.g. Timeout.timeout(1, Timeout::Error) do
https://ruby-doc.org/stdlib-2.4.0/libdoc/timeout/rdoc/Timeout.html
The exception thrown to terminate the given block cannot be rescued
inside the block unless klass is given explicitly.
Without it ActiveRecord thinks that there is no error and makes a COMMIT.
It seems it was a default behavior until ruby 2.1
I have a case where I need to create like 10000 entries in a table and after some research I decided to use a transaction to do it.
My problem is I haven't found any documentation or guide that will tell me where I put a transaction or how I execute it
This can be achieved very easily:
ActiveRecord::Base.transaction do
... your code ...
end
The code inside the block will run within a database transaction. If any error occurs during execution, all the changes will be rolled back.
I am wondering what does the querying if the connection is in transaction or not actually do ?
Example :
....
try
if not DATA_MODULE.ACRDatabase1.InTransaction then
DATA_MODULE.ACRDatabase1.StartTransaction;
....
DATA_MODULE.ACRDatabase1.Commit();
except
DATA_MODULE.ACRDatabase1.Rollback;
Does it temporarily stop the current transaction if it detects that there is another transaction going on and waits for the other transaction to complete and only then executes or what? Or does it just misfire (rollback) if there's another transaction detected?
Attempting to start a transaction which has already been started will raise an exception. The call to InTransaction simply determines if the transaction has already started or not and returns a True/False response.
I prefer this...it protects you if you have any problems while editing...and only rollback if you have a problem with the Commit. If any exception is raised after the StartTransaction...you will never get to the Commit. You will always run the finally and will make sure you are not in a Tranaction, if so Rollback. I try not to use Try Except, don't have to worry about the Raise
try
DATA_MODULE.ACRDatabase1.StartTransaction;
....
DATA_MODULE.ACRDatabase1.Commit();
finally
if DATA_MODULE.ACRDatabase1.InTransaction then
DATA_MODULE.ACRDatabase1.Rollback;
at the very least the code should be reraising the exception or your user will never know why the data is not saving. ReRaising Exception
Related to Run rails code after an update to the database has commited, without after_commit, but I think deserving its own question.
If I have code like this:
my_instance = MyModel.find(1)
MyModel.transaction do
my_instance.foo = "bar"
my_instance.save!
end
new_instance = MyModel.find(1)
puts new_instance.foo
Is this a guarantee that new_instance.foo will always output "bar" and not its previous value? I'm looking for a way to ensure that all the database actions that occur in a previous statement are committed BEFORE executing my next statements. Rails has an after_commit hook for this, but I don't want this code executed every time... only in this specific context.
I can't find anything in the documentation on Transactions that would indicate if Transaction blocks are "blocking". If they are blocking, that will satisfy my requirement. Unfortunately, I can't think of a practical way to test this behavior to confirm my suspicions one way or another.
Still researching this, but I think a transaction does block code execution until after the database confirms that it has written. Since "save!" is automatically wrapped in a transaction by Rails, the relevant code should run synchronously. The extra transaction block should be unnecessary.
I don't think Rails returns as soon as it hands off the call to the DB when the DB calls are within a transaction. The confusion I had was with after_save callbacks. After_save callbacks suffer from race conditions because they are in fact part of the transaction that saves are automatically wrapped in, so any code called by an after_save callback is not race condition safe, it is not protected by the transaction. Only after_commit calls are safe. Within the transaction Rails will hand off to the DB and then execute after_save callbacks before the DB has finished committing.
Studying this for more insights:
https://github.com/rails/rails/blob/bfdd3c2182156fa2cb81ed4f048b065a2d6f1341/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
UPDATE
Changing my answer to "no". It doesn't appear that save! or save blocks execution. From these two resources, looks like this is a common problem:
https://github.com/resque/resque/wiki/FAQ#how-do-you-make-a-resque-job-wait-for-an-activerecord-transaction-commit
https://blog.engineyard.com/2011/the-resque-way
Is there any way for a before_save filter to halt the entire save without halting the transaction? What I'm trying to do is have a "sample" version of my model that the user can interact with and save but the changes themselves are never actually saved. The following will halt the transaction and (naturally) return false when I call #model.update_attributes:
before_filter :ignore_changes_if_sample
def ignore_changes_if_sample
if self.sample?
return false
end
end
Thanks!
That's precisely what's happening here. If you look at your SQL, you should be seeing BEGIN and then COMMIT, without anything between them. The before_save is not halting the transaction; it's simply preventing the record from being saved by returning false.
To more generally answer your question, records that fail to persist do not halt transactions unless they also raise an exception. Exceptions trigger the ROLLBACK that prevents any part of the transaction from being committed. So even if you return false here, a larger, overarching transaction should continue just fine.
You can read more about transactions and how Rails uses them in the ActiveRecord::Transactions documentation.