Flatten a has_many association and get an attribute sum - ruby-on-rails

I would like to understand the most efficient way to flatten some has_many associations, and subsequently get the sum of some attribute on the child associations. Suppose you have the following data structures:
class Restaurant < ActiveRecord::Base
has_one :address
has_many :menu_items
end
class Address < ActiveRecord::Base
belongs_to :restaurant
end
class MenuItem < ActiveRecord::Base
belongs_to :restaurant
end
Say that MenuItem has the attribute "cost", and Address has the attribute "zip_code". What I would like to do is maybe find all restaurants in the zipcode '10101' and get the sum of their menu item's cost attributes. Say that I want to be able to show the average cost of restaurants' offerings in an area.
I think there are quite a few brute force-y ways to do this, but I know there should be something better. For example, in C#/LINQ if I had a similar set of data structures it would be easy to write:
var sum=restaurants.Where(r => r.zip_code==10101).SelectMany(r => r.MenuItems).Sum(mi => mi.Cost);
The best I have come up with, but that feels wrong to me, is:
def thing_I_dont_know_how_to_do
find_zip=Address.where(:zip_code => zip_code)
restaurants=Restaurant.joins(:address, :menu_items).merge(find_zip)
restaurant_ids=restaurants.map(&:id)
sum=MenuItems.sum(:cost, :conditions => {:restaurant_ids => venue_ids})
end
Can anyone help me improve upon this?

You can do it with a single SQL request which selects all the menu items of restaurants in zip code 10101, and sums all their cost:
MenuItem.joins(restaurant: :address).where(addresses: { zip_code: '10101' }).sum('cost')

Related

How to improve has_one association activerecord queries

I've a biography model, and it has one to one association with lifestyle model, one with education model, and one to one with location model.
When application loads I need to get all of this information. I'm doing this by:
biography = current_user.biography
lifestyle = biography.lifestyle
education = biography.education
location = biography.location
result = { "biography" => biography, "lifestyle" => lifestyle, "education" => education, "location" => location}
And then sending the json result back:
render :json => result
So I'm executing total of four queries for this get. Is there a way to issue less queries and get the same result back?
I've tried using joins, but it is only returning columns from one table.
n+1 won't really help here as I'm not looping over the results.
Also, includes hasn't given me the desired results also.
Is there a better alternative?
Here are some of the associations:
class Lifestyle < ActiveRecord::Base
belongs_to :biography
end
class Biography < ActiveRecord::Base
has_one :lifestyle, dependent: :destroy
has_one :education, dependent: :destroy
has_one :location, dependent: :destroy
end
biography = Biography.where(user_id: current_user.id).eager_load(:lifestyle, :education, :location).first
:)
This seems to me like a case for as_json. You could overwrite the default method in your Biography model to include the attributes you need for the biography, the associations you want to include, and the attributes for each of those associations (see http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json). Then, to avoid n+1s, you could do something like:
biography = Biography.includes(:lifestyle, :education, :location).first
render json: biography.as_json

Rails: Display products based on multiple params

I'm learning rails and trying to set up a product library where the products will be displayed based on three elements: location, category and expiry date (products can have multiple locations and categories but just one expiry date). Products will be shown as long as their expiry date hasn't passed and location and category selection will be via dropdown menus.
I started writing this question while having difficulty with incorporating the location and category selection criteria which i found a solution to but any help on what could be done better is greatly appreciated.
I've used has_many through connections to create the connections between the products, location and categories.
Here's the models:
class Product < ActiveRecord::Base
has_many :categorizations
has_many :categories, :through => :categorizations
has_many :localizations
has_many :locations, :through => :localizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, :through => :categorizations
end
class Localization < ActiveRecord::Base
belongs_to :product
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :localizations
has_many :products, :through => :localizations
end
Here's my controller. Location & category ID's are passed as params and the expiry date of the products must be greater than the current time:
class LibraryController < ApplicationController
def index
#products = Product.find(:all, include => [ :locations, :categories ],
:conditions => ['expiry_date > ? AND locations.id = ? AND categories.id = ?',
Time.now, params[:location_id],params[:category_id]])
end
end
So by passing the location_id and category_id params in the URL I can list products by a combination of both.
Is there a better way of achieving what I'm trying to do?
This will also do what you want:
#products = Product.find_all_by_category_id_and_location_id(params[:category_id], params[:location_id])
You can also user Product.where which is supposedly better than find.
For more information, Google "dynamic finders".
Ok. No, I don't think there is a "better" way in this case. There certainly are "different" ways of doing what you want, but on the face of it, what you're doing is fine, and it doesn't scream out "this code is terrible!" or anything.
Questions of advice/style are tough to answer here, because ultimately the answer to them is, "search the web for what other people are doing in your situation, and evaluate/make the decision yourself if your solution seems conventional/logical," or these kinds of questions are answered via study of relevant books on the topic.
It's nearly impossible to answer a qualitative question like this, because:
There's several ways to solve every problem, many of which are neither "right" or "wrong"
There's always edge cases where people break the "rules", in which case even unconventional solutions can absolutely be the best way to do something
You're the developer, the one building the thing. To some extent you're expected to take a leadership role, and decide what's best
The reason I ask you to define "better" is primarily because of #1 - unless you give us a specific outcome you're trying to achieve, all you'll get are (a) answers that are full of opinion, and not directed toward a specific goal or (b) simply a different way of doing something which may or may not help you. Therefore, they aren't very useful in practical terms.
You could also improve upon your solution by using, "Product.where" (preferred over find in rails 3.1) and also turn them into named_scopes in Rails like and chain them as required.
scope :not_expired, where('expiry_date > ?', Time.now)

