Enterprise Rails: 資料庫篇

稿子是 2009 年底寫的,本來想一口氣連 SOA 的部份一起整理 (對 SOA 有興趣的話,可以看我之前的投影片 91 頁 ~ 119 頁),不過一直拖到現在。雖然內容還是有點些雜亂,而且現在流行 NoSQL (?),不過還是就貼出來吧。

這本 2009 年底的書,趁著轉職的空檔終於一口氣讀完了。有些重要的背景知識,如果你一直追所謂的最新技術,反而是學不到。這本書其實講 Rails 不多,前一大半都是在講 RDBMS 關聯式資料庫,後半則是 SOA 架構。

像 RDBMS 裡面的 referential integrity、composite key、DB View、triggers、materialized View 這些東西,因為身為 MySQL 使用者很少用到、Rails 也沒有內建支援,實際寫 code 好像也都在 application layer 應用層處理掉了,所以到底要幹嘛用的呢? 他們被發明一定有原因吧? 是在什麼情境上使用呢?

第四章 Database As a Fortress

作者非常強調資料庫的重要,一家企業最重要的資產是資料,而不是員工 (資料不見就全完了,員工沒了再找就有了)。Framework 會變、程式會有 bug、可能也不會只有 Rails 會去存取 DB (所以只用 ActiveRecord Validation 並不可靠),總之只有你的 RDBMS 可以保障資料的正確性,data integrity 長存。

PostgreSQL 也比 MySQL 適合企業應用,因為很多 SQL 標準 MySQL 並不支援 (很多直到 5.0 後才支援)。PostgreSQL 會輸掉市佔率的原因是 1. MySQL 背後有一家商業公司支持 2. PostgreSQL 有很長一段時間不像 MySQL 有提供 Windows 安裝包 (話說這本書對 MySQL 其實還蠻揶揄的)。

作者也不建議使用 migration,因為它沒有支援所有的 DDL (Data Definition Language)。反正只有跑一次,不像 DML (Data Manipulation Language) 會一直用,用 ORM 比較方便。

作者持續強調 referential integrity 對企業層級的上線應用程式非常重要。會說不重要的,一定是該死的 MySQL 使用者(died-hand MySQL users),千萬別因為你的工具(i.e. MySQL, Rails)沒有這個功能,就說不重要。

隨著資料越來越多,你的老闆會想要知道一些數據分析報表,這裡作者點出一個重要的議題:你的報表會害了你的網站!! 你越常去產生你的報表,你的網站就會越來越慢。”Report are killing your site!!” 為什麼呢? 因為去 DB 撈大量的資料做報表,是非常龐大複雜的 SQL 操作,你的 DB 的效能會被拖慢,整個網站的效能就跟著 down 下來。該怎麼解呢? 撈 report 的 queries 不要對 production database 做就沒事了吧?

這裡作者又對 MySQL users 揶揄了:因為大多數的 MySQL users 第一個想到的方案就是使用 master/slave 來解決,也就是讓 query report 對 slave DB 做,但是即時如此不會影響到 production 運作,也沒辦法解決 query 查詢很慢的問題。

