Welcome Anthony to the wonderful world of Rails! Hopefully this post can help get your many:many working properly.
Given your current setup, having only one foreign key in the games table, each game can only have one genre. And currently one genre could be set up with multiple games, as more than one game could point to it with its foreign key.
In order to convert your setup to be able to do many to many (N:M) then you need an “associative table” – that is – a table in the middle which has the foreign keys. Then multiple associations could be established in both directions – not only could genres relate to multiple games, but also games could have multiple genres. (Sometimes this kind of middle table is called a “join table”.) This associative table will have foreign keys to both games and genres.
to get started, first create a model for the new table:
# app/models/game_genre.rb
class GameGenre < ApplicationRecord
belongs_to :game
belongs_to :genre
end
With that in place then the following two migrations can be run, this first one to build out an associative table in the database:
# db/migrate/20220731161900_create_game_genres.rb
class CreateGameGenres < ActiveRecord::Migration[7.0]
def change
# Adds an associative table which will hold N:M associations between games and genres
create_table :game_genres do |t|
t.references :game, foreign_key: { to_table: :games }, index: true
t.references :genre, foreign_key: { to_table: :genres }, index: true
end
end
end
And a second one to populate that new table based on any existing 1:M records already in place in games. This preserves any genre stuff you might already have kicking around. The final action in the second migration is to remove what would then be the unnecessary lingering foreign key games_id
from games:
# db/migrate/20220731162000_migrate_genre_data.rb
class MigrateGenreData < ActiveRecord::Migration[7.0]
def self.up
if column_exists?(:games, :genres_id)
# Migrate any existing 1:M associations from the games table to the new associative table
Game.joins('INNER JOIN genres ON games.genres_id = genres.id')
.select('games.id AS game_id', 'genres.id AS genre_id')
.each do |game_genre|
GameGenre.create(game_id: game_genre.game_id, genre_id: game_genre.genre_id)
end
# Remove the 1:M column from games, starting with its index
remove_index :games, name: 'index_games_on_genres_id'
remove_column :games, :genres_id
Game.reset_column_information
end
end
def self.down
unless column_exists?(:games, :genres_id)
# Note that this should really have a singular name -- "genre" instead of "genres" -- it's set up
# this way only to match how the original table was set up.
add_reference :games, :genres, index: { name: 'index_games_on_genres_id' }
Game.reset_column_information
if Object.const_defined?('GameGenre')
GameGenre.each do |game_genre|
Game.find_by(id: game_genre.game_id)&.update(genres_id: game_genre.genre_id)
end
end
end
end
end
After successfully running these two migrations with bin/rails db:migrate
then you can remove the belongs_to found in your Game
model, and also if you have an inverse has_many found in Genre
then it can be axed. The has_many would look something like this:
# If this exists in your Genre model then remove it:
has_many :games, foreign_key: :genres_id
Ultimately now your Game
and Genre
models can look like:
# app/models/game.rb
class Game < ApplicationRecord
has_many :game_genres
has_many :genres, through: :game_genres
end
# app/models/genre.rb
class Genre < ApplicationRecord
has_many :game_genres
has_many :games, through: :game_genres
end
And then you should be able to do this in a bin/rails c to see a list of games related to your first genre:
Genre.first.games
Let me know how you get on!