【數據庫】ORM 原理及實例教程

一、概述

面向對象編程和關係型數據庫,都是目前最流行的技術,但是它們的模型是不一樣的。

面向對象編程把所有實體看成對象(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 對於對象和數據庫表的映射,有一些命名限制。

(1)一個類對應一張表。類名是單數,且首字母大寫;表名是複數,且全部是小寫。比如,表books對應類Book。(2)如果名字是不規則複數,則類名依照英語習慣命名,比如,表mice對應類Mouse,表people對應類Person。(3)如果名字包含多個單詞,那麼類名使用首字母全部大寫的駱駝拼寫法,而表名使用下劃線分隔的小寫單詞。比如,表book_clubs對應類BookClub,表line_items對應類LineItem。(4)每個表都必須有一個主鍵字段,通常是叫做id的整數字段。外鍵字段名約定為單數的表名 + 下劃線 + id,比如item_id表示該字段對應items表的id字段。

三、示例庫

下面使用 這個庫,演示如何使用 ORM。

OpenRecord 是仿 Active Record 的,將其移植到了 JavaScript,而且實現得很輕量級,學習成本較低。我寫了一個 ,請將它克隆到本地。

$ git clone https://github.com/ruanyf/openrecord-demos.git

然後,安裝依賴。

$ cd openrecord-demos
$ npm install

示例庫裡面的數據庫,是從 的 Sqlite 數據庫。它的 Schema 圖如下( 大圖下載)。

四、連接數據庫

使用 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: '',

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返回的是一個數組。