Leaf
Leaf

Reputation: 553

Polymorphic has many though with STI in rails

I've faced unusual situation with rails association and created simple sample to show that. Assume we have 4 models: 1) list.rb

class List < ApplicationRecord;end

2) show.rb

class Show < List;end

3) joined_table.rb

class JoinedTable < ApplicationRecord
  belongs_to :listable, polymorphic: true
  belongs_to :work
end

4) work.rb

class Work < ApplicationRecord
  has_many :joined_tables, dependent: :destroy
  has_many :lists, through: :joined_tables, source: :listable, source_type: 'List'
  has_many :shows, through: :joined_tables, source: :listable, source_type: 'Show'
end

Thus we have next schema.rb

create_table "joined_tables", force: :cascade do |t|
  t.integer  "listable_id"
  t.string   "listable_type"
  t.integer  "work_id"
  t.datetime "created_at",    null: false
  t.datetime "updated_at",    null: false
end

create_table "lists", force: :cascade do |t|
  t.string   "type"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

create_table "works", force: :cascade do |t|
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

Now when you got all the info, could anybody explain me why when I'm trying

Work.first.shows

it generates

 Show Load (0.2ms)  SELECT "lists".* FROM "lists" INNER JOIN "joined_tables" ON "lists"."id" = "joined_tables"."listable_id" WHERE "lists"."type" IN ('Show') AND "joined_tables"."work_id" = ? AND "joined_tables"."listable_type" = ?  [["work_id", 1], ["listable_type", "Show"]]

that looks like a proper request, but when I'm trying to do

Work.first.lists

it generates

  List Load (0.1ms)  SELECT "lists".* FROM "lists" INNER JOIN "joined_tables" ON "lists"."id" = "joined_tables"."listable_id" WHERE "joined_tables"."work_id" = ? AND "joined_tables"."listable_type" = ?  [["work_id", 1], ["listable_type", "List"]]

which IS NOT THE SAME and missing checking lists.type.

I know that is poor architecture and one probably should use scope on associations in Work leaving aside polymorphism in JoinedTable, but that's just something I've faced in project that came up to me. Also I'd love to hear alternative solutions to that, but most of all I'd like to know why rails generates different queries.

P.S. In example I'm using rails 5 and SQLite. But in real project using rails 4 and postgres it's even worse. If I do first Work.first.shows and then Work.first.lists it generates

  List Load (0.4ms)  SELECT "lists".* FROM "lists" INNER JOIN "list_works" ON "lists"."id" = "list_works"."listable_id" WHERE "list_works"."work_id" = $1 AND "list_works"."listable_type" IN ('List', 'Show')  [["work_id", 50]]

but if I do Work.first.lists at once it generates

  List Load (0.8ms)  SELECT "lists".* FROM "lists" INNER JOIN "list_works" ON "lists"."id" = "list_works"."listable_id" WHERE "list_works"."work_id" = $1 AND "list_works"."listable_type" = 'List'  [["work_id", 50]]

Thank you for help!

Upvotes: 1

Views: 74

Answers (1)

chumakoff
chumakoff

Reputation: 7034

Work.first.lists doesn't generate lists.type because List is the base class in the hierarchy, so lists are all records of all types in the lists table. Also, keep in mind that all List records will have type = nil in DB, since List is the base class.

Furthermore, when you use polymorphic association with STI, it by default always assigns the name of the base class to the *_type column of polymorphic association. In your case listable_type of JoinedTable will always be "List"

The second problem is because when you do Work.first.lists first, Rails doesn't know that there is the child Show class, since it hasn't been loaded yet at that moment. You should whether make it autoload by accessing it (it happened when you did Work.first.shows), or use something like require_dependency.

Here is a working solution for you:

class Work < ApplicationRecord
  has_many :joined_tables, dependent: :destroy
  has_many :lists, -> { where(type: nil) }, through: :joined_tables, source: :listable, source_type: "List"
  has_many :shows, -> { where(type: "Show") }, through: :joined_tables, source: :listable, source_type: "List"
end

Upvotes: 1

Related Questions