使用TS+Sequelize實現更簡潔的CRUD
如果是經常使用Node來做服務端開發的童鞋,肯定不可避免的會運算元據庫,做一些增刪改查(CRUD
,Create Read Update Delete
)的操作,如果是一些簡單的操作,類似定時指令碼什麼的,可能就直接生寫SQL語句來實現功能了,而如果是在一些大型專案中,數十張、上百張的表,之間還會有一些(一對多,多對多)的對映關係,那麼引入一個ORM
(Object Relational Mapping
)工具來幫助我們與資料庫打交道就可以減輕一部分不必要的工作量,Sequelize
就是其中比較受歡迎的一個。
CRUD原始版 手動拼接SQL
先來舉例說明一下直接拼接SQL
語句這樣比較“底層”的操作方式:
CREATE TABLE animal (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
PRIMARY KEY (`id`)
);
建立這樣的一張表,三個欄位,自增ID、name
以及weight
。
如果使用mysql
這個包來直接運算元據庫大概是這樣的:
const connection = mysql.createConnection({})
const tableName = 'animal'
connection.connect()
// 我們假設已經支援了Promise
// 查詢
const [results] = await connection.query(`
SELECT
id,
name,
weight
FROM ${tableName}
`)
// 新增
const name = 'Niko'
const weight = 70
await connection.query(`
INSERT INTO ${tableName} (name, weight)
VALUES ('${name}', ${weight})
`)
// 或者透過傳入一個Object的方式也可以做到
await connection.query(`INSERT INTO ${tableName} SET ?`, {
name,
weight
})
connection.end()
看起來也還算是比較清晰,但是這樣帶來的問題就是,開發人員需要對錶結構足夠的瞭解。
如果表中有十幾個欄位,對於開發人員來說這會是很大的記憶成本,你需要知道某個欄位是什麼型別,拼接SQL
時還要注意插入時的順序及型別,WHERE
條件對應的查詢引數型別,如果修改某個欄位的型別,還要去處理對應的傳參。
這樣的專案尤其是在進行交接的時候更是一件恐怖的事情,新人又需要從頭學習這些表結構。
以及還有一個問題,如果有哪天需要更換資料庫了,放棄了MySQL
,那麼所有的SQL
語句都要進行修改(因為各個資料庫的方言可能有區別)
CRUD進階版 Sequelize的使用
關於記憶這件事情,機器肯定會比人腦更靠譜兒,所以就有了ORM
,這裡就用到了在Node
中比較流行的Sequelize
。
ORM是幹嘛的
首先可能需要解釋下ORM
是做什麼使的,可以簡單地理解為,使用物件導向的方式,透過操作物件來實現與資料庫之前的交流,完成CRUD
的動作。
開發者並不需要關心資料庫的型別,也不需要關心實際的表結構,而是根據當前程式語言中物件的結構與資料庫中表、欄位進行對映。
就好比針對上邊的animal
表進行操作,不再需要在程式碼中去拼接SQL
語句,而是直接呼叫類似Animal.create
,Animal.find
就可以完成對應的動作。
Sequelize的使用方式
首先我們要先下載Sequelize
的依賴:
npm i sequelize
npm i mysql2 # 以及對應的我們需要的資料庫驅動
然後在程式中建立一個Sequelize
的例項:
const Sequelize = require('Sequelize')
const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1:3306/ts_test')
// dialect://username:password@host:port/db_name
// 針對上述的表,我們需要先建立對應的模型:
const Animal = sequelize.define('animal', {
id: { type: Sequelize.INTEGER, autoIncrement: true },
name: { type: Sequelize.STRING, allowNull: false },
weight: { type: Sequelize.INTEGER, allowNull: false },
}, {
// 禁止sequelize修改表名,預設會在animal後邊新增一個字母`s`表示負數
freezeTableName: true,
// 禁止自動新增時間戳相關屬性
timestamps: false,
})
// 然後就可以開始使用咯
// 還是假設方法都已經支援了Promise
// 查詢
const results = await Animal.findAll({
raw: true,
})
// 新增
const name = 'Niko'
const weight = 70
await Animal.create({
name,
weight,
})
sequelize定義模型相關的各種配置:
拋開模型定義的部分,使用Sequelize
無疑減輕了很多使用上的成本,因為模型的定義一般不太會去改變,一次定義多次使用,而使用手動拼接SQL
的方式可能就需要將一段SQL
改來改去的。
而且可以幫助進行欄位型別的轉換,避免出現型別強制轉換出錯NaN
或者數字被截斷等一些粗心導致的錯誤。
透過定義模型的方式來告訴程式,有哪些模型,模型的欄位都是什麼,讓程式來幫助我們記憶,而非讓我們自己去記憶。
我們只需要拿到對應的模型進行操作就好了。
這還不夠
But,雖說切換為ORM
工具已經幫助我們減少了很大一部分的記憶成本,但是依然還不夠,我們仍然需要知道模型中都有哪些欄位,才能在業務邏輯中進行使用,如果新人接手專案,仍然需要去翻看模型的定義才能知道有什麼欄位,所以就有了今天要說的真正的主角兒:
CRUD終極版 裝飾器實現模型定義
Sequelize-typescript
是基於Sequelize
針對TypeScript
所實現的一個增強版本,拋棄了之前繁瑣的模型定義,使用裝飾器直接達到我們想到的目的。
Sequelize-typescript的使用方式
首先因為是用到了TS
,所以環境依賴上要安裝的東西會多一些:
# 這裡採用ts-node來完成舉例
npm i ts-node typescript
npm i sequelize reflect-metadata sequelize-typescript
其次,還需要修改TS
專案對應的tsconfig.json
檔案,用來讓TS
支援裝飾器的使用:
{
"compilerOptions": {
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
}
}
然後就可以開始編寫指令碼來進行開發了,與Sequelize
不同之處基本在於模型定義的地方:
// /modles/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'
@Table({
tableName: 'animal'
})
export class Animal extends ModelAnimal> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: number
@Column
name: string
@Column
weight: number
}
// 建立與資料庫的連結、初始化模型
// app.ts
import path from 'path'
import { Sequelize } from 'sequelize-typescript'
import Animal from './models/animal'
const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1:3306/ts_test')
sequelize.addModels([path.resolve(__dirname, `./models/`)])
// 查詢
const results = await Animal.findAll({
raw: true,
})
// 新增
const name = 'Niko'
const weight = 70
await Animal.create({
name,
weight,
})
與普通的Sequelize
不同的有這麼幾點:
- 模型的定義採用裝飾器的方式來定義
- 例項化
Sequelize
物件時需要指定對應的model
路徑 - 模型相關的一系列方法都是支援
Promise
的
如果在使用過程中遇到提示XXX used before model init
,可以嘗試在例項化前邊新增一個await
運算子,等到與資料庫的連線建立完成以後再進行操作
但是好像看起來這樣寫的程式碼相較於Sequelize
多了不少呢,而且至少需要兩個檔案來配合,那麼這麼做的意義是什麼的?
答案就是OOP
中一個重要的理念:繼承。
使用Sequelize-typescript實現模型的繼承
因為TypeScript
的核心開發人員中包括C#
的架構師,所以TypeScript
中可以看到很多類似C#
的痕跡,在模型的這方面,我們可以嘗試利用繼承減少一些冗餘的程式碼。
比如說我們基於animal
表又有了兩張新表,dog
和bird
,這兩者之間肯定是有區別的,所以就有了這樣的定義:
CREATE TABLE dog (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
leg INT NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE bird (
id INT AUTO_INCREMENT,
name VARCHAR(14) NOT NULL,
weight INT NOT NULL,
wing INT NOT NULL,
claw INT NOT NULL,
PRIMARY KEY (`id`)
);
關於dog
我們有一個腿leg
數量的描述,關於bird
我們有了翅膀wing
和爪子claw
數量的描述。
特意讓兩者的特殊欄位數量不同,省的有槓精說可以透過新增type
欄位區分兩種不同的動物 :p
如果要用Sequelize
的方式,我們就要將一些相同的欄位定義define
三遍才能實現,或者說寫得靈活一些,將define
時使用的Object
抽出來使用Object.assign
的方式來實現類似繼承的效果。
但是在Sequelize-typescript
就可以直接使用繼承來實現我們想要的效果:
// 首先還是我們的Animal模型定義
// /models/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'
@Table({
tableName: 'animal'
})
export default class Animal extends ModelAnimal> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: number
@Column
name: string
@Column
weight: number
}
// 接下來就是繼承的使用了
// /models/dog.ts
import { Table, Column, Model } from 'sequelize-typescript'
import Animal from './animal'
@Table({
tableName: 'dog'
})
export default class Dog extends Animal {
@Column
leg: number
}
// /models/bird.ts
import { Table, Column, Model } from 'sequelize-typescript'
import Animal from './animal'
@Table({
tableName: 'bird'
})
export default class Bird extends Animal {
@Column
wing: number
@Column
claw: number
}
有一點需要注意的:每一個模型需要單獨佔用一個檔案,並且採用export default
的方式來匯出
也就是說目前我們的檔案結構是這樣的:
├── models
│ ├── animal.ts
│ ├── bird.ts
│ └── dog.ts
└── app.ts
得益於TypeScript
的靜態型別,我們能夠很方便地得知這些模型之間的關係,以及都存在哪些欄位。
在結合著VS Code
開發時可以得到很多動態提示,類似findAll
,create
之類的操作都會有提示:
Animal.createAnimal>({
abc: 1,
// ^ abc不是Animal已知的屬性
})
透過繼承來複用一些行為
上述的例子也只是說明了如何複用模型,但是如果是一些封裝好的方法呢?
類似的獲取表中所有的資料,可能一般情況下獲取JSON
資料就夠了,也就是findAll({raw: true})
所以我們可以針對類似這樣的操作進行一次簡單的封裝,不需要開發者手動去呼叫findAll
:
// /models/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'
@Table({
tableName: 'animal'
})
export default class Animal extends ModelAnimal> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: number
@Column
name: string
@Column
weight: number
static async getList () {
return this.findAll({raw: true})
}
}
// /app.ts
// 這樣就可以直接呼叫`getList`來實現類似的效果了
await Animal.getList() // 返回一個JSON陣列
同理,因為上邊我們的兩個Dog
和Bird
繼承自Animal
,所以程式碼不用改動就可以直接使用getList
了。
const results = await Dog.getList()
results[0].leg // TS提示錯誤
但是如果你像上邊那樣使用的話,TS會提示錯誤的:[ts] 型別“Animal”上不存在屬性“leg”。
。
哈哈,這又是為什麼呢?細心的同學可能會發現,getList
的返回值是一個Animal[]
型別的,所以上邊並沒有leg
屬性,Bird
的兩個屬性也是如此。
所以我們需要教TS
認識我們的資料結構,這樣就需要針對Animal
的定義進行修改了,用到了 範型。
我們透過在函式上邊新增一個範型的定義,並且新增限制保證傳入的範型型別一定是繼承自Animal
的,在返回值轉換其型別為T
,就可以實現功能了。
class Animal {
static async getListT extends Animal>() {
const results = await this.findAll({
raw: true,
})
return results as T[]
}
}
const dogList = await Dog.getListDog>()
// 或者不作任何修改,直接在外邊手動as也可以實現類似的效果
// 但是這樣還是不太靈活,因為你要預先知道返回值的具體型別結構,將預期型別傳遞給函式,由函式去組裝返回的型別還是比較推薦的
const dogList = await Dog.getList() as Dog[]
console.log(dogList[0].leg) // success
這時再使用leg
屬性就不會出錯了,如果要使用範型,一定要記住新增extends Animal
的約束,不然TS
會認為這裡可以傳入任意型別,那麼很難保證可以正確的相容Animal
,但是繼承自Animal
的一定是可以相容的。
當然如果連這裡的範型或者as
也不想寫的話,還可以在子類中針對父類方法進行重寫。
並不需要完整的實現邏輯,只需要獲取返回值,然後修改為我們想要的型別即可:
class Dog extends Animal {
static async getList() {
// 呼叫父類方法,然後將返回值指定為某個型別
const results = await super.getList()
return results as Dog[]
}
}
// 這樣就可以直接使用方法,而不用擔心返回值型別了
const dogList = await Dog.getList()
console.log(dogList[0].leg) // success
小結
本文只是一個引子,一些簡單的示例,只為體現出三者(SQL
、Sequelize
和Sequelize-typescript
)之間的區別,Sequelize
中有更多高階的操作,類似對映關係之類的,這些在Sequelize-typescript
中都有對應的體現,而且因為使用了裝飾器,實現這些功能所需的程式碼會減少很多,看起來也會更清晰。
當然了,ORM
這種東西也不是說要一股腦的上,如果是初學者,從個人層面上我不建議使用,因為這樣會少了一個接觸SQL的機會
如果專案結構也不是很複雜,或者可預期的未來也不會太複雜,那麼使用ORM
也沒有什麼意義,還讓專案結構變得複雜起來
以及,一定程度上來說,通用就意味著妥協,為了保證多個資料庫之間的效果都一致,可能會拋棄一些資料庫獨有的特性,如果明確的需要使用這些特性,那麼ORM
也不會太適合
選擇最合適的,要知道使用某樣東西的意義
最終的一個示例放在了GitHub上:
參考資料:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2459/viewspace-2813987/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 使用rails實現最簡單的CRUDAI
- 使用Node+React實現簡單CRUDReact
- 使用PreparedStatement實現CRUD操作
- 使用 Laravel Eloquent 構造器讓模型更簡潔Laravel模型
- 讓你的DEVONthink UI 介面更簡潔?devUI
- 如何讓我們的模型更簡潔模型
- 使用AsyncAPI規範簡潔實現CQRS事件溯源案例API事件
- Python 3.8新功能盤點:更快,更簡潔,更一致,更現代化Python
- 使用 Macro 讓你的程式碼更簡潔,更具有可讀性Mac
- MultiItem進階 使用DataBinding 讓 RecyclerView程式碼更簡潔清爽View
- Flutter GetX使用---簡潔的魅力!Flutter
- 實現檔案拖放的一種簡潔方法 (轉)
- 簡潔地使用 vim
- 【JS】裝飾器讓你的程式碼更簡潔JS
- 通過 Laravel 訊息通知使用 EasySms 簡訊服務,讓你的程式碼更簡潔Laravel
- MVC5使用Angular.Js實現CrudMVCAngularJS
- [翻譯] Async/Await 使你的程式碼更簡潔AI
- Spring Boot 整合 Lombok 讓程式碼更簡潔Spring BootLombok
- SpringBoot實現mongoDB的CRUDSpring BootMongoDB
- Go Interface 的優雅使用,讓程式碼更整潔更容易測試Go
- 一個出於ARouter,卻更輕便簡潔的Route工具
- 瞭解CSS的查詢匹配原理,讓CSS更簡潔、高效CSS
- 分享常用的CSS函式,助你寫出更簡潔的程式碼CSS函式
- 使程式碼更簡潔(二)—-集合轉換相關
- 前端簡潔並實用的工具類前端
- Git命令簡潔使用指南Git
- SpringMVC中採用簡潔的配置實現檔案上傳SpringMVC
- Vue+Element UI實現CRUDVueUI
- Kotlin-for-Android : 讓你的Android程式碼更簡潔KotlinAndroid
- MyBatis 的簡單 CRUD 操作MyBatis
- [翻譯] 讓你的程式碼更簡短,更整潔,更易讀的ES6小技巧
- 9個JavaScript小技巧:寫出更簡潔,高效程式碼JavaScript
- 讓動畫實現更簡單,Flutter 動畫簡易教程!動畫Flutter
- 2021 Google 開發者大會 | 更簡潔、更高效,創造更流暢的移動端使用者體驗Go
- Android 時間軸的實現(RecyclerView更簡單)AndroidView
- SpringBoot整合Redis使用Restful風格實現CRUD功能Spring BootRedisREST
- [譯] 利用 Immutability(不可變性)編寫更為簡潔高效的程式碼
- Vue+Ant Design實現CRUDVue