這裡觀念上要區分的是 OLTP (Online Transaction Processing) 和 OLAP (Online Analytical Processing 這兩種類型的 query,前者是一般的讀/寫/更新,這種是會讓使用者可以馬上等結果,大部分的前台網站就是屬於這種。後者則是一個 query 就須需要收集上百萬的資料去分析,例如:有多少客戶買了產品A,又買了產品 B,依照地點跟時間。

因為 OLTP 和 OLAP 是如此不同,沒道理 database 的設計也相同。OLAP 要快,就必須使用 denormalized 非正規化的方式來存放資料。但是將正規化資料和逆正規化資料混雜在同一個 DB,則非常容易造成資料的混亂不同步,寫出 buggy 有臭虫的程式。

要解決這個問題的領域是使用獨立的 data warehouse 存放 denormalized 的資料。(書就到此打住了,作者推薦了 The Data Warehouse Toolkit: The Complete Guide to dimensional Modeling by Ralph Kimball 一書)

第五章 Building a Solid Data Model

Data layer 層級的 constraint 才保證一定正確,因為應用層的 Model validation 可以跳過,放在 code 裡也容易被改掉。作者也示範了這兩者都可以寫單元測試。

referial constraint 如果只用 Model 做,destroy 時就會失效。而 Model 要做 referial constraint 只能用 has_many :depentent 但是如果誤用了 :delete 就 orz 了。因此最保證的作法還是 data layer 做。

資料庫記得加 index 在 1. foreign key 2. 任何有 SQL where 條件的地方

第六章 Refactoring to Third Normal Form

3NF (三階正規化) 能做到 DB 資料不重複:只要不是 primary key、不是 foreign key、不是 intrinsic data(eg. name)、不是 measured value (例如 time, temperature),而是一個 literally bound data,都應該正規化出來新建一個 table。不先做,後來要加會十分痛苦,尤其在一個已經有資料的 production db 上。

範例中的有很多 table 都有相同的地址欄位定義:Postgres 支援 multipie inheritance,可以處理重複的 DDL,再搭配上 Rails 可用 plugin mixin 處理重複的 code。

這裡留下一個第八章才回答的問題,只有一個 primary key 沒辦法一次撈出 has_many 的 has_many 資料。多保留一個 foreign key 又可能造成 direct 和 in-direct 的資料不一定一致。

第七章 Domain Data

Domain data (指網站預先就有的必要初始資料,又叫做 seed data) 也應該使用 table 存,因為 1. 保持 referential integrity 2. 維護 3ND 跟擴充彈性。實作上則可以做成常數形式。

strategy patterns with domain tables 這招示範了將 Order PAYMENT_TYPE 變成 domain tables 來做,除了變成 constant object,情境是如果不同 payment_type 會有不同的 validation。首先變成 constant 後,可以順利將 validation code 都從 order 搬走。而再進一步 rails single table inheritance 和 template method 將 validation 分散到個別的 domain model。總之,作者將 domain data 的彈性做了非常好的示範。

雖然正規化會導致大量的 table 和 model,但是也因為如此每個都很小非常容易測試,bugs 也就容易集中在小區域容易找到。

第八章 Composite Keys and Domain Key/Normal Form

這章討論 composite key 的優缺,以及如何使用。

ID column 系統的優點 1. Rails 內建 2. 簡單,除了 primary key 之外皆可修改,物件的 primary key 一定不變 3. 提供與真實資料的間接性,因此也不需有修改 primary key 的機會。 4. unique key 好做

composite key 的優點就沒這麼顯而易見,也不一定用的到。他的用途在於提供一種特別的 data integrity。table 中不一定可以發現有 natural composite key, 但是如果有而你忽略他,可能會有大問題。這個情境就是:

只有一個 primary key 沒辦法一次撈出 has_many 的 has_many 資料,如果只是加上一個 reference key, 可能導致 refential integrity hole. 這時 composite key 才是唯一解決之道。

DFNF 比 3NF 更近一步保證 referential integrity 在複雜的 relationship 中。不能因為工具沒有,就覺得不重要。因為 Rails 沒有內建,導致很多人不知道 DK/NF or natural keys 等這些已經發展成熟的資料庫基礎理論,在設計 schema design 上而有很大的缺陷。

先來檢討 single column ID 是否應該用 Rails 內建的數字,首先找有沒有別的 unique column,再來覺得它是否不會 或 不常變更,特別是連編輯介面都沒有的 domain data 特別符合這個條件,如果是,則可以用 set_primary_key 換掉,移除不必要重複 id column。不過,如果不是 domain data, 我們就必須產生這個 primary key,用法是寫在 before_create 裡,另外要注意還是使用 self.id。一個額外的好處是,這些可能被當做 foreign_key 的 nature key,也是有用的資訊,不像本來的數字 id 一定還要去本來的 table 查。

這裡我有個疑問是 Is there a REAL performance difference between INT and VARCHAR primary keys? ,在我碰到的例子是,很多人擔心非 integer 當 key 會影響效能 :/ (題外話,很多人愛用 type code,但都被我建議改成 string constant ) 但是我想這差距的微乎其微,尤其在你沒有上百萬的資料列。重點還是,你選的 nature key 不需要有被修改的可能 :>

Rails 要支援 composite keys 有兩個方法,一個是使用 Dr. Nic Williams 的 plugin,一個是本書作者的 Rails-DK/NF hybrid 法:

具體的作法是,保留 ID column 欄位,但是 DB 還是加上 composite key 的 foreign key referential integrity 限制。好處在 Rails 裡面不方便改 primary key, 當有修改的需求時,使用 hybird 法就不錯簡單(不需要裝plugin)。但是回過頭想,如果你需要修改 nature key,可以先想想是不是最好的作法應該是刪掉舊的,插入新的。

一個小技巧是附寫掉本來的 writor,這樣就不需要手動設定 composite key 的值了。如果要改已經被 reference constraint 限制的 composite keys 怎麼辦? 直接改會爆,這時候需要使用 deferrable constraints 的機制和 transaction。

不過 hybrid 法的缺點是 1. 因為維護兩套 index key 的關係, 新增修改刪除的 index cost 比較高。 2. 需要多寫上述的 code 才能省掉手動設計的麻煩。

書沒寫哪個最好,看起來是如果非得有修改 nature key 的需求才只用 hybrid 法。

第九章 Guaranteeing Complex Relationships with Triggers

stored procedure 和 triggers
使用 PL/pgSQL 做例子。

第十章 Multiple Table Inheritance

Rails 的 polymorphic associations 功能讓你可以定義兩個 table 的關係,不需要事前知道是哪一個 table。

不過,polymorphic associations 違反了 referential integrity !! 原因很簡單,既然不知道 _id 會指到哪個 table,自然也就沒辦法在 DB layer 加上 foreign key constraint

什麼是多型?

要達成多型的方式,實作上要考慮的是 STI 或 MTI:前者 Rails 有內建,後者 Rails 有用了 polymorphic 方式來達成 XOR relationships,也算是一種簡易的 MTI。

這本書用了 logical 和 physical models 來分別描述概念上和實際上的切法

STI 適用於 subclass 共用很多 data。如果共用的不多,除了浪費 table, model 也會被 getter 和 setter 污染。另外因為 class name 寫在 table 了,如果要修改 class name 會很麻煩。

這裡提的 MTI 作法是,還是為了要用到的 foreign key 開欄位,並且也為每個關係加上 belongs_to,但是允許 null 值,因為要做 XOR on columns: 要用 boolean 做 XOR,兩個還好做,超過三個就複雜了。這裡的作法很聰明,轉成數字再相加,只要檢查是不是等於 1,用這樣的方法做 database constrant check。

都加了 belongs_to,要怎麼做到多型。這裡作者使用了 reflection 和 inheritance relationship 的技巧,在 parent object 上實作了 getter 和 setter method,厲害。我們也可以實作一個 Factory method 在 parent 上,來建構適合的 sub-class。

第11章 View-Backed Models

有些複雜的 join 和 conditions 單靠 active record 寫不出來(畢竟簡單好用的東西,畢竟是犧牲一些不常使用的功能),而必須寫一些很醜的 SQL 混雜 active record,但是這樣就失去使用 active record 的意義(與DB-independent)。如果要漂亮,只能先盡可能先撈出來,然後再從 application layer 層過濾資料,但是這樣又失去效能,畢竟撈資料是 DB 的專長。

解法是 view-backed model,materialized view。

database view 有兩個定義 1. named subquery 2. a table that is defined by an algorithm
後者的實作是,我們先用 SQL 定義 DB view,然後就可以用 ActiveRecord 代表那個 view/table,更 cool 的是,還可以加上 association!! 當然,這是 read-only 的,記得不要加 destroy。

subquery 的定義也表示,你不能對 view/ view-backed models 操作 insert/update/delete/reference/constraint/index。
references 也不需要,如果你要 reference,就表示你應該加到 view 裡,constraint 也是。

indexing 也無,畢竟是 just-in-time 撈出來的資料。但是本來的 index 欄位當然還是有作用。別氣餒,畢竟本來使用 View 的目的不是效能,而是為了讓 AR 更簡潔清楚。如果真的需要效能,下一章的 materialized view。

第12章 Materialized Views

materialized view 就是 cached 的 view。既然是 cache,就一樣有 cache 的議題: 資料的更新,何時由誰 expire?

materialized view 是 cache-complete copy of view, 也是全部都有留資料,不像 memcached 會把不常用或超過記憶體的資料移除。

幾個重點: 1. materialized view 既然是實體的 table,也就可以加 index 2. 要加上 refresh function 當偵測到 base table 有修改 3. 或是 invalidation function 如果不想馬上更新 4. 在 base table 加上 triggers 以偵測修改 5. auxiliart view, reconciler view 隱藏實作細節

不過看完還是覺得使用 materialized view 真是挺難的!! XD

參與討論

3 則留言

  1. 從很久以前覺得為啥明明就不符合真正RDBMS定義的MySQL會熱門是一件很怪的事情, 不過這世界就是這樣, 連Access都可以活到現在了~見怪不怪吧!

  2. 有沒有真正符合 RDBMS 定義跟流不流行,有何關係? 以前 postgresql 確實不易安裝、中文有問題、缺少易用的 client 工具,這些應該都是造成不太流行的原因。

發佈留言

發表迴響