ruby-on-railsinheritanceassociationspolymorphic-associationsmulti-table-inheritance

How do you set up MTI in Rails with a polymorphic belongs_to association?


In an effort to create a Short, Self Contained, Correct (Compilable), Example, imagine that I want to do the following.

I have a blog website. There are two types of posts, TextPost and LinkPost. There are also two types of users, User and Guest. I would like to implement Multiple Table Inheritance with TextPost and LinkPost, by which I mean (hopefully I'm using the term correctly):

Each type of Post can belong to either a User or a Guest. So we have a polymorphic belongs_to situation.

My question is how to accomplish these goals.

I tried the following, but it doesn't work.

class Post < ApplicationRecord
  self.abstract_class = true

  belongs_to :author, polymorphic: true # user or guest
  validates :title, :author_id, :author_type, presence: true
end

class TextPost < Post
  validates :content, presence: :true
end

class LinkPost < Post
  validates :url, presence: :true
end

class User < ApplicationRecord
  has_many :text_posts, as: :author
  has_many :link_posts, as: :author
  validates :name, presence: true
end

class Guest < ApplicationRecord
  has_many :text_posts, as: :author
  has_many :link_posts, as: :author
end

class CreateTextPosts < ActiveRecord::Migration[6.1]
  def change
    create_table :text_posts do |t|
      t.string :title
      t.string :content
      t.references :author, polymorphic: true

      t.timestamps
    end
  end
end

class CreateLinkPosts < ActiveRecord::Migration[6.1]
  def change
    create_table :link_posts do |t|
      t.string :title
      t.string :url
      t.references :author, polymorphic: true

      t.timestamps
    end
  end
end

class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :name

      t.timestamps
    end
  end
end

class CreateGuests < ActiveRecord::Migration[6.1]
  def change
    create_table :guests do |t|

      t.timestamps
    end
  end
end

Console output:

 :001 > user = User.create(name: 'alice')
   (1.6ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Create (1.2ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "alice"], ["created_at", "2021-06-11 23:33:38.445387"], ["updated_at", "2021-06-11 23:33:38.445387"]]
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1
 :002'> text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type:
'user')
Traceback (most recent call last):
        1: from (irb):2:in `<main>'
NameError (wrong constant name user)

Solution

    1. The names of constants look like the names of local variables, except that they begin with a capital letter.

    2. All the built-in classes, along with the classes you define, have a corresponding global constant with the same name as the class called class name.

    So in your case, when you define User class, there's a constant class name: User, but not user, that why the error NameError (wrong constant name user) is raised.

    try text_post = TextPost.create(title: 'foo', content: 'lorem ipsum', author_id: 1, author_type: 'User')