Ruby - less expensive way for chained ActiveRecord queries - ruby-on-rails

What I have is currently working, but seems to be very expensive, any ideas on making it less expensive would be great!
A User has many Plans, which has many PlanDates. Each PlanDates has a certain recipe denoted by a recipe_id attribute. Each Plan has a meal_type attribute which is either Meat, Vegetarian, or Choice, the latter means mixed. Each Recipe has a type_of_meal attribute that is either Meat or Vegetarian. Each Recipe also has a friendly name attribute.
For a given PlanDate, I need to build an options_for_select in the following format:
[ [recipe_id, "recipe_name"], [recipe_id, "recipe_name"] ... ]
The options:
must remove all the recipe_ids that have previously been given to the User (regardless of Plan)
must remove all the recipe_ids with a type mismatch (i.e., if a Plan has Meat designated, the options must not have any Vegetarian recipe_ids), certainly this is not true if the Plan has Choice designated
Here's the code I currently have:
# builds an array of all the recipe_ids that have been given to this User on some PlanDate on some Plan
recipes_used_before_for_this_user = PlanDate.select { |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
# narrows down the world of recipes to those that do NOT have an id of a recipe_used_before_for_this_user
recipes_not_used_before = Recipe.select { |r| (recipes_used_before_for_this_user.include? r.id) == false }
# going forward, let's assume current_pd = the PlanDate object in question
if current_pd.plan.meal_type == "Choice"
# easiest: if the meal_type is "Choice" then we just take the recipes_not_used_before and map them into the appropriate format
recipe_choices_array = recipes_not_used_before.map { |r| [ r.id, r.name ] }
else
# if the plan has a "Meat" or "Vegetarian" specification, we need to first narrow the recipes_not_used_before down by the right type and then map into the appropriate format
recipe_choices_array = recipes_not_used_before.select { |r| r.type_of_meal == potential_pd.first.plan.meal_type }.map { |r| [ r.id, r.name ] }
end
Again, working, but I have a lot of PlanDates and a lot of Recipes, so if there is any way to streamline even further, would love your ideas. Thanks!

The reason you're experiencing expensive queries is because you're not actually using ActiveRecord's query interface, or even SQL to narrow your query, but instead are loading the entire dataset into Ruby memory objects and then looping over the result in Ruby.
I suspect that if you inspect your logfiles you'll see something like this:
>> PlanDate.select{ |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
PlanDate Load (1.3ms) SELECT "plan_dates".* FROM "plan_dates"
=> [#<PlanDate....
What you want to do is to use ActiveRecord's query interface to build the query, something like this:
PlanDate.includes(plan: [:user]).where("plan.user_id == ?", :user_id).pluck('recipe_id')
What that does is first: Specify relationships to be included in the result set, then specify the where conditions of your SQL query, and finally pull out the recipe ids using pluck.
See http://guides.rubyonrails.org/active_record_querying.html for more info.

Related

Rails: Dynamically selecting an attribute for comparison

I have a database of different food menus that I am trying to search through. In general everything works fine, but I think that there must be a cleverer way in writing the code compared to what I am doing now.
Every menu has a set of boolean attributes describing the kind of kitchen (e.g. cuisine_thai, cuisine_italian, etc.). In my view I have a dropdown allowing the user to select the type of food he wants and then I am passing the param on and save it in my search-object.
#search.cuisine_type = params[:cuisine_type]
I then continue to check for the different kitchen types and see if there is a match.
#Filter for Thai cuisine
if(#search.cuisine_type == "cuisine_thai")
#menus = #menus.select{ |menu| menu.cuisine_thai == true}
end
#Filter for French cuisine
if(#search.cuisine_type == "cuisine_italian")
#menus = #menus.select{ |menu| menu.cuisine_italian == true}
end
#Filter for Peruvian cuisine
if(#search.cuisine_type == "cuisine_peruvian")
#menus = #menus.select{ |menu| menu.cuisine_peruvian == true}
end
Eventhough the way I do it works, there must be a better way to do this. I feel that the value stored in #search.cuisine_type could just determine the attribute I check on #menus.
Any help on this is greatly appreciated. Thanks!
Yes, your intuition is correct!
I'll assume #menus is an array are ActiveRecord Menu objects, and that the cuisine_* attributes correspond to database columns. In this case you can use ActiveRecord's attributes method.
Every ActiveRecord object has an attributes property. The docs (Rails 4.2.1) say:
attributes()
Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
To verify this is the case, if you peek at the attributes of a given menu, you should see a hash containing:
{
"cuisine_italian" => true,
"cuisine_thai" => false,
# etc.
}
Also, as a minor point of code readability, these two statements are effectively the same:
#menus = #menus.select { ... }
#menus.select! { ... }
Therefore, your search can be rewritten as:
if #search.cuisine_type.present?
#menus.select! { |m| m.attributes[#search.cuisine_type] }
end
Wouldn't it be better if you had column "cuisine" in database and have it set to thai, italian and so on?
Then you'd only check if certain food matches array of kitchens selected by the user.

Rails find with a block

I have seen Rails find method taking a block as
Consumer.find do |c|
c.id == 3
end
Which is similar to Consumer.find(3).
What are some of the use cases where we can actually use block for a find ?
It's a shortcut for .to_a.find { ... }. Here's the method's source code:
def find(*args)
if block_given?
to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
end
If you pass a block, it calls .to_a (loading all records) and invokes Enumerable#find on the array.
In other words, it allows you to use Enumerable#find on a ActiveRecord::Relation. This can be useful if your condition can't be expressed or evaluated in SQL, e.g. querying serialized attributes:
Consumer.find { |c| c.preferences[:foo] == :bar }
To avoid confusion, I'd prefer the more explicit version, though:
Consumer.all.to_a.find { |c| c.preferences[:foo] == :bar }
The result may be similar, but the SQL query is not similar to Consumer.find(3)
It is fetching all the consumers and then filtering based on the block. I cant think of a use case where this might be useful
Here is a sample query in the console
consumer = Consumer.find {|c|c.id == 2}
# Consumer Load (0.3ms) SELECT `consumers`.* FROM `consumers`
# => #<Consumer id: 2, name: "xyz", ..>
A good example of a use-case is if you have a JSON/JSONB column and don't want to get involved in the more complex JSON SQL.
required_item = item_collection.find do |item|
item.jsondata['json_array_property'][index]['property'] == clause
end
This is useful if you can constrain the scope of the item_collection to a date-range, for example, and have a smaller set of items that require filtering further.

Rail3 'Return False Unless XYZ' Query Not Working

In my rails3.1 application, I'm trying to apply the following logic in one of my order model.
def digital?
line_items.map { |line_item| return false unless line_item.variant_id = '102586070' }
end
I've created a separate variant called prepaid_voucher which has id = 102586070. Despite this, the result is false...
Order has many line_items
LineItem belongs to order and variant
Variant has many line_items
Is this the best way to perform such a task and how can I fix?
First of all I think you want a double == here line_item.variant_id = '102586070', then I rather go for something like that (If I understand what you want)
def digital?
line_items.select{|line_item| line_item.variant_id == '102586070'}.any?
end
But it's hard to understand what you really want, what is the expected behavior if the id is not found?

Rails, How to determine if a Nested Record Set Exists

I have the following two models:
Project, has_many ProjectParticipants
ProjectParticipants, belongs_to Project
How can I request to determine given these 5 ProjectParticipants, do they belong to a Project?
Also, it should be strictly those 5, not more or less.
Any thoughts on how to elegantly solve for this type of count?
Assuming participants contain the 5 participants you want to check.
participants.all? {|o| o.project }
This will return true of all participants have a project, otherwise false.
To return the project that was found you can do:
And to see if all participants have the same project:
first_participant = participants.shift
participants.all? {|o| o.project == first_participant.project} unless first_participant.nil?
The good thing about this method is that it short circuits if one of the participant's doesn't have the same project(more efficient).
Edit:
To return the project that they all share, you can do:
first_participant = participants.shift
project_shared = participants.all? {|o| o.project == first_participant.project} and first_particpant.project unless first_participant.nil?
project_shared will be set to the project that they are all share, otherwise it will be to nil/false.
So you can then do:
if project_shared
# do work
else
# they dont share a project!
end
You can compare the properties of the ProjectParticipant records in a group:
participants.length == 5 and participants.all? { |p| p.project_id == project.id }
This validates that your array of participants contains five entries and that all of them have the same project_id assigned. Comparing p.project == project will have the side-effect of loading the same Project five times.
To check if they simply belong to a project, you can do this:
participants.length == 5 and participants.all? { |p| p.project_id? }
That project may be deleted and the reference could be invalid, so you may need to resort to actually fetching it:
participants.length == 5 and participants.all? { |p| p.project }
You can also use the group_by method to see if there's only one project involved:
grouped = participants.group_by(&:project)
!grouped[nil] and grouped.length == 1 and grouped.first[1].length == 5
The group_by method will organize the given array into a hash where the key is specified as a parameter and the value is a list of objects matching. It can be handy for situations like this.

How to apply named_scopes incrementally in Rails

named_scope :with_country, lambad { |country_id| ...}
named_scope :with_language, lambad { |language_id| ...}
named_scope :with_gender, lambad { |gender_id| ...}
if params[:country_id]
Event.with_country(params[:country_id])
elsif params[:langauge_id]
Event.with_state(params[:language_id])
else
......
#so many combinations
end
If I get both country and language then I need to apply both of them. In my real application I have 8 different named_scopes that could be applied depending on the case. How to apply named_scopes incrementally or hold on to named_scopes somewhere and then later apply in one shot.
I tried holding on to values like this
tmp = Event.with_country(1)
but that fires the sql instantly.
I guess I can write something like
if !params[:country_id].blank? && !params[:language_id].blank? && !params[:gender_id].blank?
Event.with_country(params[:country_id]).with_language(..).with_gender
elsif country && language
elsif country && gender
elsif country && gender
.. you see the problem
Actually, the SQL does not fire instantly. Though I haven't bothered to look up how Rails pulls off this magic (though now I'm curious), the query isn't fired until you actually inspect the result set's contents.
So if you run the following in the console:
wc = Event.with_country(Country.first.id);nil # line returns nil, so wc remains uninspected
wc.with_state(State.first.id)
you'll note that no Event query is fired for the first line, whereas one large Event query is fired for the second. As such, you can safely store Event.with_country(params[:country_id]) as a variable and add more scopes to it later, since the query will only be fired at the end.
To confirm that this is true, try the approach I'm describing, and check your server logs to confirm that only one query is being fired on the page itself for events.
Check Anonymous Scopes.
I had to do something similar, having many filters applied in a view. What I did was create named_scopes with conditions:
named_scope :with_filter, lambda{|filter| { :conditions => {:field => filter}} unless filter.blank?}
In the same class there is a method which receives the params from the action and returns the filtered records:
def self.filter(params)
ClassObject
.with_filter(params[:filter1])
.with_filter2(params[:filter2])
end
Like that you can add all the filters using named_scopes and they are used depending on the params that are sent.
I took the idea from here: http://www.idolhands.com/ruby-on-rails/guides-tips-and-tutorials/add-filters-to-views-using-named-scopes-in-rails
Event.with_country(params[:country_id]).with_state(params[:language_id])
will work and won't fire the SQL until the end (if you try it in the console, it'll happen right away because the console will call to_s on the results. IRL the SQL won't fire until the end).
I suspect you also need to be sure each named_scope tests the existence of what is passed in:
named_scope :with_country, lambda { |country_id| country_id.nil? ? {} : {:conditions=>...} }
This will be easy with Rails 3:
products = Product.where("price = 100").limit(5) # No query executed yet
products = products.order("created_at DESC") # Adding to the query, still no execution
products.each { |product| puts product.price } # That's when the SQL query is actually fired
class Product < ActiveRecord::Base
named_scope :pricey, where("price > 100")
named_scope :latest, order("created_at DESC").limit(10)
end
The short answer is to simply shift the scope as required, narrowing it down depending on what parameters are present:
scope = Example
# Only apply to parameters that are present and not empty
if (!params[:foo].blank?)
scope = scope.with_foo(params[:foo])
end
if (!params[:bar].blank?)
scope = scope.with_bar(params[:bar])
end
results = scope.all
A better approach would be to use something like Searchlogic (http://github.com/binarylogic/searchlogic) which encapsulates all of this for you.

Resources