I'm using the following code in a Rails 5.2 project:
c = Country.where(name: "test").first_or_create
c.status = "Old"
c.save
This works, however, I only want to change the status when there is not a country already (so I only want to insert countries, not update). However, I also need to use c later in my code.
I realize I could just do an if statement, but I have to do a similar thing multiple times in my code, and it seems like a shortcut must exist.
Use create_with by following:
c = Country.create_with(status: 'old').find_or_create_by(name: 'test')
This will create Country with status 'old' and name 'test' only and only if it doesn't find the country with name 'test'. If it does find the country with name 'test', it will not update the status.
Ultimately, it will also return the country in c, whether after finding or creating.
create_with only sets attributes when creating new records from a relation object.
The first_or_create method finds the result in table and If it exists in the table, the first instance will be returned. If not, then create is called.
If a block is provided, that block will be executed only if a new instance is being created. The block is NOT executed on an existing record.
So If you only want to change status when country is created then use block with first_or_create method.
so your code looks like
Country.where(name: "test").first_or_create do |country|
country.status = "Old"
country.save
end
By default where returns array type and you will need to iterate to assign a new value.
And find_by returns actual object or nil.
Country.where(name: "nothing") => [] or [obj, ...]
Country.find_by(name: "nothing") => nil or object
You can use find_or_create option:
Country.find_or_create_by(name: 'test') do |c|
c.status = "Old"
end
c = Country.find_or_create_by(name: 'test')
c.status = "Old"
c.save
Related
I am fetching data from CSV files and saving them to the database. Here's the simplified code structure (it's a rake task):
... loading CSV tools...
car = Car.new
car.tender_load_id = data.element[8]
car.brand = data.element[2]
car.color = 'green'
...
car.build_car_metadata(mbol: help_var[:mbol], ...)
car.first_registration = data.element[21]
car.skip_registration_code_validation = true # for not creating registration_code
if existing_car = Car.where("cars.tender_load_id ILIKE ?", "%#{car.tender_load_id}%").first
# car already exists => update
existing_car.update(car)
puts "Car exists, updating car with ID #{existing_car.id}."
else
car.save!
end
When I run this take task, I get the following error message:
NoMethodError: undefined method `reject' for #<Car:0x007fa3e2063a78>
and it points out to this line:
existing_car.update(car)
How do I make this work? I unfortunately cannot place the if-else part on the beginning of the rake task, the structure needs to be like this.
Thank you in advance.
You need to update the existing car with just the attributes, you can not pass in a full model.
Build the attributes, then create or update depending on whether a car exists already:
attributes = {}
attributes[:color] = ...
attributes[:brand] = ...
attributes[:tender_load_id] = ...
if (car = Car.where("cars.tender_load_id ILIKE ?", "%#{attributes[:tender_load_id]}%").first)
car.update(attributes)
else
Car.create!(attributes)
end
Note 1: It seems there can be multiple cars with the same tender_load_id. This means that the query for existing car will return an arbitrary record. Perhaps you want to add an order to that query.
Note 2: The way you query seems brittle. Isn't there a better ID in the CSV?
Note 3: attributes ending in _id are usually foreign keys. So perhaps find a better name for this attribute.
This is a custom rake task file, if that makes a difference.
I want to pull all user_id's from Pupil, and apply them to get the User.id for all pupils.
It happily prints out line 2 with correct user_id, but then considers user_id a 'method' and breaks on line 3. Why? Where is my mistake?
course_pupils = Pupil.where(course_id: study.course_id)
course_pupils.map { |a| puts a.user_id }
pupils = User.where(id: course_pupils.user_id )
course_pupils is still a relation when you are calling it in line 3. Line 2 is non destructive (and if it was, it would turn it into an array of nils because puts returns nil).
You need to do:
pupils = User.where(id: course_pupils.pluck(:user_id) )
Or something like that
You are doing it wrong, you cannot call an instance method user_id on a collection, try this instead
user_ids = Pupil.where(course_id: study.course_id).pluck(:user_id)
pupils = User.where(id: user_ids )
Hope that helps!
The model User has first, last and login as attributes. It also has a method called name that joins first and last.
What I want is to iterate through the Users records and create an array of hashes with the attributes I want. Like so:
results = []
User.all.map do |user|
record = {}
record["login"] = user.login
record["name"] = user.name
results << record
end
Is there a cleaner way in Ruby to do this?
Trying to map over User.all is going to cause performance issues (later, if not now). To avoid instantiating all User objects, you can use pluck to get the data directly out of the DB and then map it.
results = User.all.pluck(:login, :first, :last).map do |login, first, last|
{ 'login' => login, 'name' => first << last }
end
Instantiating all the users is going to be problematic. Even the as_json relation method is going to do that. It may even be a problem using this method, depending on how many users there are.
Also, this assumes that User#name really just does first + last. If it's different, you can change the logic in the block.
You can use ActiveRecord::QueryMethods#select and ActiveRecord::Relation#as_json:
User.select(:login, '(first || last) as name').as_json(except: :id)
I would write:
results = User.all.map { |u| { login: u.login, name: u.name } }
The poorly named and poorly documented method ActiveRecord::Result#to_hash does what you want, I think.
User.select(:login, :name).to_hash
Poorly named because it does in fact return an array of Hash, which seems pretty poor form for a method named to_hash.
Need to check if a block of attributes has changed before update in Rails 3.
street1, street2, city, state, zipcode
I know I could use something like
if #user.street1 != params[:user][:street1]
then do something....
end
But that piece of code will be REALLY long. Is there a cleaner way?
Check out ActiveModel::Dirty (available on all models by default). The documentation is really good, but it lets you do things such as:
#user.street1_changed? # => true/false
This is how I solved the problem of checking for changes in multiple attributes.
attrs = ["street1", "street2", "city", "state", "zipcode"]
if (#user.changed & attrs).any?
then do something....
end
The changed method returns an array of the attributes changed for that object.
Both #user.changed and attrs are arrays so I can get the intersection (see ary & other ary method). The result of the intersection is an array. By calling any? on the array, I get true if there is at least one intersection.
Also very useful, the changed_attributes method returns a hash of the attributes with their original values and the changes returns a hash of the attributes with their original and new values (in an array).
You can check APIDock for which versions supported these methods.
http://apidock.com/rails/ActiveModel/Dirty
For rails 5.1+ callbacks
As of Ruby on Rails 5.1, the attribute_changed? and attribute_was ActiveRecord methods will be deprecated
Use saved_change_to_attribute? instead of attribute_changed?
#user.saved_change_to_street1? # => true/false
More examples here
ActiveModel::Dirty didn't work for me because the #model.update_attributes() hid the changes. So this is how I detected changes it in an update method in a controller:
def update
#model = Model.find(params[:id])
detect_changes
if #model.update_attributes(params[:model])
do_stuff if attr_changed?
end
end
private
def detect_changes
#changed = []
#changed << :attr if #model.attr != params[:model][:attr]
end
def attr_changed?
#changed.include :attr
end
If you're trying to detect a lot of attribute changes it could get messy though. Probably shouldn't do this in a controller, but meh.
Above answers are better but yet for knowledge we have another approch as well,
Lets 'catagory' column value changed for an object (#design),
#design.changes.has_key?('catagory')
The .changes will return a hash with key as column's name and values as a array with two values [old_value, new_value] for each columns. For example catagory for above is changed from 'ABC' to 'XYZ' of #design,
#design.changes # => {}
#design.catagory = 'XYZ'
#design.changes # => { 'catagory' => ['ABC', 'XYZ'] }
For references change in ROR
This question is quite simple but I have run into the problem a few times.
Let's say you do something like:
cars = Vehicle.find_by_num_wheels(4)
cars.each do |c|
puts "#{c.inspect}"
end
This works fine if cars is an array but fails if there is only one car in the database. Obviously I could do something like "if !cars.length.nil?" or check some other way if the cars object is an array before calling .each, but that is a bit annoying to do every time.
Is there something similar to .each that handles this check for you? Or is there an easy way to force the query result into an array regardless of the size?
You might be looking for
cars = Vehicle.find_all_by_num_wheels(4)
The dynamic find_by_ methods only return one element and you have to use find_all_by_ to return multiple.
If you always want all of the cars, you should use find_all instead:
cars = Vehicle.find_all_by_num_wheels(4)
You could also turn a single Vehicle into an array with:
cars = [cars] unless cars.respond_to?(:each)
Named scoped version for your problem
Vehicle.scoped(:conditions => { :num_wheels => 4 } ).each { |car| car.inspect }
You can do this to get arrays everytimes :
cars = Vehicle.find(:all, :conditions => {num_wheels => 4})
I don't think that you have a loop that will check if the object is an array.
Another solution could be:
for i in (1..cars.lenght)
puts cars[i].inspect
end
(haven't tested, it might break to test the lenght on a string. Let me know if it does)