Simplify Rails Query - ruby-on-rails

I'm trying to reduce the number of queries in my application and need some help with the following setup:
I have 5 models:
Bet
Choice
Spotprice
Spotarea
Product
They are associated with the following:
Bet belongs_to Choice
Choice belongs_to Spotarea
Choice belongs_to Product
Choice has_many Bets
Spotprice belongs_to Spotarea
Spotprice belongs_to Product
Spotarea has_many Spotprices
Spotarea has_many Choices
Product has_many Sprotprices
Product has_many Choices
My goal is to find the Spotprices that matches a Specific Bet. To do that I uses the following queries, but I'm sure it can be done in a better way, so when I run through 100 bets and want to see if they are above or below the corrosponding Spotprice I don't overload the DB with queries.
a = Bet.find(5)
b = Choice.find(a.choice_id)
c = Spotprice.where(:spotarea_id => b.spotarea_id, :product_id => b.product_id,
:deliverydate => b.deliverydate).first
Thanks!

first of all, set up join bridges:
class Choice
has_many :spotprices, :through => :spotarea
end
class Bet
has_many :spotprices, :through => :choice
end
then you can query things like
Bet.joins(:spotprices).where("spotprices.price > bets.value")

Before trying to decrease the number of queries, you should run a performance test on your app, and monitor the database load. Sometimes it's better to run a few small queries rather than one huge query with a few joins. Certain versions of Oracle seem especially bad at joins.
An alternative to joins, if you're trying to avoid the n+1 query problem, is to use preload and pass the association (preload takes the same arguments as includes). This makes ActiveRecord run one query per table.
Basically:
you always want to avoid the n+1 problem.
trying to combine multiple queries into a join could in the best case be a premature optimization, and in the worst case actually make performance worse.

Well here's one pretty easy change:
b = Bet.includes(:choice).find(5).choice

After a few hours and a lot of Google search I found a solution that works.. After adding the join bridges I wanted to do:
Bet.find(5).spotprice
But that didn't work because to do that I needed something like this in my Choice model:
has_one :spotprice, :through => [:spotarea, :product] :source => :spotprices
I that is not possible.. apperently..
So I found this link has_one :through => multiple and I could use that answer in my situation.
class Choice < ActiveRecord::Base
belongs_to :user
belongs_to :spotarea
belongs_to :product
has_many :bets
def spotprice
Spotprice.where(:product_id => self.product_id, :spotarea_id => self.spotarea_id, :deliverydate => self.deliverydate).first
end
class Bet < ActiveRecord::Base
belongs_to :user
belongs_to :choice
has_one :spotprice, :through => :choice
With the above I can now do:
Bet.find(5).choice.spotprice
If anybody got a better solution please let me know :)

Related

Find records which assoicated records do not belong to certain record

