Ruby multiple joins - ruby-on-rails

Here are the models I created in my Rails app:
class Pet < ActiveRecord::Base
belongs_to :shelter
belongs_to :type
end
class Shelter < ActiveRecord::Base
has_many :pets
end
class Type < ActiveRecord::Base
has_many :pets
end
I'm trying to find shelters that don't have any exotic pets in them but am stuck joining the tables in the way where I can retrieve that information! Here is my latest attempt where I believe I'm at least reaching the Types table. Any help and explanation on joins would be much appreciated!
Shelter.joins(:pet => :type).where(:types => {exotic => false})

I believe it is impossible to get the results you want using just JOINS. Instead you need to find which shelters do have exotic pets and then negate that.
One way to accomplish that is through a subquery:
Shelter.where(<<~SQL)
NOT EXISTS (
SELECT 1 FROM pets
INNER JOIN types ON types.id = pets.type_id
WHERE shelters.id = pets.shelter_id
AND types.exotic IS TRUE
)
SQL
Of course that involves a lot of explicit SQL, something I don't mind, but others do not like it.
You can also do something similar using just the ActiveRecord query interface.
shelters_with_exotics = Shelter.joins(pets: :type).where(types: { exotic: true })
Shelter.where.not(id: shelters_with_exotics)
NOTE: The queries for the two examples are different. If it mattered you would need to benchmark both of them to determine which one performed best.

Related

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Condition for association Rails 4

There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.

Mongoid: How do I query for all object where the number of has_many object are > 0

I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)

ActiveRecord select through multiple joins

I'm working on a Rails-based web service that provides data about various sports team schedules. I have models that included the following:
Each Game record has a "home" and an "away" team, each of which references a Team record
Each Team record belongs to a Division record
I have modeled Game as follows:
class Game < ActiveRecord::Base
# Miscellaneous validations here
belongs_to :home_team, :class_name => Team
belongs_to :away_team, :class_name => Team
# Other stuff follows
end
Here is the model for Team:
class Team < ActiveRecord::Base
# Miscellaneous validations here
belongs_to :division
# Other stuff follows
end
And here is the model for Division:
class Division < ActiveRecord::Base
# Miscellaneous validations here
has_many :teams, :dependent => :destroy
# Other stuff follows
end
I am trying to implement a request to return all games where the home team and the away team are both from a specific division. In pseudo-code , I want something like:
SELECT games.* FROM games WHERE
"The division ID of the home team" = '1' AND
"The division ID of the away team" = '1'
I've tried various incarnation using the joins method, none of which have worked for me. The closest I've come is this:
games = Game.joins(:home_team, :away_team).where(
:home_team => {:division_id => params[:division_id]},
:away_team => {:division_id => params[:division_id]})
but this gives me an error:
SQLite3::SQLException: no such column: home_team.division_id: SELECT "games".* FROM "games" INNER JOIN "teams" ON "teams"."id" = "games"."home_team_id" INNER JOIN "teams" "away_teams_games" ON "away_teams_games"."id" = "games"."away_team_id" WHERE "home_team"."division_id" = '1' AND "away_team"."division_id" = '1'
Clearly, my syntax for specific home_team and away_team isn't working because it's not mapping them to a valid table name of "teams". But any other variant of the join I come up with seems to get me even further from what I want.
I'd appreciate any help you can provide or references to documentation that shows me how to do this kind of thing.
While the answer provided by #tsherif did the trick for me, I wanted to also share an alternate approach I figured out based on info I found elsewhere.
It turns out that ActiveRecord implements its own rules for table aliasing when you reference the same table twice in a joins. This aliasing is described in the Table Aliasing section of this link. Based on the information, I was able to determine that the second association listed in my join (:away_team) was being aliased as away_teams_games. With this table alias in mind I was able to get things working using this:
games = Game.joins(:home_team, :away_team).where(
:teams => {:division_id => params[:division_id]},
:away_teams_games => {:division_id => params[:division_id]})
While this wasn't completely obvious to me when I first looked at it, now that I see what is happening it makes some sense.
I think you might be able to try something like this:
Game.where(["(SELECT COUNT(DISTINCT teams.id) FROM teams WHERE teams.division_id = ? AND (teams.id=games.home_team_id OR teams.id=games.away_team_id)) = 2", params[:division_id]])
You have the nested query, which is a little annoying, but it lets you avoid having to join twice against the teams table.

Querying a polymorphic association

I have a polymorphic association like this -
class Image < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Page < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Site < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Approval < ActiveRecord::Base
belongs_to :approvable, :polymorphic => true
end
I need to find approvals where approval.apporvable.deleted = false
I have tried something like this -
#approvals = Approval.find(:all,
:include => [:approvable],
:conditions => [":approvable.deleted = ?", false ])
This gives "Can not eagerly load the polymorphic association :approvable" error
How can the condition be given correctly so that I get a result set with approvals who's approvable item is not deleted ?
Thanks for any help in advance
This is not possible, since all "approvables" reside in different tables. Instead you will have to fetch all approvals, and then use the normal array methods.
#approvals = Approval.all.select { |approval| !approval.approvable.deleted? }
What your asking, in terms of SQL, is projecting data from different tables for different rows in the resultset. It is not possible to my knowledge.
So you'll have to be content with:
#approvals = Approval.all.reject{|a| a.approvable.deleted? }
# I assume you have a deleted? method in all the approvables
I would recommend either of the answers already presented here (they are the same thing) but I would also recommend putting that deleted flag into the Approval model if you really care to do it all in a single query.
With a polymorphic relationship rails can use eager fetching on the polys, but you can't join to them because yet again, the relationships are not known so the query is actually multiple queried intersected.
So in the end if you REALLY need to, drop into sql and intersect all the possible joins you can do to all the types of approvables in a single query, but you will have to do lots of joining manually. (manually meaning not using rails' built-in mechanisms...)
thanks for your answers
I was pretty sure that this couldn't be done. I wanted some more confirmation
besides that I was hoping for some other soln than looping thru the result set
to avoid performance related issues later
Although for the time being both reject/select are fine but in the long run I
will have to do those sql joins manually.
Thanks again for your help!!
M

Resources