慕凡(@ryudoawaru)'s blog

目前還沒想到

Table Inheritance in PostgreSQL

| Comments

前言

Single Table Inheritance(以下簡稱 STI) 是我們在 Rails 常用的一個功能,一般來說 STI 在 Rails 的實現方式如下:

  1. 一張資料表
  2. 多個 ActiveRecord Class 使用這張表
  3. 預設用 type 這個欄位來界定這筆紀錄所屬的 class

這樣做的好處是子類別可以用父類別的欄位,主要的缺點是在可能會浪費到不需要的欄位;Rails 之所以這樣設計,主要是因為大部份的 RDBMS 系統除了 PostgreSQL 和 Oracle 以外幾乎都沒有實作 Table Inheritance。

PostgreSQL 的 Table Inheritance

PostgreSQL 的 Table Inheritance 和 STI 最大的差別在於多表繼承,在官網上就有相當詳細的介紹,基本上就是在資料表之間實作出繼承的關係,B 表繼承 A 表的話,則:

  • B 表會有所有 A 表的欄位資訊
  • 即使日後 A 表欄位有所變動,也會即時反應到 B 表上
  • B 表所建立的資料,在查尋 A 表時會全部出現,反之則無

接下來我們就來介紹如何在 Rails 中用 PostgreSQL 的 Table Inheritance 來實作 ActiveRecord 的物件繼承。

期望的 Schema

實作

建立父類別 User

1
rails g model User
1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :password
      t.string :type
      t.timestamps
    end
  end
end

class User < ApplicationRecord
end

建立 sub-class Staff

1
rails g model Staff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreateStaffs < ActiveRecord::Migration[5.0]
  def up
    execute <<-SQL
    CREATE TABLE staffs(level integer,CONSTRAINT "PK" PRIMARY KEY (id))
    INHERITS(users)
    SQL
  end

  def down
    drop_table :staffs
  end
end
class Staff < User
  self.table_name = 'staffs'
end

和原本 STI 的行為不同,由於 ActiveRecord Migration 並沒有內建相關功能,所以需要自行撰寫 SQL 式來建立 Staffs 表,在建立時需實際指定繼承自 users 表,並且將額外附加的欄位等屬性設定進去即可。

在 Model 的程式方面,必需指定子類別的資料表,否則 Rails 會按照 STI 預設行為去使用父類別的資料表(users)。

在這個範例中,在 staffs 表建立的資料都會出現在 users 表上,反之則不會。

1
2
3
4
5
6
7
8
9
10
11
12
13
User.create(username: 'ryudo', password:'12345')
Staff.create(username:'Jodeci',password:'12345')
User.pluck(:username)
   (0.4ms)  SELECT "users"."username" FROM "users"
 => ["ryudo", "Jodeci"]
Staff.pluck(:username)
   (0.4ms)  SELECT "staffs"."username" FROM "staffs" WHERE "staffs"."type" IN ('Staff')
 => ["Jodeci"]
User.count #=> 2
Staff.count #=> 1
User.last
  User Load (0.6ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
 => #<Staff id: 2, username: "Jodeci", password: "12345", type: "Staff", created_at: "2015-12-31 07:12:16", updated_at: "2015-12-31 07:12:16">

Table Inheritance 會繼承的東西:

  • 欄位資訊:包括預設值或是 NOT NULL,也就是在 SQL 建構式中看到欄位敘述的內容。
  • 父表上的欄位變動:也就是之後在父表上新增刪除修改欄位的變動,會確實的反應到子表上。

Table Inheritance 不會繼承的東西:

  • 欄位資訊以外的全部:包括 Primary Key、Constraint 或是 Index 等。

主鍵重複值問題

這應該是最大的困擾之一,就是繼承的子表的主鍵是可以和父表的值重複的,例如以下的程式碼是可以正確執行的:

測試title
1
2
User.create(id:1, username: 'ryudo', password:'12345') #=> 建立 id 為 1 的 User
Staff.create(id:1,username:'Jodeci',password:'12345') #=> 建立 id 為 1 的 Staff

這樣在 users 表會真的有兩筆 id 為 1 的紀錄,而且目前無法用資料庫的方式去迴避這個問題,不過由於兩個 id 之間是共用同一個 Sequence,在不指定 id 建立資料的狀況下,id 的值是不會重複的。

inheritance_column 存廢問題:

在上面的範例中,我們可以看到原本 STI 的 inheritance_column,也就是 type 這個欄位的存在,原本 STI 的設計是:子類別的資料將會自動在 type 上加上類別名稱,查詢 Staff 時也會預設查詢 type 為 Staff 的資料。

這個設計是因為在資料同屬一張表的前提下,必需用一個欄位去區分類別的關係,而在 Table Inheritance 下,由於每個類別有獨立的資料表,在查詢子類別時已經不需要用 type 這個條件了;但是如果在父類別的資料表查詢時,還是需要用 inheritance_column 去區分出這筆資料所屬的類別,否則所有父類別資料表的資料在 ActiveRecord 中會被視為父類別的資料。

多表繼承

其實是有這項功能的,但是目前還沒想到需要使用的場合所以這次就不討論了。

是否要使用這個功能?

好處

  • 比起 ActiveRecord 提供的 STI 更趨近於物件化
  • 由於資料表是物理上的切分,等於是一種 Partitioning,在資料量大的時候可以提升子表的效能
  • 同上,減少欄位或 Index 等資源不必要的浪費

壞處

  • Rails 預設不支援相關的 Migration 操作,需手動撰寫 SQL
  • 同上,在 Model 的設定上必需客製一些設定

以上!

Comments