Search code examples
ruby-on-railsactiverecordself-join

Rails self join with has_many association


I am trying to write an app like IMDB in rails.

I have created the Movie model. Every movie has many movie recommendations (which are also instances of Movie).

I don't know how to add the "has_many" association, how to write the migration file or how to add recommended movies to each movie.


Solution

  • You have a many-to-many relationship, which means we need a join table Recommendation.

    Create model and migration files with a generator:

    bin/rails generate model Movie
    bin/rails generate model Recommendation
    

    Then update migrations:

    # db/migrate/20221023063944_create_movies.rb
    class CreateMovies < ActiveRecord::Migration[7.0]
      def change
        create_table :movies do |t|
          # TODO: add fields
        end
      end
    end
    
    # db/migrate/20221023064241_create_recommendations.rb
    class CreateRecommendations < ActiveRecord::Migration[7.0]
      def change
        create_table :recommendations do |t|
          t.references :movie,             null: false, foreign_key: true
          t.references :recommended_movie, null: false, foreign_key: { to_table: :movies }
        end
      end
    end
    

    Run migrations:

    bin/rails db:migrate
    

    Setup models:

    # app/models/movie.rb
    class Movie < ApplicationRecord
      # NOTE: this is the relationship for join table
      has_many :recommendations, dependent: :destroy
    
      # NOTE: get movies from join table
      has_many :recommended_movies, through: :recommendations
      #       this ^ is the name of the relationship in `Recommendation` we want
    end
    
    # app/models/recommendation.rb
    class Recommendation < ApplicationRecord
      belongs_to :movie
    
      # NOTE: our foreign key is `recommended_movie_id` which rails infers 
      #       from `:recommended_movie`, but we have to specify the class:
      belongs_to :recommended_movie, class_name: "Movie"
    end
    

    Test it in the console bin/rails console:

    >> 3.times { Movie.create }
    >> Movie.first.recommended_movies << [Movie.second, Movie.third]
    >> Movie.first.recommended_movies
    => [#<Movie:0x00007f15802ec4c0 id: 2>, #<Movie:0x00007f15802ec3d0 id: 3>]
    

    or like this:

    >> Movie.second.recommendations << Recommendation.new(recommended_movie: Movie.first)
    >> Movie.second.recommended_movies
    => [#<Movie:0x00007f158215ef20 id: 1>]
    

    https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

    https://guides.rubyonrails.org/association_basics.html#self-joins