一、概述
物件導向程式設計和關係型資料庫,都是目前最流行的技術,但是它們的模型是不一樣的。
物件導向程式設計把所有實體看成物件(object),關係型資料庫則是採用實體之間的關係(relation)連線資料。很早就有人提出,關係也可以用物件表達,這樣的話,就能使用物件導向程式設計,來操作關係型資料庫。
簡單說,ORM 就是通過例項物件的語法,完成關係型資料庫的操作的技術,是"物件-關係對映"(Object/Relational Mapping) 的縮寫。
ORM 把資料庫對映成物件。
- 資料庫的表(table) --> 類(class)
- 記錄(record,行資料)--> 物件(object)
- 欄位(field)--> 物件的屬性(attribute)
舉例來說,下面是一行 SQL 語句。
SELECT id, first_name, last_name, phone, birth_date, sex FROM persons WHERE id = 10
程式直接執行 SQL,運算元據庫的寫法如下。
res = db.execSql(sql); name = res[0]["FIRST_NAME"];
改成 ORM 的寫法如下。
p = Person.get(10); name = p.first_name;
一比較就可以發現,ORM 使用物件,封裝了資料庫操作,因此可以不碰 SQL 語言。開發者只使用物件導向程式設計,與資料物件直接互動,不用關心底層資料庫。
總結起來,ORM 有下面這些優點。
- 資料模型都在一個地方定義,更容易更新和維護,也利於重用程式碼。
- ORM 有現成的工具,很多功能都可以自動完成,比如資料消毒、預處理、事務等等。
- 它迫使你使用 MVC 架構,ORM 就是天然的 Model,最終使程式碼更清晰。
- 基於 ORM 的業務程式碼比較簡單,程式碼量少,語義性好,容易理解。
- 你不必編寫效能不佳的 SQL。
但是,ORM 也有很突出的缺點。
- ORM 庫不是輕量級工具,需要花很多精力學習和設定。
- 對於複雜的查詢,ORM 要麼是無法表達,要麼是效能不如原生的 SQL。
- ORM 抽象掉了資料庫層,開發者無法瞭解底層的資料庫操作,也無法定製一些特殊的 SQL。
二、命名規定
許多語言都有自己的 ORM 庫,最典型、最規範的實現公認是 Ruby 語言的 Active Record。Active Record 對於物件和資料庫表的對映,有一些命名限制。
(1)一個類對應一張表。類名是單數,且首字母大寫;表名是複數,且全部是小寫。比如,表books
對應類Book
。
(2)如果名字是不規則複數,則類名依照英語習慣命名,比如,表mice
對應類Mouse
,表people
對應類Person
。
(3)如果名字包含多個單詞,那麼類名使用首字母全部大寫的駱駝拼寫法,而表名使用下劃線分隔的小寫單詞。比如,表book_clubs
對應類BookClub
,表line_items
對應類LineItem
。
(4)每個表都必須有一個主鍵欄位,通常是叫做id
的整數字段。外來鍵欄位名約定為單數的表名 + 下劃線 + id,比如item_id
表示該欄位對應items
表的id
欄位。
三、示例庫
下面使用 OpenRecord 這個庫,演示如何使用 ORM。
OpenRecord 是仿 Active Record 的,將其移植到了 JavaScript,而且實現得很輕量級,學習成本較低。我寫了一個示例庫,請將它克隆到本地。
$ git clone https://github.com/ruanyf/openrecord-demos.git
然後,安裝依賴。
$ cd openrecord-demos $ npm install
示例庫裡面的資料庫,是從網上拷貝的 Sqlite 資料庫。它的 Schema 圖如下(PDF 大圖下載)。
四、連線資料庫
使用 ORM 的第一步,就是你必須告訴它,怎麼連線資料庫(完整程式碼看這裡)。
// demo01.js const Store = require('openrecord/store/sqlite3'); const store = new Store({ type: 'sqlite3', file: './db/sample.db', autoLoad: true, }); await store.connect();
連線成功以後,就可以運算元據庫了。
五、Model
5.1 建立 Model
連線資料庫以後,下一步就要把資料庫的表,轉成一個類,叫做資料模型(Model)。下面就是一個最簡單的 Model(完整程式碼看這裡)。
// demo02.js class Customer extends Store.BaseModel { } store.Model(Customer);
上面程式碼新建了一個Customer
類,ORM(OpenRecord)會自動將它對映到customers
表。使用這個類就很簡單。
// demo02.js const customer = await Customer.find(1); console.log(customer.FirstName, customer.LastName);
上面程式碼中,查詢資料使用的是 ORM 提供的find()
方法,而不是直接操作 SQL。Customer.find(1)
表示返回id
為1
的記錄,該記錄會自動轉成物件,customer.FirstName
屬性就對應FirstName
欄位。
5.2 Model 的描述
Model 裡面可以詳細描述資料庫表的定義,並且定義自己的方法(完整程式碼看這裡)。
// demo03.js class Customer extends Store.BaseModel { static definition(){ this.attribute('CustomerId', 'integer', { primary: true }); this.attribute('FirstName', 'string'); this.attribute('LastName', 'string'); this.validatesPresenceOf('FirstName', 'LastName'); } getFullName(){ return this.FirstName + ' ' + this.LastName; } }
上面程式碼告訴 Model,CustomerId
是主鍵,FirstName
和LastName
是字串,並且不得為null
,還定義了一個getFullName()
方法。
例項物件可以直接呼叫getFullName()
方法。
// demo03.js const customer = await Customer.find(1); console.log(customer.getFullName());
六、CRUD 操作
資料庫的基本操作有四種:create
(新建)、read
(讀取)、update
(更新)和delete
(刪除),簡稱 CRUD。
ORM 將這四類操作,都變成了物件的方法。
6.1 查詢
前面已經說過,find()
方法用於根據主鍵,獲取單條記錄(完整程式碼看這裡)或多條記錄(完整程式碼看這裡)。
// 返回單條記錄 // demo02.js Customer.find(1) // 返回多條記錄 // demo05.js Customer.find([1, 2, 3])
where()
方法用於指定查詢條件(完整程式碼看這裡)。
// demo04.js Customer.where({Company: 'Apple Inc.'}).first()
如果直接讀取類,將返回所有記錄。
// 返回所有記錄 const customers = await Customer;
但是,通常不需要返回所有記錄,而是使用limit(limit[, offset])
方法指定返回記錄的位置和數量(完整程式碼看這裡)。
// demo06.js const customers = await Customer.limit(5, 10);)
上面的程式碼制定從第10條記錄開始,返回5條記錄。
6.2 新建記錄
create()
方法用於新建記錄(完整程式碼看這裡)。
// demo12.js Customer.create({ Email: 'president@whitehouse.gov', FirstName: 'Donald', LastName: 'Trump', Address: 'Whitehouse, Washington' })
6.3 更新記錄
update()
方法用於更新記錄(完整程式碼看這裡)。
// demo13.js const customer = await Customer.find(60); await customer.update({ Address: 'Whitehouse' });
6.4 刪除記錄
destroy()
方法用於刪除記錄(完整程式碼看這裡)。
// demo14.js const customer = await Customer.find(60); await customer.destroy();
七、關係
7.1 關係型別
表與表之間的關係(relation),分成三種。
- 一對一(one-to-one):一種物件與另一種物件是一一對應關係,比如一個學生只能在一個班級。
- 一對多(one-to-many): 一種物件可以屬於另一種物件的多個例項,比如一張唱片包含多首歌。
- 多對多(many-to-many):兩種物件彼此都是"一對多"關係,比如一張唱片包含多首歌,同時一首歌可以屬於多張唱片。
7.2 一對一關係
設定"一對一關係",需要設定兩個 Model。舉例來說,假定顧客(Customer
)和發票(Invoice
)是一對一關係,一個顧客對應一張發票,那麼需要設定Customer
和Invoice
這兩個 Model。
Customer
內部使用this.hasOne()
方法,指定每個例項對應另一個 Model 的一個例項。
class Customer extends Store.BaseModel { static definition(){ this.hasOne('invoices', {model: 'Invoice', from: 'CustomerId', to: 'CustomerId'}); } }
上面程式碼中,this.hasOne(name, option)
的第一個引數是該關係的名稱,可以隨便起,只要引用的時候保持一致就可以了。第二個引數是關係的配置,這裡只用了三個屬性。
- model:對方的 Model 名
- from:當前 Model 對外連線的欄位,一般是當前表的主鍵。
- to:對方 Model 對應的欄位,一般是那個表的外來鍵。上面程式碼是
Customer
的CustomerId
欄位,對應Invoice
的CustomerId
欄位。
然後,Invoice
內部使用this.belongsTo()
方法,回應Customer.hasOne()
方法。
class Invoice extends Store.BaseModel { static definition(){ this.belongsTo('customer', {model: 'Customer', from: 'CustomerId', to: 'CustomerId'}); } }
接下來,查詢的時候,要用include(name)
方法,將對應的 Model 包括進來。
const invoice = await Invoice.find(1).include('customer'); const customer = await invoice.customer; console.log(customer.getFullName());
上面程式碼中,Invoice.find(1).include('customer')
表示Invoice
的第一條記錄要用customer
關係,將Customer
這個 Model 包括進來。也就是說,可以從invoice.customer
屬性上,讀到對應的那一條 Customer 的記錄。
7.3 一對多關係
上一小節假定 Customer 和 Invoice 是一對一關係,但是實際上,它們是一對多關係,因為一個顧客可以有多張發票。
一對多關係的處理,跟一對一關係很像,唯一的區別就是把this.hasOne()
換成this.hasMany()
方法。從名字上就能看出,這個方法指定了 Customer 的一條記錄,對應多個 Invoice(完整程式碼看這裡)。
// demo08.js class Customer extends Store.BaseModel { static definition(){ this.hasMany('invoices', {model: 'Invoice', from: 'CustomerId', to: 'CustomerId'}); } } class Invoice extends Store.BaseModel { static definition(){ this.belongsTo('customer', {model: 'Customer', from: 'CustomerId', to: 'CustomerId'}); } }
上面程式碼中,除了this.hasMany()
那一行,其他都跟上一小節完全一樣。
7.4 多對多關係
通常來說,"多對多關係"需要有一張中間表,記錄另外兩張表之間的對應關係。比如,單曲Track
和歌單Playlist
之間,就是多對多關係:一首單曲可以包括在多個歌單,一個歌單可以包括多首單曲。資料庫實現的時候,就需要一張playlist_track
表來記錄單曲和歌單的對應關係。
因此,定義 Model 就需要定義三個 Model(完整程式碼看這裡)。
// demo10.js class Track extends Store.BaseModel{ static definition() { this.hasMany('track_playlists', { model: 'PlaylistTrack', from: 'TrackId', to: 'TrackId'}); this.hasMany('playlists', { model: 'Playlist', through: 'track_playlists' }); } } class Playlist extends Store.BaseModel{ static definition(){ this.hasMany('playlist_tracks', { model: 'PlaylistTrack', from: 'PlaylistId', to: 'PlaylistId' }); this.hasMany('tracks', { model : 'Track', through: 'playlist_tracks' }); } } class PlaylistTrack extends Store.BaseModel{ static definition(){ this.tableName = 'playlist_track'; this.belongsTo('playlists', { model: 'Playlist', from: 'PlaylistId', to: 'PlaylistId'}); this.belongsTo('tracks', { model: 'Track', from: 'TrackId', to: 'TrackId'}); } }
上面程式碼中,Track
這個 Model 裡面,通過this.hasMany('playlists')
指定對應多個歌單。但不是直接關聯,而是通過through
屬性,指定中間關係track_playlists
進行關聯。所以,Track 也要通過this.hasMany('track_playlists')
,指定跟中間表的一對多關係。相應地,PlaylistTrack
這個 Model 裡面,要用兩個this.belongsTo()
方法,分別跟另外兩個 Model 進行連線。
查詢的時候,不用考慮中間關係,就好像中間表不存在一樣。
// demo10.js const track = await Track.find(1).include('playlists'); const playlists = await track.playlists; playlists.forEach(l => console.log(l.PlaylistId));
上面程式碼中,一首單曲對應多張歌單,所以track.playlists
返回的是一個陣列。
(完)