Gregology
Gregology

Reputation: 1725

Self-Referential Associations in Rails 5

I'm trying to setup a self-referential association in Rails 5. I have a video model. A video may have a previous video (like in a tv series). Ideally it would act like this;

irb(main):001:0> first_video  = Video.create(url: 'https://youtu.be/W0lhlPdo0mw')
irb(main):002:0> second_video = Video.create(url: 'https://youtu.be/gQhlw6F603o', previous_video: first_video)
irb(main):003:0> second_video.previous_video
=> #<Video id: 1, url: "https://youtu.be/W0lhlPdo0mw">

This is my current approach but it's failing with (PG::UndefinedColumn: ERROR: column videos.video_id does not exist) so I'm having to pass around the id.

Model

class Video < ApplicationRecord
  has_one :previous_video, class_name: 'Video'
end

Migration

class CreateVideo < ActiveRecord::Migration[5.2]
  def change
    create_table :videos do |t|
      t.string :url, null: false
      t.references :previous_video, class: 'Video'

      t.timestamps
    end
  end
end

What is the best practice to achieve this in Rails 5? And why doesn't the above work as expected? Cheers Team!

Upvotes: 2

Views: 2695

Answers (2)

Jordan Pickwell
Jordan Pickwell

Reputation: 163

t.references is meant for belongs_to associations on the model, and has_one on the associated model. Below is how I would write the model and migration to accomplish what you want.

Update (2019-05-31):

Fixed the foreign key creation in the migration so it uses the correct to-table. Also added reference links for more information.

# model
class Video < ApplicationRecord
  belongs_to(
    # by default, the association name + "_id" will be used for the column name
    :next_video,
    optional: true,
    class_name: 'Video', # will use the table of the specified model
    inverse_of: :previous_video
  )

  has_one(
    :previous_video,
    class_name: 'Video', # will use the table of the specified model
    foreign_key: 'next_video_id',
    inverse_of: :next_video
  )
end

# migration
class CreateVideo < ActiveRecord::Migration[5.2]
  def change
    create_table(:videos) do |t|
      t.string(:url, null: false)

      # This will create a `next_video_id` column. Add/remove any options as you
      # see fit.
      # - index: creates an index on the column
      t.references(:next_video, index: true)

      t.timestamps
    end

    # A foreign key could have been created when specifying the `references`
    # column inside `create_table`, but it's self referential, and I'm not sure
    # if it would work. So to be safe, the foreign key is created after the
    # table is created.
    add_foreign_key(:videos, :videos, column: :next_video_id)
  end
end

Upvotes: 1

catmal
catmal

Reputation: 1758

You need to set the association like this:

has_one :prev_video, :class_name => 'Video', :foreign_key => 'previous_video'

Having set that you can call

@video.prev_video

Foreign key is the database column, the one i called prev_video can by named as you wish as long as you don't use the same name you gave to db column (previous_video).

Upvotes: 3

Related Questions