Rails - More about "N+1" - ruby-on-rails

When I try to show linked data in index action - I can't avoid N+1 problem using standart includes. Example:
Model 1 - Pet (let it will be animal: title:string. Pet can has many PetAttributeElems, they show attributes this Pet have and what values they have)
Model 2 - PetAttribute (this model contains only titles of attributes, like weight, age and 1000+ more attributes. PetAttribute has many PetAttributeElems - one attribute, such as weight, can be described for many Pets)
Model 3 - PetAttributeElem (this model belongs to pet and to petAttribute, also it has value field, that show value of attribute for pet.
When I make show action - I use in HAML:
-#pet.pet_attribute_elems.includes(:pet_attribute).each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
But when I make index action I want to use:
-#pets.each do |pet|
-pet.pet_attribute_elems.includes(:pet_attribute).each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
That includes method will call many SQL queries, for every pet
Now I solve it by manually creating additional object like this:
#pet_elems = {}
PetAtributeElems.includes(:pet_attribute)
.where(pet_id:#pets.map(&:id)).each do |elem|
pet_id = elem.pet_id
if #pet_elems.include?(pet_id)
#pet_elems[pet_id] << elem
else
#pet_elems[pet_id] = [elem]
end
end
Than I can use:
-#pets.each do |pet|
-if #pet_elems.include?(pet.id)
-#pet_elems[pet.id].each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
How could I solve that task more easy?

You're going down a non rails-convention path.
Move code out of the views so it's simply
= render #pet_attribute_elems
Make a partial to handle display
# _pet_attribute_elems.html.haml
="#{pet_attribute_elem.pet_attribtute.title}: #{pet_attribute_elem.value}"
In the controller, do the queries
def show
#pet = Pet.find(...)
#pet_attribute_elems = #pet.pet_attribute_elems.includes(:pet_attribute)
end
def index
#pet_attribute_elems = PetAttributeElem.includes(:pet_attribute)
end

Related

Rails - how to update existing record in database with a newly created object

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.

ActiveAdmin batch action form with entries from multiple classes as input

Following the batch actions guide from activeadmin, one can create a form of different types. What I want is, that the form have entries of different model classes.
I have three classes: Shop, RecordingShop and DistributionChain. A DistributionChain can have one or more Shops or RecordingShops but can also have none of them.
On the Scores index table, I want to show an export batch action and recognize which class the form entry belongs to (either Shop, RecordingShop or DistributionChain)
Is it possible to do something like:
ActiveAdmin.register Score, as: 'Delivery' do
...
batch_action :export, form: {shops: (DistributionChain.all + Shop.not_distribution_chain).collect{ |e| [e.name, e.id, e.class.name]} } do |ids, inputs|
...
end
...
end
so that the class would be a third parameter in the nested array, after the element name and id, and include it in the inputs variable?
Solved. I found a way through declaring variables in the admin resource code itself.
ActiveAdmin.register Score, as: 'Delivery' do
...
EXPORT_INPUTS = DistributionChain.all + Shop.not_distribution_chain
EXPORT_INPUTS_DATA = EXPORT_INPUTS.collect.with_index{ |e, i| [e.name, i] }
...
batch_action :export, form: {shops: EXPORT_INPUTS_DATA } do |ids, inputs|
element = EXPORT_INPUTS[inputs['shops'].to_i]
...
end
...
end
EXPORT_INPUTS contains the actual set of elements from multiple classes.
EXPORT_INPUTS_DATA is the variable (array) to use in the form and has the name and the index of the elements in the array. The chosen index can be retrieved later as inputs['shops'] and EXPORT_INPUTS[inputs['shops'].to_i] corresponds exactly to the element we want.
NB: EXPORT_INPUTS_DATA should be defined outside the action. If you write
batch_action :export, form: {shops: EXPORT_INPUTS.collect.with_index{ |e, i| [e.name, i] } }
directly, inputs['shops'] will only be equal to the id of the element in its respective class, which does not tell us anything.

rails "where" statement: How do i ignore blank params

I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking

Rails - dynamic method creation

I have two methods that are identical apart from the ActiveRecord class they are referencing:
def category_id_find(category_name)
category = Category.find_by_name(category_name)
if category != nil
return category.id
else
return nil
end
end
def brand_id_find(brand)
brand = Brand.find_by_name(brand)
if brand != nil
return brand.id
else
return nil
end
end
Now, I just know there must be a more Railsy/Ruby way to combine this into some kind of dynamically-created method that takes two arguments, the class and the string to find, so I tried (and failed) with something like this:
def id_find(class, to_find)
thing = (class.capitalize).find_by_name(to_find)
if thing.id != nil
return thing.id
else
return nil
end
end
which means I could call id_find(category, "Sports")
I am having to populate tables during seeding from a single, monster CSV file which contains all the data. So, for example, I am having to grab all the distinct categories from the CSV, punt them in a Category table then then assign each item's category_id based on the id from the just-populated category table, if that makes sense...
class is a reserved keyword in Ruby (it's used for class declarations only), so you can't use it to name your method parameter. Developers often change it to klass, which preserves the original meaning without colliding with this restriction. However, in this case, you'll probably be passing in the name of a class as a string, so I would call it class_name.
Rails' ActiveSupport has a number of built in inflection methods that you can use to turn a string into a constant. Depending on what your CSV data looks like, you might end up with something like this:
def id_find(class_name, to_find)
thing = (class_name.camelize.constantize).find_by_name(to_find)
...
end
If using a string, you can use constantize instead of capitalize and your code should work (in theory):
thing = passed_in_class.constantize.find_by_name(to_find)
But you can also pass the actual class itself to the method, no reason not to:
thing = passed_in_class.find_by_name(to_find)

Getting the name of Ruby method for a literal hash query

In a rails application, I have a number of attributes for a model called Record. I want to design a method that when called on an attribute, returns the name of the attribute (which is essentially a method on the Record object). This name is then passed to an Hash, which returns a number (for the sake of this example, say the number is a percentage which is then multiplied by the original attribute value to get a new value).
For example, say my Record has four attributes: teachers, students, principals, and parents. The method would then look like the following:
def name
**something here**
end
and the corresponding new_value method and PRECENTAGE hash would look like this:
def new_value
self * PERCENTAGE[self.name]
end
PERCENTAGE = {
"teachers" => 0.40,
"students" => 0.53,
"principals" => 0.21,
"parents" => 0.87
}
Then, to execute this whole thing, I would do Record.students.new_value, which would return new number of students according to the percentage obtained in the hash.
I know that to get the name of a method that is currently executing, you can do something like this: (found on http://ryat.la/7RDk)
def this_method
__method__
end
but that won't work for me, because I need the name of the previously executed method.
If you have any suggestions as to an alternative approach to accomplishing my goal, I'd be happy to try something else.
Ryan, I'm struggling to understand your question, but I think this is what you want, for record.teachers_percent, for example:
["teachers", "students", "principals", "parents"].each do |attrib|
Record.class_eval <<-RUBY
def #{attrib}_percent
#{attrib} * PERCENTAGE[#{attrib.inspect}]
end
RUBY
end
Although this is probably a cleaner solution, giving record.percent(:teachers) or record.percent("teachers"):
class Record
def percent(attrib)
self.send(attrib) * PERCENTAGE[attrib.to_s]
end
end

Resources