Single-table inheritance And Polymorphic

STI 與 多型關聯 的 combo 技

先來假設一個情境

使用者可以訂閱不同的方案,年繳或月繳之類的

1. 單一表格繼承 STI (Single-table inheritance)

  1. YearPlanMonthPlan 共用,Plan 這個 table
1
2
3
4
5
6
7
# Table name: plans
# id :bigint not null, primary key
# type :string(191) not null
# status :integer default("pending")
# amount :integer
# created_at :datetime not null
# updated_at :datetime not null

STI 單一表格繼承,會根據你使用建立的 modelplans 的這個 tabletype 中紀錄 model_name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# model/plan.rb
class Plan < ApplicationRecord
# do_something
end

# model/foo_plan.rb
class YearPlan < Plan
# do_something
end

# model/boo_plan.rb
class MonthPlan < Plan
# do_something
end

ex.

1
2
3
4
# rails console
YearPlan.create!(amount: 999, status: "active")

MonthPlan.create!(amount: 99, status: "active")

這樣的情況,會在 planstable 中 建立一筆 type 分別為 "YearPlan""MonthPlan" 的 record

而在 console YearPlan.last,只是去 Plans 這個 table

因為這個功能,在 rails 裡,column 的名字不能亂取成 type

2. 多型關聯 polymorphic

先假設另外一個單純的情境來講 polymorphic,以 Rails實戰聖經裡面的例子,airtcle、photo 都可以被 comment
所以 comment 可以用多型的關係指向不同的 model 關聯。

1
2
3
4
5
6
7
8
9
10
11
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end

class Article < ApplicationRecord
has_many :comments, as: :commentable
end

class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
1
2
3
4
5
6
7
8
9
10
11
article = Article.first

# 透過關連新增留言
comment = article.comments.create(:content => "First Comment")

# 你可以發現 Rails 很聰明的幫我們指定了被留言物件的種類和id
comment.commentable_type => "Article"
comment.commentable_id => 1

# 也可以透過 commentable 反向回查關連的物件
comment.commentable => #<Article id: 1, ....>

到這裡我一直都認為,在寫入的 commentable_type ,是去抓取物件的 model(class) name,直到他與 STI 相遇了……

回到 STI 與 polymorphic

使用者可以分別訂閱不同的方案,但其實可以開成一個 model 就好。

1
2
3
4
5
6
7
8
9
10
11
12
class Subscription < ApplicationRecord
belongs_to :planable, polymorphic: true
end

# Table name: subscriptions
#
# id :bigint not null, primary key
# planable_type :string(191)
# planable_id :integer
# status :integer default("active")
# created_at :datetime not null
# updated_at :datetime not null

這樣的情況預期可以讓 Subscription 根據 planable 帶進來的 record 去找到相關連的 object

1
2
3
my_plan = YearPlan.last

record = Subscription.create!(planable: my_plan, status: "active")

接著讓我們來看看 這個 record

planable_type 寫的是 寫的是 Plan ,不是 YearPlan ,所以看起來是去抓他的 table_name,要注意!

💡 解法

1
2
3
4
5
before_validation :set_type

def set_type
self.planable_type = planable.class.name
end

若有任何指教,歡迎留言給我。🙏

參考資料

  1. stack over flow
  2. Rails實戰聖經