使用TS+Sequelize實現更簡潔的CRUD

若是是常用Node來作服務端開發的童鞋,確定不可避免的會操做數據庫,作一些增刪改查(CRUDCreate Read Update Delete)的操做,若是是一些簡單的操做,相似定時腳本什麼的,可能就直接生寫SQL語句來實現功能了,而若是是在一些大型項目中,數十張、上百張的表,之間還會有一些(一對多,多對多)的映射關係,那麼引入一個ORMObject Relational Mapping)工具來幫助咱們與數據庫打交道就能夠減輕一部分沒必要要的工做量,Sequelize就是其中比較受歡迎的一個。javascript

CRUD原始版 手動拼接SQL

先來舉例說明一下直接拼接SQL語句這樣比較「底層」的操做方式:java

CREATE TABLE animal (
  id INT AUTO_INCREMENT,
  name VARCHAR(14) NOT NULL,
  weight INT NOT NULL, 
  PRIMARY KEY (`id`)
);
複製代碼

建立這樣的一張表,三個字段,自增ID、name以及weight
若是使用mysql這個包來直接操做數據庫大概是這樣的:node

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語句都要進行修改(由於各個數據庫的方言可能有區別)mysql

CRUD進階版 Sequelize的使用

關於記憶這件事情,機器確定會比人腦更靠譜兒,因此就有了ORM,這裏就用到了在Node中比較流行的Sequelizegit

ORM是幹嗎的

首先可能須要解釋下ORM是作什麼使的,能夠簡單地理解爲,使用面向對象的方式,經過操做對象來實現與數據庫以前的交流,完成CRUD的動做。
開發者並不須要關心數據庫的類型,也不須要關心實際的表結構,而是根據當前編程語言中對象的結構與數據庫中表、字段進行映射。github

就比如針對上邊的animal表進行操做,再也不須要在代碼中去拼接SQL語句,而是直接調用相似Animal.createAnimal.find就能夠完成對應的動做。sql

Sequelize的使用方式

首先咱們要先下載Sequelize的依賴:typescript

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定義模型相關的各類配置:docsnpm

拋開模型定義的部分,使用Sequelize無疑減輕了不少使用上的成本,由於模型的定義通常不太會去改變,一次定義屢次使用,而使用手動拼接SQL的方式可能就須要將一段SQL改來改去的。

並且能夠幫助進行字段類型的轉換,避免出現類型強制轉換出錯NaN或者數字被截斷等一些粗心致使的錯誤。

經過定義模型的方式來告訴程序,有哪些模型,模型的字段都是什麼,讓程序來幫助咱們記憶,而非讓咱們本身去記憶。
咱們只須要拿到對應的模型進行操做就行了。

這還不夠

But,雖然說切換爲ORM工具已經幫助咱們減小了很大一部分的記憶成本,可是依然還不夠,咱們仍然須要知道模型中都有哪些字段,才能在業務邏輯中進行使用,若是新人接手項目,仍然須要去翻看模型的定義才能知道有什麼字段,因此就有了今天要說的真正的主角兒:sequelize-typescript

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 Model<Animal> {
  @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不一樣的有這麼幾點:

  1. 模型的定義採用裝飾器的方式來定義
  2. 實例化Sequelize對象時須要指定對應的model路徑
  3. 模型相關的一系列方法都是支持Promise

若是在使用過程當中遇到提示XXX used before model init,能夠嘗試在實例化前邊添加一個await操做符,等到與數據庫的鏈接創建完成之後再進行操做

可是好像看起來這樣寫的代碼相較於Sequelize多了很多呢,並且至少須要兩個文件來配合,那麼這麼作的意義是什麼的?
答案就是OOP中一個重要的理念:繼承

使用Sequelize-typescript實現模型的繼承

由於TypeScript的核心開發人員中包括C#的架構師,因此TypeScript中能夠看到不少相似C#的痕跡,在模型的這方面,咱們能夠嘗試利用繼承減小一些冗餘的代碼。

好比說咱們基於animal表又有了兩張新表,dogbird,這二者之間確定是有區別的,因此就有了這樣的定義:

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 Model<Animal> {
  @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開發時能夠獲得不少動態提示,相似findAllcreate之類的操做都會有提示:

Animal.create<Animal>({
  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 Model<Animal> {
  @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數組
複製代碼

同理,由於上邊咱們的兩個DogBird繼承自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 getList<T extends Animal>() {
    const results = await this.findAll({
      raw: true,
    })
    return results as T[]
  }
}

const dogList = await Dog.getList<Dog>()
// 或者不做任何修改,直接在外邊手動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
複製代碼

小結

本文只是一個引子,一些簡單的示例,只爲體現出三者(SQLSequelizeSequelize-typescript)之間的區別,Sequelize中有更多高階的操做,相似映射關係之類的,這些在Sequelize-typescript中都有對應的體現,並且由於使用了裝飾器,實現這些功能所需的代碼會減小不少,看起來也會更清晰。

固然了,ORM這種東西也不是說要一股腦的上,若是是初學者,從我的層面上我不建議使用,由於這樣會少了一個接觸SQL的機會
若是項目結構也不是很複雜,或者可預期的將來也不會太複雜,那麼使用ORM也沒有什麼意義,還讓項目結構變得複雜起來
以及,必定程度上來講,通用就意味着妥協,爲了保證多個數據庫之間的效果都一致,可能會拋棄一些數據庫獨有的特性,若是明確的須要使用這些特性,那麼ORM也不會太適合
選擇最合適的,要知道使用某樣東西的意義

最終的一個示例放在了GitHub上:notebook | typescript/sequelize

參考資料:

相關文章
相關標籤/搜索