6

I'm having a hard time getting a list of the games involved from a hierarchical parent relationship when multiple foreign keys are implemented on a relationship in the middle.

Given League Object NFC, find all of its Game objects [G1,G3,G4]

#  id           :integer          not null, primary key
#  name         :string
class League
  has_many :teams
  # has_many :games, :through => :teams (Is there some way to do this?)
end

#  id         :integer          not null, primary key
#  team_name    :string
#  league_id :integer
class Team
  belongs_to :league
  has_many :home_games, :foreign_key => team_a_id, :source => :game
  has_many :away_games, :foreign_key => team_b_id, :source => :game
end

#  id                   :integer          not null, primary key
#  game_name            :string
#  team_a_id :integer          not null
#  team_b_id :integer          not null
class Game
  belongs_to :home_team, :class_name => Team
  belongs_to :away_team, :class_name => Team
end

Data Examples:

LEAGUE - TEAM - GAME 
---------------------------------
AFC - 
        PATRIOTS - 
                 Home       Away   
               G1(PATRIOTS vs DALLAS)
               G2(PATRIOTS vs PITTSBURG)
        PITTSBURG - 
               G2(PATRIOTS vs PITTSBURG)
NFC - 
        DALLAS - 
               G1(PATRIOTS vs DALLAS)
               G3(DALLAS vs GREENBAY)
               G4(DALLAS vs SEATTLE)
        GREENBAY
               G3(DALLAS vs GREENBAY)
        SEATTLE
               G4(DALLAS vs SEATTLE)

The answer will contain a Rails 4 compliant answer. Special consideration may be awarded to a RAILS 5 answer if the Rails 4 alternative is very inefficient.

nfc = League.where(name: 'NFC').first
# <answer>
puts nfc.games 
##  array containing objects [G1,G2,G3]

The challenge Im having with is the home_team / away_team and combining data from the foreign keys.

shadowbq
  • 1,232
  • 1
  • 16
  • 29
  • I'd strongly encourage you to clarify your question. You seem to ask two separate things: How to “get a list of the leagues involved from a subset of games” and how to “Find all games that have NFC teams”. You don't explain what criteria makes a Team “NFC”, and instead show an example of querying for Leagues named “NFC”. – coreyward Jan 26 '17 at 18:45
  • Hopefully its been clarified. – shadowbq Jan 26 '17 at 18:56
  • It would be much simpler to just list a team's games in a single association, then indicate `home` as a boolean value. Ie: Team `has_many :games` -- Home games are `games.where(home: true)` – Wes Foster Jan 26 '17 at 19:01
  • understand hold: Going back to code to get relevant data set problem – shadowbq Jan 26 '17 at 19:08
  • I see what your issue is. On mobile now, but I can answer shortly when I'm back at my computer. Question: is Rails 5 an option, or are you locked to 4? – coreyward Jan 26 '17 at 19:11
  • 1
    @meagar He doesn't have 'games' defined as a relationship on Team, so that won't work as-is. – coreyward Jan 26 '17 at 19:17
  • 1
    @coreyward You're right, my bad. I removed the bounty prior to closing, I can't restore it but I'll offer up a bounty of my own. – user229044 Jan 26 '17 at 19:19
  • Ill put back the bounty / award it to answer 200 pts – shadowbq Jan 26 '17 at 19:19
  • @shadowbq Is the data as presented accurate? Does a team from the AFC really participate in games in the NFC? For example, Patriots, a team in the AFC, has games in the NFC league. – user229044 Jan 26 '17 at 19:20
  • Yes the data is accurate.AFC plays NFC in game #1. Teams have games that play across the leagues. The solution needs to find all games involved. – shadowbq Jan 26 '17 at 19:23

2 Answers2

3

A possible solution is to define a games method on League that finds all of the games where either foreign key points to one of its teams:

class League

  has_many :teams


  def games
    Game.where('team_a_id in (:ids) or team_b_id in(:ids)', ids: teams.pluck(:id))
  end
end

You can accomplish the same thing with a join:

Game.joins('inner join teams on teams.id = games.team_a_id or teams.id = games.team_b_id').where('teams.league_id = ?', id) 
user229044
  • 232,980
  • 40
  • 330
  • 338
  • can you rewrite this as scope lambda rocket – shadowbq Jan 26 '17 at 20:28
  • Can you award yourself or cancel the bounty .. If you can't and need to wait for it to lapse, I will come back in 7 days and attempt to replace the bounty and award it to you. (Not alot of exp with bounties sorry) – shadowbq Jan 26 '17 at 20:41
  • @shadowbq I'd really encourage you to look at [Paul's answer](http://stackoverflow.com/a/41885374/229044). I'm not enough of an SQL guru to gauge it's performance relative to mine, but he's definitely correct in that mine may return duplicate teams. – user229044 Jan 27 '17 at 13:49
3

I'm going to give an answer, because the first solution by @meagar requires two SQL queries instead of one (also, isn't that a SQL syntax error if a league has no teams?), and the second solution will contain duplicate Game instances if both teams were from the same league.

In general I try to avoid joins in my reusable scopes, since they force the query into a certain "shape". So I would write something like this:

class Game
  # ...
  scope :for_league, ->(league_id) {
    where(<<-EOQ, league_id)
      EXISTS (SELECT  1
              FROM    teams t
              WHERE   t.id IN (games.team_a_id, games.team_b_id)
              AND     t.league_id = ?)
    EOQ
  }
  # ...
end

This SQL technique is called a "correlated sub-query" by the way. I admit it looks strange the first time you see it, but it is a pretty normal thing to do. You can see that the subquery "reaches out" to reference games. Your database should have no problem optimizing it (given indexes on your foreign keys of course), but conceptually speaking it runs the subquery once per row in the games table.

Paul A Jungwirth
  • 23,504
  • 14
  • 74
  • 93
  • This would be a better solution if it didn't require hardcoding table and attribute names into a SQL literal. Arel makes it possible to create more flexible scopes that can be reused without brittle hardcoding of attributes. – coreyward Jan 30 '17 at 18:27