Rails executing update on deleted records.
I have a web app on ruby on rails in which I created some users and after that I opened the rails console and assigned U1 to one of my user let say last user then assigned the same User to U2. Then I run U1.destroy which executes successfully
after that I updated the name of user through U2 and it returns me true Although, user was destroyed from database when I checked it. My concern is rails should give me false as there was no object in database against that ID.
If you want to double check that record exists before updating you can use reload
user.reload.update(name: "Some Name")
It will raise ActiveRecord::RecordNotFound if record with such id is absent
UPDATE changes the values of the specified columns in all rows that satisfy the condition.
https://www.postgresql.org/docs/current/sql-update.html
Rails doesn't return false or raise an exception because the UPDATE is still a valid query in the database, even if no rows match the condition. If you connect directly to your PostgreSQL database and run...
UPDATE users
SET name = 'test'
WHERE id = 123
...if 123 is an id that no longer exists, then the database will successfully execute the query and respond with:
UPDATE 0
If it is an id that still exists, the database will respond with:
UPDATE 1
This is similar to how Rails behaves if you use update_all. If you were to run update_all on a record that no longer exists, you'd see something like:
User.where(id: 123).update_all(name: 'test')
=> 0
But if the record exists you'd see:
User.where(id: 123).update_all(name: 'test')
=> 1
No error will be raised.
The purpose of the Rails update and update_all methods is just to attempt to run an UPDATE query in the database. If there is a timing issue and the record no longer exists, that's not something that the database or Rails is designed to give warnings about.
Related
Hello I was trying the update the data in the table using the rails console.
Box.where("code = 'learning'").update(duration: 10)
I ran this command.
The data is temporarily changing.
Box.where("code = 'learning'")
When I run this the precious data is being displayed.
Could anyone let me the issue.
Thank you in advance.
#update updates a single record.
user = User.find_by(name: 'David')
user.update(name: 'Dave')
It will return true/false depending on if the record was actually updated. You can see the validation errors by inspecting the errors object:
user.errors.full_messages
In non user-interactions situations like seed files and the console it can be helpful to use the bang methods such as #update!, #save! and #create! which will raise an exception if the record is invalid.
If you want to update multiple records at once you need to use #update_all:
Box.where("code = 'learning'")
.update_all(duration: 10)
This creates a single SQL update statement and is by far the most performant option.
You can also iterate through the records:
Box.where("code = 'learning'").find_each do |box|
box.update(duration: 10)
end
This is sometimes necissary if the value you are updating must be calculated in the application. But it is much slower as it creates N+1 database queries.
I'm currently deploying a Ruby on Rails web application with Postgres. I'm working with Docker, just to say it.
When I deploy my application, I insert some predefined data into the database. When I want to create a new record, I get a duplicate key error.
Full error message:
ERROR: duplicate key value violates unique constraint "modelname_pkey"
DETAIL: Key (id)=(1) already exists
How can I solve this? How can I tell Rails to continue with the primary key from the last record?
You can check insert_all method from Rails 6. If you are in lower versions of Rails use activerecord-import gem.
In case of insert_all
First form the json
new_records = [{id: 1, name: 'steve'},{id: 2, name: 'george'}]
Model.insert_all(new_records)
This will insert records if its not already there and ignore if records are there.
In case of activerecord-import
new_records = [{id: 1, name: 'steve'},{id: 2, name: 'george'}]
Model.import new_records, on_duplicate_key_ignore: true
references:
activerecord-import
rails-6-insert_all
If you don't specify an ID the database will choose one for you.
person = Person.create!( name: "MacReady", thing: false )
If you need to reference a specific ID, reconsider that. Relying on special IDs is too fragile, as you're discovering. Database IDs should be considered unique identifiers with no further special meaning.
For example, instead of remembering that "user ID 1 is the admin user" add an "admin" field.
admin = User.create!( name: "Yarrow Hock", admin: true )
Now you can check if user.admin for any user, have as many admins as you want, and change whether a user is an admin at any time.
I think this can also resolve your problem. Add this in your staging or preprod console (I am using Heroku for this so I added in my heroku rails console):
connection = ActiveRecord::Base.connection
connection.execute("SELECT setval(pg_get_serial_sequence('table_name', 'id'), MAX(id)) FROM table_name;")
Check out this blog for more info and how this is been generated.
I'm developing a reservation system for stuff that you could rent.
I would like to restrict multiple users from reserving the same item.
I display a list, which user can click on the item to check the details.
If any user has already opened the detail view then other user can not open it at the same time.
I am maintaining a flag call is_lock to check if the record is already locked but I was facing issue when multiple users clicked on the same item at the same time.
So I implementing pessimistic lock, which reduced the rate of occurrence of this issue but multiple users opening the same item but it did not completely fixed the issue. I am still facing the same thing.
begin
Item.transaction do
item = Item.lock.where(id: item_id, is_lock: false)
item.is_lock = true;
item.save!
end
rescue Exception => e
# Something went wrong.
end
Above is the code that I have implemented.
Please let me know if I am doing anything wrong.
EDIT:
I've tried the solution provided by #rmlockerd in following way:
Run rails in 2 separate consoles.
Fetch record with lock that has id:100 from console-1.
Fetch to fetch the same record from console-2.
But the above test failed as I was able to fetch the same record from both console even though the record was locked from console-1.
Run rails in 2 separate consoles.
It might be misleading to just look at the snippet you provided, but there does seem like a possible race condition due to your .where predicate.
If User2 attempts to get a lock on the same item after User1 but before first the transaction commits, the .where will still return the original record with is_lock false. The default behaviour for .lock is to simply wait its turn for a lock. So User2 would block until the original transaction commits, then get a lock and proceed to set is_lock to true as well.
The good news is that when you get a lock, Rails reloads the record so you are getting the latest data. Checking is_lock after obtaining the lock should eliminate that race condition, like so:
Item.transaction do
item = Item.lock.find_by(id: item_id, is_lock: false) # only 1, so where is unnecessary
return if item.blank? || !item.is_lock
item.update!(is_lock: true)
end
# I have the lock stuff...
The .lock method also takes an optional 'locking clause' -- which varies based on the database you use -- that can be used to configure the locking behaviour. For example, if you use Postgres, you could do:
Item.transaction do
item = Item.lock('FOR UPDATE SKIP LOCKED').find_by(id: item_id, is_lock: false)
return if item.blank?
item.update!(is_lock: true)
end
The SKIP LOCKED clause directs Postgres to automatically skip any record that is already locked. In the race condition described above, the second call to .lock would bail immediately and return nil, so a simple check of item presence would suffice. Check out the Postgres or MySQL documentation if you're interested in database-specific locking clauses.
So I had some problems with my database. I have an import functionality which I used to import a few thousand records. Now I was developing locally (SQLite) and everything worked fine. I pushed my updates to heroku where I have a PostgreSQL database. I ran my import there and it appeared to work well too. Now, I'm trying to manually create an other record in that table and I kept getting the error that the unique ID was a duplicate. So, after some online searching I created this extension to active record:
def self.reset_pk_sequence
case ActiveRecord::Base.connection.adapter_name
when 'SQLite'
new_max = maximum(primary_key) || 0
update_seq_sql = "update sqlite_sequence set seq = #{new_max} where name = '#{table_name}';"
ActiveRecord::Base.connection.execute(update_seq_sql)
when 'PostgreSQL'
ActiveRecord::Base.connection.reset_pk_sequence!(table_name)
else
raise "Task not implemented for this DB adapter"
end
end
I ran it on my table, and the ID is now counting up from where it should (3000 something instead of 1,2,3, etc).
My problem now is that I am able to create new records in my table, and I see them in my index page. However, when I try to open that specific record, I get this error:
The page you were looking for doesn't exist.
You may have mistyped the address or the page may have moved.
The link from my index is just to the ID of the record. When I check if this ID exists in the console, I'm perfectly able to find the record. What's going wrong here?
I figured it out. I had some empty associations that I was trying to display in the view. The error threw me off since it appeared like something was wrong with my record.
My seeds file populated the countries table with a list of countries. But now it needs to be changed to hard-code the id (instead of rails generating the id column for me).
I added the id column and values as per below:
zmb: {id: 103,code: 'ZMB', name: Country.human_attribute_name(:zambia, default: 'Error!'), display_order: nil, create_user: user, update_user: user, eff_date: Time.now, exp_date: default_exp_date},
skn: {id: 104,code: 'SKN', name: Country.human_attribute_name(:st_kitts_and_nevis, default: 'Error!'), display_order: nil, create_user: user, update_user: user, eff_date: Time.now, exp_date: default_exp_date}
countries.each { |key, value| countries_for_later[key] = Country.find_or_initialize_by(id: value[:id]); countries_for_later[key].assign_attributes(value); countries_for_later[key].save!; }
Above it just a snippet. I have added an id: for every country.
But when I run db:seed I get the following error:
ActiveRecord::RecordInvalid: Validation failed: Code has already been taken
I am new to rails so I'm not sure what is causing this - is it because the ID column already exists in the database?
What I think is happening is you have existing data in your database ... let's say
[{id:1 , code: 'ABC'},
{id:2 , code: 'DEF'}]
Now you run your seed file which has {id: 3, 'DEF'} for example.
Because you are using find_or_initialize_by with id you are running into errors. Since you can potentially insert duplicates.
I recon you should just clear your data, but you can try doing find_or_initialize_by using code instead of id. That way you wont ever have a problem of trying to create a duplicate country code.
Country.find_or_initialize_by(code: value[:code])
I think you might run into problems with your ids, but you will have to test that. It's generally bad practice to do what you are doing. Whether they ids change or now should be irrelevant. Your seed file should reference the objects that are being created not ids.
Also make sure you aren't using any default_scopes ... this would affect how find_or_initialize_by works.
The error is about Code: Code has already been taken. You've a validation which says Code should be uniq. You can delete all Countries and load seeds again.
Run this in the rails console:
Country.delete_all
Then re-run the seed:
rake db:seed
Yes, it is due to duplicate entry. In that case run ModelName.delete_all in your rails console and then run rake db:seed again being in the current project directory. Hope this works.
ActiveRecord::RecordInvalid: Validation failed: Code has already been taken
is the default error message for the uniqueness validator for :code.
Running rake db:reset will definitely clear and reseed your database. Not sure about the hardcoded ids though.
Check this : Overriding id on create in ActiveRecord
you will have to disable protection with
save(false)
or
Country.create(attributes_for_country, without_protection: true)
I haven't tested this though, be careful with your validators.
Add the line for
countries_for_later[key].id = value[:id]
the problem is that you can't set :id => value[:id] to Country.new because id is a special attribute, and is automatically protected from mass-assignment
so it will be:
countries.each { |key, value|
countries_for_later[key] = Country.find_or_initialize_by(id: value[:id])
countries_for_later[key].assign_attributes(value)
countries_for_later[key].id = value[:id] if countries_for_later[key].new_record?
countries_for_later[key].save(false)
}
The ids data that you are using in your seeds file: does that have any meaning outside of Rails? Eg
zmb: {id: 103,code: 'ZMB',
is this some external data for Zambia, where 103 is it's ID in some internationally recognised table of country codes? (in my countries database, Zambia's "numcode" value is 894). If it is, then you should rename it to something else, and let Rails decide what the id field should be.
Generally, mucking about with the value of ID in rails is going to be a pain in the ass for you. I'd recommend not doing it. If you need to do tests on data, then use some other unique field (like 'code') to test whether associations etc have been set up, or whatever you want to do, and let Rails worry about what value to use for ID.