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.
Related
I have this simple piece of code.
ActiveRecord::Base.transaction do
User.new(username: "michael").save!
User.new(username: "lebron").save!
User.new(username: "michael").save! # not unique error
end
Fun fact is that error in the 3rd line doesn't rollback all saves, but just the last one.
What am I not understanding about TX in Ruby so it behaves like that?
I know from documentation that
Both #save and #destroy come wrapped in a transaction
So I assume they are wrapped into a different one then the parent transaction.
Thuss making rollback from the 3rd statement not effective on the two prior ones.
How that should be addressed, so I have all statements rolled back during the course of the transaction?
Actually, ActiveRecord::Base.transaction will verify a single transaction so you can convert this into a single transaction so that it will work properly. So push user details into an array and create it.
Example:
user_details = [{username: "michael"}, {username: "lebron"}, {username: "michael"}] User.create(user_details)
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.
I've got the following update query running in a function called by a before_destroy callback in a Rails model:
Annotation.joins(:annotation_groups)
.where({'annotation_groups.group_id' => self.id})
.update_all({qc_approved: false}) if doc.in_qc?`
(I've also tried the following simpler version to see if another angle works: self.annotations.update_all({qc_approved: false}))
Both generate the below SQL query in "Server development log" (debugging in RubyMine):
UPDATE "annotations" SET "qc_approved" = 'f' WHERE "annotations"."id" IN (SELECT "annotations"."id" FROM "annotations" INNER JOIN "annotation_groups" ON "annotation_groups"."annotation_id" = "annotations"."id" WHERE "annotation_groups"."group_id" = 159)
However, as far as I can tell, that SQL never causes a DB update, even though the destroy process afterwards works fine. I can set a breakpoint directly after the statement and look at the database, and the qc_approved fields are still true. However, I can copy and paste the statement into a Postgres console and run it, and it updates the fields correctly.
Is anyone aware as to what would cause this behavior? Does before_destroy exist in its own strange alternate transactional universe that causes odd behavior like this? What scenario would cause the SQL to show up in the server log but not make it to the DB?
Thanks to the quick and helpful comments above confirming the nature of the callback inside the larger transaction, I figured it out.. despite the name, before_destroy was actually executing after dependent destroy calls, so that the joined annotation_group table row was destroyed before the UPDATE statement that relied on it was called in the transaction.
To be more specific, I added :prepend => true to the before_destroy definition so that it ran before the destroys as intended.
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
I have the following code in a rails model:
foo = Food.find(...)
foo.with_lock do
if bar = foo.bars.find_by_stuff(stuff)
# do something with bar
else
bar = foo.bars.create!
# do something with bar
end
end
The goal is to make sure that a Bar of the type being created is not being created twice.
Testing with_lock works at the console confirms my expectations. However, in production, it seems that in either some or all cases the lock is not working as expected, and the redundant Bar is being attempted -- so, the with_lock doesn't (always?) result in the code waiting for its turn.
What could be happening here?
update
so sorry to everyone who was saying "locking foo won't help you"!! my example initially didin't have the bar lookup. this is fixed now.
You're confused about what with_lock does. From the fine manual:
with_lock(lock = true)
Wraps the passed block in a transaction, locking the object before yielding. You pass can the SQL locking clause as argument (see lock!).
If you check what with_lock does internally, you'll see that it is little more than a thin wrapper around lock!:
lock!(lock = true)
Obtain a row lock on this record. Reloads the record to obtain the requested lock.
So with_lock is simply doing a row lock and locking foo's row.
Don't bother with all this locking nonsense. The only sane way to handle this sort of situation is to use a unique constraint in the database, no one but the database can ensure uniqueness unless you want to do absurd things like locking whole tables; then just go ahead and blindly try your INSERT or UPDATE and trap and ignore the exception that will be raised when the unique constraint is violated.
The correct way to handle this situation is actually right in the Rails docs:
http://apidock.com/rails/v4.0.2/ActiveRecord/Relation/find_or_create_by
begin
CreditAccount.find_or_create_by(user_id: user.id)
rescue ActiveRecord::RecordNotUnique
retry
end
("find_or_create_by" is not atomic, its actually a find and then a create. So replace that with your find and then create. The docs on this page describe this case exactly.)
Why don't you use a unique constraint? It's made for uniqueness
A reason why a lock wouldn't be working in a Rails app in query cache.
If you try to obtain an exclusive lock on the same row multiple times in a single request, query cached kicks in so subsequent locking queries never reach the DB itself.
The issue has been reported on Github.