Rails app using STI -- easiest way to pull these records?

I'm learning my way around Rails and am working on a sample app to keep track of beer recipes.
I have a model called Recipe which holds the recipe name and efficiency.
I have a model called Ingredient which is using STI - this is subclassed into Malt, Hop, and Yeast.
Finally, to link the recipes and ingredients, I am using a join table called rec_items which holds the recipe_id, ingredient_id, and info particular to that recipe/ingredient combo, such as amount and boil time.
Everything seems to be working well - I can find all my malts by using Malt.all, and all ingredients by using Ingredient.all. I can find a recipe's ingredients using #recipe.ingredients, etc...
However, I'm working on my recipe's show view now, and am confused as to the best way to accomplish the below:
I want to display the recipe name and associated info, and then list the ingredients, but separated by ingredient type. So, if I have a Black IPA # 85% efficiency and it has 5 malts and 3 hops varieties, the output would be similar to:
BLACK IPA (85%)
Ingredient List
MALTS:
malt 1
malt 2
...
HOPS:
hop 1
...
Now, I can pull #recipe.rec_items and iterate through them, testing each rec_item.ingredient for type == "Malt", then do the same for the hops, but that doesn't seem very Rails-y nor efficient. So what is the best way to do this? I can use #recipe.ingredients.all to pull all the ingredients, but can't use #recipe.malts.all or #recipe.hops.all to pull just those types.
Is there a different syntax I should be using? Should I using #recipe.ingredient.find_by_type("Malt")? Doing this in the controller and passing the collection to the view, or doing it right in the view? Do I need to specify the has_many relationship in my Hop and Malt models as well?
I can get it working the way I want using conditional statements or find_by_type, but my emphasis is on doing this "the Rails way" with as little DB overhead as possible.
Thanks for the help!
Current bare-bones code:
Recipe.rb
class Recipe < ActiveRecord::Base
has_many :rec_items
has_many :ingredients, :through => :rec_items
end
Ingredient.rb
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
end
Malt.rb
class Malt < Ingredient
end
Hop.rb
class Hop < Ingredient
end
RecItem.rb
class RecItem < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingredient
end
recipes_controller.rb
class RecipesController < ApplicationController
def show
#recipe = Recipe.find(params[:id])
end
def index
#recipes = Recipe.all
end
end
Updated to add
I'm now unable to access the join table attributes, so I posted a new question:
Rails - using group_by and has_many :through and trying to access join table attributes
If anyone can help with that, I'd appreciate it!!
It's been a while since I've used STI, having been burned a time or two. So I may be skipping over some STI-fu that would make this easier. That said...
There are many ways of doing this. First, you could make a scope for each of malt, hops, and yeast.
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
named_scope :malt, :conditions => {:type => 'Malt'}
named_scope :hops, :conditions => {:type => 'Hops'}
...
end
This will allow you to do something line:
malts = #recipe.ingredients.malt
hops = #recipe.ingedients.hops
While this is convenient, it isn't the most efficient thing to do, database-wise. We'd have to do three queries to get all three types.
So if we're not talking a ton of ingredients per recipe, it'll probably be better to just pull in all #recipe.ingredients, then group them with something like:
ingredients = #recipe.ingredients.group_by(&:type)
This will perform one query and then group them into a hash in ruby memory. The hash will be keyed off of type and look something like:
{"Malt" => [first_malt, second_malt],
"Hops" => [first_hops],
"Yeast" => [etc]
}
You can then refer to that collection to display the items however you wish.
ingredients["Malt"].each {|malt| malt.foo }
You can use group_by here.
recipe.ingredients.group_by {|i| i.type}.each do |type, ingredients|
puts type
ingredients.each do |ingredient|
puts ingredient.inspect
end
end
The utility of STI in this instance is dubious. You might be better off with a straight-forward categorization:
class Ingredient < ActiveRecord::Base
belongs_to :ingredient_type
has_many :rec_items
has_many :recipes, :through => :rec_items
end
The IngredientType defines your various types and ends up being a numerical constant from that point forward.
When you're trying to display a list this becomes easier. I usually prefer to pull out the intermediate records directly, then join out as required:
RecItem.sort('recipe_id, ingredient_type_id').includes(:recipe, :ingredient).all
Something like that gives you the flexibility to sort and group as required. You can adjust the sort conditions to get the right ordering. This might also work with STI if you sort on the type column.

