Rails - Adding a bi-directional relationship

Viewed 67

I'm trying to setup a sample application that tracks football matches. There's currently 3x tables:

  • Ground - The home ground of a particular team
  • Team - The actual team itself
  • Game - A game, which will hold two teams

I can successfully query "Which two teams are playing in this game?", but I'm struggling to reverse it and ask "Which games has this team played in?"

I've setup a new rails project, with Rails 5.1.7 and been using the Rails console to query the data.

Migrations

Ground

  def change
    create_table :grounds do |t|
      t.string :ground_name
      t.string :ground_location
      t.belongs_to :team
      t.timestamps
    end
  end
end

Game

  def change
    create_table :games do |t|
      t.timestamps
      t.datetime :game_time
      t.belongs_to :team_one
      t.belongs_to :team_two
    end
  end
end

Team

  def change
    create_table :teams do |t|
      t.string :team_name
      t.timestamps
    end
  end
end

Models

Ground

class Ground < ApplicationRecord
  belongs_to :team
end

Game

class Game < ApplicationRecord
  belongs_to :team_one, :class_name => "Team"
  belongs_to :team_two, :class_name => "Team"
  has_many :teams
end

Team

class Team < ApplicationRecord
  has_one :ground
end

I can successfully query:

g1 = Game.new
g1.team_one = Team.first
g1.team_two = Team.second
g1.save

Game.first.team_one -> Correctly spits out team
Game.first.team_two -> Correctly spits out team

I'd like to be able to get a list of games for a team:

Team.first.games
  Team Load (0.0ms)  SELECT  "teams".* FROM "teams" ORDER BY "teams"."id" ASC LIMIT ?  [["LIMIT", 1]]
NoMethodError: undefined method `games' for #<Team:0x5a53fa0>
        from (irb):1


I'm definitely missing something, but would hope this is possible. If anyone could shed any light on this, I'd really appreciate it! Thanks in advance.

4 Answers

You can get games like below. unscope is important. The query will not run correctly without unscope.

class Team < ApplicationRecord
  has_one :ground
  has_many :games, lambda { |team|
    unscope(:where)
      .where('team_one_id = ? OR team_two_id = ?', team.id, team.id)
  }
end

t1 = Team.first
t1.games
#=> [Game id: 1, team_one_id: 1, team_two_id: 2, ..]
t2 = Team.second
t2.games
#=> [Game id: 1, team_one_id: 1, team_two_id: 2, ..]

But I'm struggling to reverse it and ask "Which games has this team played in?"

If you want to find all the games a team has played in you can use the #or method like this:

team = Team.find(1) # or any other criteria to find the team you want to get the games for

Game.where(team_one: team).or(Game.where(team_two: team))
class Game < ApplicationRecord
  belongs_to :team_one, :class_name => "Team"
  belongs_to :team_two, :class_name => "Team"

  scope :by_team, -> (team) { 
    where(team_one_id: team.id).or(where(team_two_id: team.id)) 
  }
end

Game.by_team(Team.first)

First up, answer the easy case: using your Game model we can easily list the games that a team plays at home, and the away games (I hope that is the correct term). I assume, from your data-structure that team 1 hosts the game. So we get something like:

class Team 
  has_many :home_games, class_name: 'Game', foreign_key: :team_one 
  has_many :away_games,  class_name: 'Game', foreign_key: :team_two 
end 

But to list all the games of a team, we can do several things:

The least efficient but very easy/naive implementation:

 def games 
   home_games + away_games 
 end 

this will fetch all home and away games, convert to array and add/concatenate the arrays, BUT no way to later add where, sorting, etc ...

A better way is to use the database

def games 
  Game.where("team_one_id = ? or team_two_id = ?", self.id, self.id) 
end 

(in rails 5 you can also write this as Game.where(team_one_id: self.id).or(Game.where(team_two_id: self.id)) but I still prefer the old way as more readable and less typing)

Then you would be able to write something like

@team = Team.find(params[:id])
@games = @team.games.order(:game_time)
Related