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.
Related
I am having some issues when trying to skip a default_scope when doing an ActiveRecord join.
Even though the codebase is quite large, I am just showing the basics as I think it shows pretty well what the problem is:
class Client
belongs_to :company
end
class Company
default_scope { where(version_id: nil) }
end
I am building a complex report, so I need to join multiple tables and filter on them. However, I can't successfully skip the default scope when fetching Clients.
Client.joins(:company).to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
As you can see that is automatically including the Company default_scope. So I tried this:
Company.unscoped { Client.joins(:company) }.to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
Again, I got the same result, even when using unscoped with the block.
Then I decided to add a new association to the model, with the unscoped scope:
class Client
belongs_to :company
belongs_to :unscoped_company, -> { unscoped }, foreign_key: :company_id, class_name: "Company"
end
Having added that, I gave another try:
Client.joins(:unscoped_company).to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
And still the scoped is being applied.
Do you know how can I successfully join both tables without applying that default_scope?
Removing that default_scope is not an option as It is a big application and changing that will require too much time.
Rails v4.2.7
Ruby v2.2.3
I did some research without finding any straight solution.
Here a couple of workarounds. I cannot say if they're going to work in your chained joins.
First basic, do it manually:
Client.joins("INNER JOINS companies ON companies.id = clients.company_id").to_sql
Other option define a `CompanyUnscoped` class which inherits from `Company`, removing the default_scope:
class CompanyUnscoped < Company
self.default_scopes = []
end
Don't forget to add this line to Client class:
belongs_to :company_unscoped, foreign_key: :company_id
Then you should be able to call
Client.joins(:company_unscoped)
#=> SELECT "clients".* FROM "clients" INNER JOIN "companies" ON "companies"."id" = "clients"."company_id"
Apply directly as a class method
Client.unscoped.joins(:company).to_sql
You might try https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-unscope but I would've expected https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Default/ClassMethods.html#method-i-unscoped to work. You might try it again without a block, letting it come after the undesired scope is applied.
Seems like there isn't any straightforward solution that you are looking for. There are couple of discussion threads for reference.
https://github.com/rails/rails/issues/20679
https://github.com/rails/rails/issues/20011
But I liked the way what #iGian has suggested.
You can do it manually:
Client.joins('inner join company on companies.id = clients.company_id')
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.
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.
I am new to Ruby on Rails, but I have created a few simple apps in the past. Now I am doing something a little more complex and I am stumped on database design.
I am creating a sports league manager and I need some advice on how the relationship between teams and games is modelled to point me in the right direction. Every time a game is played between two teams, the match is recorded. I'd like to be able to do the following:
1) On a specific team's page, I would like to show a list of matches the team has participated in.
2) I would like to keep a record of each team's wins, losses, and ties to show on a league standings page.
On point #1 I figured this would be a many-to-many relationship, that is, a Team has many Matches, and a Match has many Teams (well, just two actually). The point I am a bit stumped on is how and where to store the stats for each team. Where do I keep the wins/losses/ties? Are they part of the Team table? If so, if I was to have a page with team standings showing each teams w/losses/ties, how would I get that information?
This isn't really finished, but maybe this will help you or someone else get the ball rolling here.
I'm focusing on just how to structure the relationship between Teams and Matches. At least part of the solution lies in using a polymorphic association, I believe, and part of it would perhaps be a self join. I swear it's right in front of me and I'm not seeing it.
Taking baby steps here, assuming you have a table like this for your Matches tableā¦
id | home_team_id | away_team_id | home_team_score | away_team_score
You can set that up in your models with these associations:
class Match
belongs_to :home_team, :class_name => :team
belongs_to :away_team, :class_name => :team
end
class Team
has_many :home_matches, :foreign_key => :home_team_id, :class_name => :matches
has_many :away_matches, :foreign_key => :away_team_id, :class_name => :matches
end
The obvious problem there is that there are two relationships when there really should only be one. That's why I think a polymorphic association can help, but this is sort of convoluted.
See the Rails guide on polymorphic associations and see if that helps you see what I can't.
I would suggest against creating a traditional many-to-many relationship here. Instead, you'd have just two tables: Teams and Matches.
Each team would be identified by a row in Teams and would have a unique identifier, such as TeamId.
The Matches table would the following columns:
MatchId - a synthetic primary key
SeasonId - identifies the season the match took place in
HomeTeamId - the home team
VisitngTeamId - the away team
HomeTeamScore
VisitngTeamScore
... Any other statistics you'd want to keep for an individual match
I presume you have the notion of home and visiting teams. If not, you can just name these columns Team1Id and Team2Id, or something along those lines.
The point I am a bit stumped on is how and where to store the stats for each team. Where do I keep the wins/losses/ties?
The wins, losses, and ties are implicit in the Matches table - you can query that to get back a team's record. For instance, the following query returns the wins, loses, and ties for team X:
-- Wins
SELECT COUNT(*)
FROM Matches
WHERE SeasonID = #SeasonID AND
(HomeTeamId = X AND HomeTeamScore > VisitingTeamScore) OR
(VisitingTeamId = X AND VisitingTeamScore > HomeTeamScore)
-- Loses
SELECT COUNT(*)
FROM Matches
WHERE SeasonID = #SeasonID AND
(HomeTeamId = X AND HomeTeamScore < VisitingTeamScore) OR
(VisitingTeamId = X AND VisitingTeamScore < HomeTeamScore)
-- Ties
SELECT COUNT(*)
FROM Matches
WHERE SeasonID = #SeasonID AND
(HomeTeamId = X OR VisitingTeamId = X)
AND VisitingTeamScore = HomeTeamScore
Even if you wanted to denormalize the data model and store this information for each team, you wouldn't want to do it in the Teams table because you may want to know how many wins/losses/ties a team has for a given season. (I presume a team may stick together through multiple seasons. If this is not the case, disregard.)
I'm going off-the-cuff here, but consider:
tblTeam
TeamID
TeamName
. . . OtherTeamFields
tblMatch
MatchID
MatchDate
MatchLocationID
. . . OtherMatchFields
tblTeam_Matches
TeamID FK on tblTeam.TeamID
MatchID FK on tblMatchID
TeamStanding (Win, Loss, Tie)
The structure above has some pros and cons. On the pro side, the outcome for each team involved in a match is stored properly with the team's relationship to that match. One can retrieve the results for each team through a range of matches by setting criteria for TeamID and TeamStanding (i.e. "WHERE TeamStanding = "Win").
However, there is a more complex, but probably more scalable and usefull way, where you would define a TeamScore field for tblTeam_Matches. In this case, the Match winner would be determined by a rather difficult series of sub-queries (By difficult, I mean difficult for ME. I am betting there are folks here on SO who could pop a smaple out quickly . . . But it is a bit of a brain-teaser).
I believe the second option would be a more "proper" way to do it, but I messed with puling some version of:
StatsQuery:
TeamName
TotalMatches
Wins
Losses
Ties
I had a difficult time of it. Correlated sub-queries are NOT my strong point (yet).
Anyway, hope that gives you some food for thought . . .
I think this can be done using following way
class Team < ActiveRecord::Base
has_many :games
has_many :matches,:through => :games
end
class Matche < ActiveRecord::Base
has_many :games
has_many :teams,:through => :games
end
class Game < ActiveRecord::Base
belongs_to :team
belongs_to :match
end
and if you need to find total matches played by any team then
= team.games.count
if you need to find total won matches by any team then
= team.games.where(:winner => true).count
The business logic is this: Users are in a Boat through a join table, I guess let's call that model a Ticket. But when a User instance wants to check who else is on the boat, there's a condition that asks if that user has permission see everyone on the Boat, or just certain people on the Boat. If a User can see everyone, the normal deal is fine: some_user.boats.first.users returns all users with a ticket for that boat. But for some users, the only people that are on the boat (as far as they're concerned) are people in, let's say the dining room. So if User's ticket is "tagged" (using an acts_as_taggable style system) with "Dining Room", the only Users returned from some_user.boats.first.users should be Users with tickets tagged "Dining Room".
Just for the record, I'm not trying to design something to be insane from the getgo - I'm trying to wedge this arbitrary grouping into a (mostly) existent system.
So we've got:
class User
has_many :tickets
has_many :boats, :through => :tickets
end
class Ticket
belongs_to :user
belongs_to :boat
end
class Boat
has_many :tickets
has_many :users, :through => :tickets
end
Initially, I thought that I could conditionally modify the virtual class like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
has_many :tickets, :include => :tags, :conditions => ['tags.id in (?)', [#{tag_ids.to_s(:db)}]]
code
)
That gets all the way down to generating the SQL, but when generated, it generates SQL ending in:
LEFT OUTER JOIN "tags" ON ("tags"."id" = "taggings"."tag_id") WHERE ("tickets"._id = 1069416589 AND (tags.id in (5001,4502)))
I've tried digging around the ActiveRecord code, but I can't find anywhere that would prefix that 'id' in the SQL above with an underscore. I know that associations are loaded when an ActiveRecord class is loaded, and I'd assume the same with a singleton class. shrug.
I also used an alias_method_chain like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
def tickets_with_tag_filtering
tags = Tag.find(etc, etc)
tickets_without_tag_filtering.scoped(:include => :tags, :conditions => {:'tags.id' => tags})
end
alias_method_chain :tickets, :tag_filtering
code
)
But while that approach produces the desired Tickets, any joins on those tickets use the conditions in the class, not the virtual class. some_user.boats.first.users returns all users.
Any type of comment will be appreciated, especially if I'm barking up the wrong tree with this approach. Thanks!
So a wild guess about your underscore issue is that Rails is generating the assocation code based on the context at the time of evaluation. Being in a singleton class could mess this up, like so:
"#{owner.table_name}.#{association.class.name}_id = #{association.id}"
You could get in there and define a class name property on your singleton class and see if that fixes the issue.
On the whole I don't recommend this. It creates behavior that is agonizing to track down and impossible to extend effectively. It creates a landmine in the codebase that will wound you or someone you love at a later time.
Instead, consider using a named_scope declaration:
class User
has_many :taggings, :through => :tickets
named_scope :visible_to, lambda { |looking_user|
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
}
end
While you may have to go back and edit some code, this is much more flexible in the ways it can be used:
Boat.last.users.visible_to( current_user )
It's clear that a restriction is being placed on the find, and what the purpose of that restriction is. Because the conditions are dynamically calculated at runtime, you can deal with the next weird modification your client hits you with. Say some of their users have xray vision and clairvoyance:
class User
named_scope :visible_to, lambda { |looking_user|
if looking_user.superhuman?
{}
else
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
end
}
end
By returning an empty hash, you can effectively nullify the effect of the scope.
Why not just grab all users on the boat and include their tags.
Then run a quick filter to include & return only the users with the same tag as the inquiring user.
What version of Rails are you using? Have you tried upgrading to see if the underscore issue is fixed? It's like it can't find the foreign key to put in as "tag_id" or somethin'.
My ruby-fu is limited, so I'm not sure how to dynamically include the correct method options at run-time.
Just to help you clarify, you have to worry about this two places. You want to filter a user's viewable users so they only see users with the same tags. Your structure is:
user <--> tickets <--> boats <--> tickets <--> users
... right?
So, you need to filter both sets of tickets down to the ones with the current_user's tags.
Maybe you just need a current_user.viewable_users() method and then filter everything through that? I'm not sure what existing functionality you've got to preserve.
Blech, I don't feel like I'm helping you at all. Sorry.
Your approach is the problem. I know it seems expedient at the moment to hack something in where you don't have to refactor the existing call sites, but I believe given time this will come back to haunt you as the source of bugs and complexity.
Sleeping dogs that lie come back to bite you hard, in my experience. Typically in the form of a future developer who doesn't know your association is "magic" and uses it assuming it's just pail ole rails. He/she likely won't even have a reason to write a test case that would expose the behavior either, which raises the odds you'll only find out about the bug when it's live in production and the client is unhappy. Is it really worth the time you're saving now?
Austinfrombostin is pointing the way. Different semantics? Different names. Rule number one is always to write code that says what it does as clearly as possible. Anything else is the path of madness.