I have the following models: Game
, HighSchoolTeam
, and ClubTeam
. I want Game
two have a team_one
and a team_two
field, with each field referring to a HighSchoolTeam
or a ClubTeam
.
In HighSchoolTeam
and ClubTeam
I have has_many :games, as: :teamable
. In Game
I would like to do something like the following...
class Game < ApplicationRecord
belongs_to :team_one, polymorphic: true, class_name: "Teamable"
belongs_to :team_two, polymorphic: true, class_name: "Teamable"
end
...but the class_name: "Teamable
part doesn't seem to work.
Edit:
schema.rb
ActiveRecord::Schema.define(version: 2019_12_24_011346) do
...
create_table "club_teams", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "fields", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "games", force: :cascade do |t|
t.bigint "tournament_id", null: false
t.string "team_one_type", null: false
t.bigint "team_one_id", null: false
t.string "team_two_type", null: false
t.bigint "team_two_id", null: false
t.bigint "field_id", null: false
t.date "date"
t.datetime "start_time"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["field_id"], name: "index_games_on_field_id"
t.index ["team_one_type", "team_one_id"], name: "index_games_on_team_one_type_and_team_one_id"
t.index ["team_two_type", "team_two_id"], name: "index_games_on_team_two_type_and_team_two_id"
t.index ["tournament_id"], name: "index_games_on_tournament_id"
end
create_table "high_school_teams", force: :cascade do |t|
t.string "school_name"
t.string "team_name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "tournaments", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "games", "fields"
add_foreign_key "games", "tournaments"
end
game.rb
class Game < ApplicationRecord
belongs_to :tournament
belongs_to :team_one, polymorphic: true
belongs_to :team_two, polymorphic: true
belongs_to :field, optional: true
end
high_school_team.rb
class HighSchoolTeam < ApplicationRecord
has_many :players
has_many :games, as: :teamable, dependent: :destroy
def name
self.school_name
end
end
club_team.rb
class ClubTeam < ApplicationRecord
has_many :players
has_many :games, as: :teamable, dependent: :destroy
end
console output
code/scout-db [master●] » rails c --sandbox
Running via Spring preloader in process 48525
Loading development environment in sandbox (Rails 6.0.1)
Any modifications you make will be rolled back on exit
WARNING: This version of ruby is included in macOS for compatibility with legacy software.
In future versions of macOS the ruby runtime will not be available by
default, and may require you to install an additional package.
irb(main):001:0> game = Game.new({ team_one_id: "high-school-team-2", team_one_type: "HighSchoolTeam", team_two_id: "club-team-2", team_two_type: "ClubTeam" })
(0.2ms) BEGIN
=> #<Game id: nil, tournament_id: nil, team_one_type: "HighSchoolTeam", team_one_id: 0, team_two_type: "ClubTeam", team_two_id: 0, field_id: nil, date: nil, start_time: nil, created_at: nil, updated_at: nil>
irb(main):002:0> game.team_one_id
=> 0
irb(main):003:0> game.save
(0.3ms) SAVEPOINT active_record_1
HighSchoolTeam Load (0.4ms) SELECT "high_school_teams".* FROM "high_school_teams" WHERE "high_school_teams"."id" = $1 LIMIT $2 [["id", 0], ["LIMIT", 1]]
ClubTeam Load (0.3ms) SELECT "club_teams".* FROM "club_teams" WHERE "club_teams"."id" = $1 LIMIT $2 [["id", 0], ["LIMIT", 1]]
(0.4ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
irb(main):004:0> game.errors.full_messages.inspect
=> "[\"Tournament must exist\", \"Team one must exist\", \"Team two must exist\"]"
(2, Syosset, Braves, 2019-12-31 01:07:41.367913, 2019-12-31 01:07:41.367913)
exists in the high_school_teams
table and (2, Foobars, 2019-12-31 01:07:52.697821, 2019-12-31 01:07:52.697821)
exists in the club_teams
table.
Of course class_name: "Teamable"
does not work as the whole point of a polymorphic association is that the class that the class (and more importantly target table) of the association is dynamic. Its not needed either.
A polymorphic association uses a separate association_name_type
string column which contains the class name which its associated with.
Given the following data:
| id | team_one_id | team_one_type # ...
----------------------------------
1 | 1 | "HighSchoolTeam"
2 | 2 | "ClubTeam"
3 | 3 | "HighSchoolTeam"
When you do Game.find(1).team_one
Rails knows to use the HighSchoolTeam
class and join the high_school_teams
table. But it needs to pull the rows out before it can make the connection and of course your database knows nothing about the relation and won't maintain referential integrity.
So all you need is:
class Game < ApplicationRecord
belongs_to :team_one, polymorphic: true
belongs_to :team_two, polymorphic: true
end
And make sure you have team_one_type
and team_two_type
string columns on the games
table.