I have a model named "Person".
In ruby console, I first declare an instance of Person, then I update the attributes, then save.
person = Person.last
person.name = "jeff"
person.save
After doing these, I got message like this:
(9.9ms) BEGIN
(7.5ms) ROLLBACK
=> false
What are "BEGIN", "ROLLBACK", "false" refers to separately?
I googled, but nothing came out.
When you do save in Rails, it wraps the database operation in a transaction. BEGIN is written to the log when the transaction starts and ROLLBACK is logged if the operation fails (because all write operations in the transaction—UPDATE, INSERT or DELETE—are "rolled back").
false is the value that save returns when the operation fails.
You usually want to use save! instead of save because it will raise an (informative) exception if the operation fails.
Also look at update_attributes:
http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html
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 have this index in my Model's table:
UNIQUE KEY `index_panel_user_offer_visits_on_offer_id_and_panel_user_id` (`offer_id`,`panel_user_id`)
And this code:
def get_offer_visit(offer_id, panel_user_id)
PanelUserOfferVisit.where(:offer_id => offer_id, :panel_user_id => panel_user_id).first_or_create!
end
is randomly causing an ActiveRecord::RecordNotUnique exception in our application,
the issue is:
Duplicate entry for key
'index_panel_user_offer_visits_on_offer_id_and_panel_user_id'
I've read on the rails doc that first_or_create! is not an atomic method and can cause this kind of issues in a high concurrency environment and that a solution would be just to catch the exception and retry.
I tried different approaches, including Retriable gem to retry a certain number of times to repeat the operation but RecordNotUnique is still raised.
I even tried to change the logic:
def get_offer_visit(offer_id, panel_user_id)
begin
offer_visit = PanelUserOfferVisit.where(:offer_id => offer_id, :panel_user_id => panel_user_id).first_or_initialize
offer_visit.save!
offer_visit
rescue ActiveRecord::RecordNotUnique
offer_visit = PanelUserOfferVisit.where(:offer_id => offer_id, :panel_user_id => panel_user_id).first
raise Exception.new("offer_visit not found") if offer_visit.nil?
offer_visit
end
end
But the code still raise the custom exception I made and I really don't understand how it can fails to create the record on the first try because the record exists but then when it tries to find the record it doesn't find it again.
What am I missing?
You seem to be hitting rails query cache for PanelUserOfferVisit.where(...).first query: rails have just performed identical SQL query, it thinks that result will remain the same for the duration of the request.
ActiveRecord::Base.connection.clear_query_cache before the second call to db should help
Also you do not have to save! the record if it is not new_record?
If I understand properly your problem here is due to a race condition. I didn't found the exact piece of code from Github but lets say the method find_or_create is something like (edit: here is the code):
def first_or_create!(attributes = nil, &block)
first || create!(attributes, &block)
end
You should try to solve the race conditions using optimistic or pessimistic locking
Once locking implemented within your custom method, you should continue capturing possible NotUnique exceptions but try to clean cache or use uncached block before to perform the find again.
I experienced a similar issue but the root cause was not related to a race condition.
With first_or_create!, the INSERT remove surrounding spaces in the value but the SELECT don't.
So if you try this:
Users.where(email: 'user#mail.com ')
(mind the space at the end of the string), the resulting sql query will be:
SELECT "users".* FROM "users" WHERE "users"."email" = 'user#mail.com ' ORDER BY "users"."id";
which result in not finding the potentially existing record, then the insert query will be:
INSERT INTO "users" ("email") VALUES ('user#mail.com');
which can result in ActiveRecord::RecordNotUnique error if a record with 'user#mail.com' as an email already exists.
I do not have enough details about your problem so it might be unrelated but it might helps others.
Specifically http://guides.rubyonrails.org/active_record_validations.html#custom-methods
I feel like this isn't answered/documented properly. If you look at the example code -> all it does is call errors.add which according to this http://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
doesn't do much other except add the message to the errors.
Upon testing, it does halt the save but I will leave this here for people to find.
An object is saved to the database only if it is valid. Using 'errors.add(:attribute_name, error_message)' associates an error with the object making it invalid, resulting in the object not being saved.
If an error exists on a field in an object (we'll call it "x"), then x.valid? is false. A failure of this validity check DOES prevent the object from saving. It will either return false if you call x.save (or create(x_params)), or raise an error if you call x.save! (or create!(x_params)). Raising errors is especially useful in the context of creating multiple record in a transaction, as that will break you out of the transaction and into your rescue block (assuming you allow for such).
I'm trying to understand how to use hstore and it seems the database isn't updated if I try to modify a value in my hash.
In my rails console I do
u = User.new
u.hash_column = {'key' => 'first'}
u.save
I get a message in my console
(0.4ms) BEGIN
SQL (2.0ms) UPDATE "users" SET "hash_column" = $1, "updated_at" = $2 WHERE ...
(18.0ms) COMMIT
and when I check in the DB the column has the correct data
now when I try
u.hash_column['key'] = 'second'
the model seems correct in the console
puts u.hash_column
gives
{"key"=>"second"}
however when I try to save this
u.save
in my console I just get
(0.3ms) BEGIN
(0.2ms) COMMIT
with no update statement and when I check the DB the data hasn't changed.
if I change another attribute on the user model and save it hash_column still doesn't get updated. The only way I can change the hash_column in the database seems to be to assign to the complete hash like
u.hash_column = {'key' => 'second'}
is this how it is meant to work or am I doing something wrong?
This is due to the way that rails currently tracks changes. It will only track changes when you use the attributes setter (u.hash_column). If you update a value in the hash, you have to notify Rails that the value changed by calling u.hash_column_will_change!. This marks the value as dirty and the next time save is called, it will be persisted.
This also effects arrays and strings and dates. If you call u.string_value.gsub! or u.array_column << value those changes will not be persisted without calling the <column>_will_change! method for those columns
Here is my before_save method:
before_save :check_postal
def check_postal
first_three = self.postal_code[0..2]
first_three.downcase!
postal = Postal.find_by_postal_code(first_three)
if postal
self.zone_id = postal.zone_id
else
PostalError.create(postal_code: self.postal_code)
return false
end
end
Everything runs fine when self.zone_id = postal.zone_id but inside the else statement, my PostalError.create(postal_code: self.postal_code) doesn't save the record to the database..
I know it's got something to do with the return false statement, because when I remove it, it saves fine -- but then that defeats the purpose..
How can I get a new PostalError record to save while returning false to prevent the current object from saving..
You're exactly right: the problem is the before_save.
The entirety of the save process is wrapped in a transaction. If the save fails, whether it be because of a validation failure, an exception being rolled back or something else, the transaction is rolled back. This undoes the creation of your PostalError record.
Normally this is a good thing - it's so that incomplete saves don't leave detritus around
I can think of two ways to solve this. One is to not create the record there at all: use a after_rollback hook to execute it once the danger has passed.
The other way is to create that record using a different database connection (since transactions are a per connection thing). An easy way to do that is to use a different thread:
Thread.new { PostalError.create(...)}.join
I stuck the join on there so that this waits for the thread to complete rather than adding a degree of concurrency to your app that you might not expect.
i don't know, but i trying to guess the solve.
else
PostalError.create(postal_code: self.postal_code)
self.zone_id = postal.zone_id
end