Thinking Sphinx and searching multiple models

I'm looking for a way to perform a search against multiple models (see this post), and got a couple of answers saying that Thinking Sphinx would be a good match for this kind of thing.
Indeed, it looks sweet, and it seems the application-wide search capability (ThinkingSphinx.search) is close to what I want. But the docs state this will return various kinds of model objects, depending on where a match was found.
I have a models somewhat like this:
Employee
Company
Municipality
County
Where employees are linked to County only though Company, which in turn is linked to a Municipality, which in turn is linked to the actual County.
Now as a result from my search, I really only want Employee objects. For example a search for the string "joe tulsa" should return all Employees where both words can be found somewhere in the named models. I'll get some false positives, but at least I should get every employee named "Joe" in Tulsa county.
Is this something that can be achieved with built in functionality of Thinking Sphinx?
I think what you should do in this case is define associations for your Employee model (which you probably have already), e.g.:
class Employee < ActiveRecord::Base
...
belongs_to :company
has_one :municipality, :through => :company
has_one :county, :through => :company
...
end
class Company < ActiveRecord::Base
...
belongs_to :municipality
has_many :employees
has_one :county, :through => :municipality
...
end
class Municipality < ActiveRecord::Base
...
belongs_to :county
has_many :companies
...
end
class County < ActiveRecord::Base
...
has_many :municipalities
...
end
Edit: I tested the multi-level has_one relationship, and it doesn't work like that. Seems to be fairly complex to model these 4 layers without denormalizing. I'll update if I come up with something. In any case, if you denormalize (i.e. add redundant foreign IDs to all models to your employees table), the associations are straightforward and you massively increase your index generation time. At the same time, it may involve more work to insure consistency.
Once the associations are set up, you can define the Sphinx index in your Employee model, like this:
define_index do
indexes :name, :sortable => :true
indexes company(:name), :as => :company
indexes municipality(:name), :as => :municipality
indexes county(:name), :as => :county
...
end
That way the columns in your associations are indexed as well, and Sphinx will automatically join all those tables together when building the index.

Rails - data view from has_many and Has_many_and_belongs_to_many

I'm trying to implement this project:
http://img7.imagebanana.com/img/cnb46ti2/relationships.png
I want to let view the skills of an employee on the employee's show page
An employee has a position, and every position has skills which an employee of this position needs to know
so if I understand right, positions and skills have an n:m relationship, and they need a join table for a has_many_and_belongs_to_many relationship. Because a position includes many skills and every skill belongs to many positions.
now my questions
the position_skill-table -> is it better to use a has_and_belongs_to_many relationship, so this table has no own id or is it better to use a has_many :through relationship? I guess it's better do use a has_and_belongs_to_many relationship, because this relationship table will not have any further information inside than just the two keys. Am I right?
if I take a has_and_belongs_to_many - relationship, is that the only thing I need to write into the models?
a) class Position < ActiveRecord :: Base (...) has_and_belongs_to_many :skills (...)
b) class Skill < ActiveRecord :: Base (...) has_and_belongs_to_many :positions (...)
c) into db\migrate def self.up create_table :positon_skill, :id => false do |t| (...)
and after that, the positions and skills are connected with each other? Is that right? Did I forget something?
if that's right, how can I let the skills view on employee's show page? An employee has 1 position, and this position has several skills... What for code do I need to write into the show.html.erb of employee? Something like <%= employee.position.skill %>? Do I also need to render something? Sorry, I'm very confused and I think I read too much information in web... Or is there any description in web which exactly describes what I need for?
thanks alot in advance and sorry for that redundant question.
If you're sure you aren't going to want to add any information later to the position_skills table, has_and_belongs_to_many will work fine. However, has_many :through is far more flexible if you change your mind later and isn't much harder to set up.
If you use has_and_belongs_to_many, you only need association declarations in the models and the database table with position_id:integer and skill_id:integer fields. Seems like you've got that already.
To be able to access employee.position.skills in your view, you need to eagerly load the employee's associations. Try something like the following:
class EmployeesController < ApplicationController
...
def show
#employee = Employee.find(params[:id], :include => { :position => :skills })
end
...
end
I think that should work if you stick with has_and_belongs_to_many, but if you go for has_many :through (which I recommend), you'll need to use :include => { :position => { :position_skills => :skills } }
This is what it looks like in your diagram. Consider the following:
class Employee < ActiveRecord :: Base
belongs_to :position
...
end
class Position < ActiveRecord :: Base
has_many :employees
has_many :position_skills
has_many :skills, :through => :position_skills
...
end
class PositionSkill < ActiveRecord :: Base
belongs_to :position
belongs_to :skill
...
end
class Skill < ActiveRecord :: Base
has_many :position_skills
has_many :positions, :through => :position_skills
...
end
The only problem with this is that an employee is tied to a single position. While this position has many skills through positions. I would change it to position belongs_to employee and employee has_many positions. This leaves it open to track employees that move from one position to the next. Let me know if you need further info on that.

Resources