In my system I have a following structure:
class Worker
has_many :worker_memberships
end
class WorkerMembership
belongs_to :worker
belongs_to :event
end
class Event
has_many :worker_memberships
end
Imagine I have a certain #event. How can I find all workers that have NO worker_memberships belonging to this #event?
This is pretty much synthesis of both other answers.
First: stick to has_many through as #TheChamp suggests. You're probably using it already, just forgot to write it, otherwise it just wouldn't work. Well, you've been warned.
I generally do my best to avoid raw SQL in my queries whatsoever. The hint about select I provided above produces a working solution, but does some unneessary stuff, such as join when there's no practical need for it. So, let's avoid poking an association. Not this time.
Here comes the reason why I prefer has_many through to has_and_belongs_to_many in many-to-many associations: we can query the join model itself without raw SQL:
WorkerMembership.select(:worker_id).where(event: #event)
It's not the result yet, but it gets us the list of worker_ids we don't want. Then we just wrap this query into a "give me all but these guys":
Worker.where.not(id: <...> )
So the final query is:
Worker.where.not(id: WorkerMembership.select(:worker_id).where(event: #event) )
And it outputs a single query (on #event with id equal to 1):
SELECT `workers`.* FROM `workers` WHERE (`workers`.`id` NOT IN (SELECT `worker_memberships`.`worker_id` FROM `worker_memberships` WHERE `worker_memberships`.`event_id` = 1))
I also give credit to #apneadiving for his solution and a hint about mysql2's explain. SQLite's explain is horrible! My solution, if I read the explain's result correctly, is as performant as #apneadiving's.
#TheChamp also provided performance costs for all answers' queries. Check out the comments for a comparison.
Since you want to set up a many to many relationship between Worker and Event, I'd suggest you use the through association.
Your resulting models would be.
class Worker
has_many :worker_memberships
has_many :events, :through => :worker_memberships
end
class WorkerMembership
belongs_to :worker
belongs_to :event
end
class Event
has_many :worker_memberships
has_many :workers, :through => :worker_memberships
end
Now you can just call #event.workers to get all the workers associated to the event.
To find all workers that don't belong to the #event you can use:
# get all the id's of workers associated to the event
#worker_ids = #event.workers.select(:id)
# get all workers except the ones belonging to the event
Worker.where.not(:id => #worker_ids)
The one-liner
Worker.where.not(:id => #event.workers.select(:id))
Try this:
Worker.where(WorkerMembership.where("workers.id = worker_memberships.worker_id").where("worker_memberships.event_i = ?", #event.id).exists.not)
Or shorter and reusable:
class WorkerMembership
belongs_to :worker
belongs_to :event
scope :event, ->(event){ where(event_id: event.id) }
end
Worker.where(WorkerMembership.where("workers.id = worker_memberships.worker_id").event(#event.id).exists.not)
(I assumed table and column names from conventions)

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)

eager loading a small subset of all has_many objects based on conditions

How do I eager-load only some of the objects in a has_many relationship?
Basically, I have four objects: Assignment, Problem, AssignmentUser, and ProblemUser.
#assignment.rb
has_many :problems
has_many :assignment_users
#assignment_user.rb
belongs_to :assignment
belongs_to :user
has_many :problem_users
#problem.rb
belongs_to :assignment
has_many :problem_users
#problem_user.rb
belongs_to :user
belongs_to :problem
belongs_to :assignment_user
attr_accessor :complete #boolean
On a page showing a single assignment, I want to show all of the problems, as well the user's status on each problem, if it exists. (It might not, if this is the first time the user is viewing the page.)
I can't call assignment_user.problem_users and then snake the problems out like so:
-#assignment_user.problem_users.each do |problem_user|
= check_box_tag "problems[#{problem_user.problem.id}]", 1, problem_user.complete
= link_to problem_user.problem.name, assignment_problem_path(assignment_id => problem_user.problem.assignment_id, :id => problem_user.problem_id)
because there might not be ProblemUser entries for every Problem that belongs to an assignment; creating all of those ProblemUser objects whenever someone creates a Problem object would be wasteful, so they're only created on the fly.
What I want is to be able to iterate over the Problems that belong to the particular Assignment, then for each Problem, find a ProblemUser that matches...but without creating an N+1 problem. I could create two arrays, one with all of the problems and one with all of the problem_users, but then I would have to match them up, right? Maybe that's the best way... but any recommendations on best practices are appreciated here.
Try using :include something along the lines of...
#assignment.rb
has_many :problems, :include => :problem_user
has_many :assignment_users
Assuming a field named description in each of the tables assignments, problems, and problem_users the solution should resemble this...
Assignment.find(1).problems.collect { |a| [a.assignment.description, a.description, a.problem_user.description] }

Should I denormalize a has_many has_many?

I have this:
class User < ActiveRecord::Base
has_many :serials
has_many :sites, :through => :series
end
class Serial < ActiveRecord::Base
belongs_to :user
belongs_to :site
has_many :episodes
end
class Site < ActiveRecord::Base
has_many :serials
has_many :users, :through => :serials
end
class Episode < ActiveRecord::Base
belongs_to :serial
end
I would like to do some operations on User.serials.episodes but I know this would mean all sorts of clever tricks. I could in theory just put all the episode data into serial (denormalize) and then group_by Site when needed.
If I have a lot of episodes that I need to query on would this be a bad idea?
thanks
I wouldn't bother denormalizing.
If you need to look at counts, you can check out counter_cache on the relationship to save querying for that.
Do you have proper indexes on your foreign keys? If so, pulling the data from one extra join shouldn't be that big of a deal, but you might need to drop down to SQL to get all the results in one query without iterating over .serials:
User.serials.collect { |s| s.episodes }.uniq # ack! this could be bad
It really depends on the scale you are needing out of this application. If the app isn't going to need to serve tons and tons of people then go for it. If you are getting a lot of benefit from the active record associations then go ahead and use them. As your application scales you may find yourself replacing specific instances of the association use with a more direct approach to handle your traffic load though.

How do I use ActiveRecord to find unrelated records?

I have a many-to-many relationship set up through a join model. Essentially, I allow people to express interests in activities.
class Activity < ActiveRecord::Base
has_many :personal_interests
has_many :people, :through => :personal_interests
end
class Person < ActiveRecord::Base
has_many :personal_interests
has_many :activities, :through => :personal_interests
end
class PersonalInterest < ActiveRecord::Base
belongs_to :person
belongs_to :activity
end
I now want to find out: in which activities has a particular user not expressed interest? This must include activities that have other people interested as well as activities with exactly zero people interested.
A successful (but inefficent) method were two separate queries:
(Activity.all - this_person.interests).first
How can I neatly express this query in ActiveRecord? Is there a (reliable, well-kept) plugin that abstracts the queries?
I think the easiest way will be to just use an SQL where clause fragment via the :conditions parameter.
For example:
Activity.all(:conditions => ['not exists (select 1 from personal_interests where person_id = ? and activity_id = activities.id)', this_person.id])
Totally untested, and probably doesn't work exactly right, but you get the idea.

Resources