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

一、概述

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

面向對象編程把所有實體看成對象(object),關係型數據庫則是採用實體之間的關係(relation)連接數據。很早就有人提出,關係也可以用對象表達,這樣的話,就能使用面向對象編程,來操作關係型數據庫。

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

簡單說,ORM 就是通過實例對象的語法,完成關係型數據庫的操作的技術,是"對象-關係映射"(Object/Relational Mapping) 的縮寫。

ORM 把數據庫映射成對象。

  • 數據庫的表(table) --> 類(class)
  • 記錄(record,行數據)--> 對象(object)
  • 字段(field)--> 對象的屬性(attribute)
【數據庫】ORM 原理及實例教程

舉例來說,下面是一行 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 原理及實例教程

四、連接數據庫

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



分享到:


相關文章: