搭建 nodeJS 伺服器之(2)sequelize

asVenus發表於2019-03-01

前言

《搭建 nodeJS 伺服器之(2)sequelize》是系列教程的第二部分。同時,本系列教程將會帶你從零架構一個五臟俱全的後端專案。

傳送門:

  • 《搭建 nodeJS 伺服器之(1)koa》 – 基於 nodeJs 平臺的下一代 web 開發框架
  • 《搭建 nodeJS 伺服器之(3)測試》 – mocha + supertest(敬請期待)
  • 《joi 完全指南》 – 物件模式驗證庫(敬請期待)

Mysql 與 Sequelize 的關係

開始之前,我們先要對 ORM 有個大致的瞭解,如果你還是可耐的新萌,暫可先跳過一節,如果你想成為一名鬥士,暫可漂洋過海再來看我,如果你擁著一顆熾熱的心,那麼,停下吧!請聽我一敘。

敘,何為 ORM ?

首先,讓我們請上一位人間大佬——百度百科(一副生無可戀可憐的表情看著你)

ORM(Object Relational Mapping),稱為物件關係對映,用於實現物件導向程式語言裡不同型別系統的資料之間的轉換。

最後,千呼萬喚始出根本不存在的神界大佬——wiki(呵呵)

物件關係對映(Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程式設計技術,用於實現物件導向程式語言裡不同型別系統的資料之間的轉換。

兩位大佬一言一語間揭露了一個事實,我們都是渣渣(灰),還是讓我說一句人話吧!ヾ(๑❛ ▿ ◠๑ )

Sequelize 呢!是一個基於 Promise 的 NodeJs ORM 庫,目前支援 ostgres, MySQL, SQLite 和 Microsoft SQL Server 等資料庫程式。Sequelize 相當一箇中間人負責兩者,誰呢?js 和 mysql 之間的交流,你沒看錯,他讓你可以用 js 的語法和概念蹂躪資料庫,同時還提供了一些更高階的抽象,降低了使用時的複雜度,甚至都不必知 mysql 是何方神聖。Σ>―(〃°ω°〃)♡→ 哇,開森不開森,心動不心動,熟悉的語言,熟悉的寫法,簡直是解放勞動力(懶癌患者)的神器呀!

(話鋒一轉)我們倘若要真想從臉到屁股徹底瞭解 sequelize,還必須看看它對 mysql 做了些什麼 ( ◞˟૩˟)◞

山迴路轉 mysql (⋟﹏⋞),讓我們看一下,Sequelize 中各部分與 Mysql 概念上的對應關係:

搭建 nodeJS 伺服器之(2)sequelize
  • 例項化 Sequelize 連線到 Database: 通過例項化 Sequelize 類,連線到資料庫程式指定的資料庫。
  • 定義 Model 對映 Table: 通過模型對映資料表的定義並代理操作方法
  • 指定 DataTypes 宣告 Data Types: 把資料庫的資料型別變成在 js 上下文中更合適的用法。
  • 使用 Op 生成 Where 子句 Operators: 為選項物件提供強大的解耦和安全檢測。
  • 關聯 Association 替代複雜的 Foreign Key 和 多表查詢: 用一套簡單的方法管理複雜的多表查詢。
  • 呼叫 Transcation 封裝 Transation : 對事務一層簡單而必要的封裝。

題外話:Myqsl 是什麼鬼,聽明白的求原諒,一臉懵逼的請無視

從一個小專案開始

一眨眼,一個,二個(數手指),都快三個月了,你要是還記得 《搭建 nodeJS 伺服器之(1)koa》 這篇文章,那真是祖墳上燒高香,記不得,呵呵(你肯定是沒看過)。

在系列教程的第一篇中我們(喂喂喂!誰和你我們的)被作者帶節奏,新建新建新建(檔案目錄),安裝安裝安裝(依賴),但卻那麼小氣只講了下 koa 外掛的用法和業務程式碼的組織方式。那麼,從本章開始,我們就有錢把“場景”和“資料”這兩位角兒請出來,唱一出對角戲。

劃重點:本篇教程分為入門和進階兩部分,入門部分會從頭到尾講解一個文章釋出平臺,支援多使用者、點贊、收藏和評論。進階部分會以問答的方式進一步解疑答惑(灰色引用部分的形式)。

開始之前,千萬別忘了先把 Sequelize 以及依賴包安裝到本地(丟~~又讓我安裝)。

npm i sequelize mysql2 -D
複製程式碼

第一步,連線到資料庫

Sequelize 是庫的入口類,可做兩件事情:

  1. 連線到資料庫
  2. 設定資料表的全域性配置。

所以嘛!暫且可把 Sequelize 的例項 看做 Mysql 中的 Database(資料庫)

// app/config/databse.config.js
/* ☝  你看,我可是把當前程式碼所放的檔案寫的明明白白 */

export default {
  // 開啟哪個資料庫
  database: `test`,
  // 使用者名稱
  username: `root`,
  // 密碼
  password: `1234`,
  // 使用哪個資料庫程式
  dialect: `mysql`,
  // 地址
  host: `localhost`,
  // 埠
  port: 3306,
  // 連線池
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  },
  // 資料表相關的全域性配置
  define: {
    // 是否凍結表名
    // 預設情況下,表名會轉換為複數形式
    freezeTableName: true,
    // 是否為表新增 createdAt 和 updatedAt 欄位
    // createdAt 記錄表的建立時間
    // updatedAt 記錄欄位更新時間
    timestamps: true,
    // 是否為表新增 deletedAt 欄位
    // 預設情況下, destroy() 方法會刪除資料,
    // 設定 paranoid 為 true 時,將會更新 deletedAt 欄位,並不會真實刪除資料。
    paranoid: false
  }
}
複製程式碼

匯入配置檔案,並例項化 Sequelize。

// app/models/test/index.js

import Sequelize from `sequelize`
import config from `../../config/database.config`

// 例項化,並指定配置
export const sequelize = new Sequelize(config)

// 測試連線
sequelize
  .authenticate()
  .then(() => {
    console.log(`Connection has been established successfully.`)
  })
  .catch(err => {
    console.error(`Unable to connect to the database:`, err)
  })
複製程式碼

劃重點:models 目錄用於存放 Sequelize 庫相關檔案,下層目錄對應 Sequelize 開啟 Mysql 中的 Database,每個下層目錄中的 index.js 主檔案用於整合 Model,而其他 .js 檔案對應當前 Database 中的一張 Table

搭建 nodeJS 伺服器之(2)sequelize

第二步,建立模型

Model 是由 sequelize.define()(sequelize 就是上小節中的例項) 方法定義用於對映資料模型和資料表之間的關係的物件模型。(╬◣д◢) 說人話,啊啊是,其實就是 Mysql 中的一張資料表啦。哈~哈~哈~哈~

那麼,你還不快 (╯>д<)╯⁽˙³˙⁾ 新建一個檔案 User.js 存放使用者表的模型定義,如下:

// /models/test/User.js

export default (sequelize, DataTypes) =>
  // define() 方法接受三個引數
  // 表名,表欄位的定義和表的配置資訊
  sequelize.define(`user`, {
    id: {
      // Sequelize 庫由 DataTypes 物件為欄位定義型別
      type: DataTypes.INTEGER(11),
      // 允許為空
      allowNull: false,
      // 主鍵
      primaryKey: true,
      // 自增
      autoIncrement: true,
    },
    username: {
      type: DataTypes.STRING,
      allowNull: false,
      // 唯一
      unique: true
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
  })
複製程式碼

然後,匯入並同步到 Mysql 中。

// /test/index.js

import Sequelize from `sequelize`
import config from `../../config/database.config`

export const sequelize = new Sequelize(config)

// 匯入
export const User = sequelize.import(__dirname + `/User`)
// 同步到 Mysql 中
// 也就是將我們用 js 物件宣告的模型通過 sequelize 轉換成 mysql 中真正的一張資料表
sequelize.sync()

// ...
複製程式碼

劃重點:推薦將所有的模型定義在 單檔案 中以實現模組化,並通過 sequelize.import() 方法把模組匯入到 index.js 中統一管理。

Sequelize 庫會為我們執行以下 Mysql 原生命令在 test 中建立一張名為 user 的資料表。

CREATE TABLE IF NOT EXISTS `user` (`id` INTEGER(11) NOT NULL auto_increment UNIQUE , `username` VARCHAR(255) NOT NULL UNIQUE, `password` VARCHAR(255) NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
複製程式碼
搭建 nodeJS 伺服器之(2)sequelize

sequelize.sync() 將模型同步到資料庫的三種方法和區別?

// 標準同步
// 只有當資料庫中不存在與模型同名的資料表時,才會同步
sequelize.sync()
// 動態同步
// 修改同名資料表結構,以適用模型。
sequelize.sync({alter: true})
// 強制同步
// 刪除同名資料表後同步,謹慎使用,會導致資料丟失
sequelize.sync({force: true})

// 另外,當你指定表與表之間的關聯後,修改被關聯的表結構時會丟擲異常。
// 需要先註釋掉關聯程式碼,然後更新同步模型後,再取消掉註釋即可。

// 再另外,當你有新的關聯時必須使用動態同步才會生效。
複製程式碼

同步成功後,我們把 sequelize.synce 註釋掉。因為,我們再次重啟應用後不需要再重新同步。

// sequelize.sync()
複製程式碼

然後,在 controllers 中建立同名 User.js 檔案,存放使用者相關的介面邏輯(註冊,登入,登出,查詢和刪除等)

// /controllers/User.js

import { User } from `../models/test/`


export default class {
  static async register (ctx) {
    const post = ctx.request.body
    let result
    try {
      // 呼叫模型的 create()  方法插入一行資料
      result = await User.create(post)
    } catch (error) {
      return ctx.body = {success: false, error}
    }

    ctx.body = {success: true, data: result}
  }
  // ...
}
複製程式碼

這裡你可能會有個疑問,就是控制器(controllers)中的邏輯分類應該是對應模型(model)還是路由(router)?其實這個問題很好回答,controllers 原本就是為了更好的管理 router 而分離出來的,而 router 的介面路徑也應該恰好能夠自我解釋控制器邏輯的作用,所以控制器中的邏輯分類應該按照路由區別。

最後,掛載至路由。

// /router.js
import Router from `koa-router`
const router = new Router

router.prefix(`/api/v1`)

import User from `./controllers/User`

router
  .post(`/register`, User.register)
  // ...

export default router
複製程式碼

開啟 postman(或者其他介面除錯工具)發起請求。

搭建 nodeJS 伺服器之(2)sequelize

檢視 Mysql。

搭建 nodeJS 伺服器之(2)sequelize

User.create() 方法返回的是一個由 Sequelize 庫定義的結果集類(模型上大部分直接運算元據庫的方法都返回這一結果集類)。

result = await User.create(post)

// 可直接獲取結果集中的欄位值
result.username
// 或者使用結果集物件提供的方法
result.getDataValue(`username`)
// 或者將結果集解析為一個 JSON 物件
result.toJSON()

// 踩坑必備
// 直接在結果集類上新增自定義資料是無效的
result.newAttr = `newValue`
// 呼叫 setDataValue 方法或者呼叫 toJSON() 將它轉換為一個物件
result.setDataValue(`newAttr`, `newValue`)
複製程式碼

那麼,為什麼可以直接將結果集物件賦值給 ctx.body?
因為在內部 koa 會把 ctx.body 序列化。

body = JSON.stringify(body)
複製程式碼

模型方法

除了使用者模型外,我們還需定義文章模型(article)、文章的點贊模型(article_like)、文章的收藏模型(article_star)和文章的評論模型(article_comment)。

這裡列出了模型上一些運算元據庫常用的方法。☞ 更多詳情,請移步官方

findOne()
findAll()
findById()
findOrCreate()
findOrBuild()
findAndCountAll()
create()
bulkCreate()
update()
upsert()
destroy()
increment()
decrement()
count()
max()
min()
sun()
複製程式碼

資料型別

DataTypes 物件為模型的欄位指定資料型別。

以下列出了部分 DataTypes 型別 對應的 Mysql 資料型別。☞ 更多詳情,請移步官方

// 字串
STRING(N=255)               // varchar(0~65535)
CHAR(N=255)                 // char(0~255)
TEXT(S=tiny/medium/long)    // s + text
複製程式碼
// 數字

// 整數
TINYINT(N?)         // tinyint(1-byte)
SMALLINT(N?)        // smallint(2-byte)
MEDIUMINT(N?)       // mediumint(3-byte)
INTEGER(N=255?)     // integer(6-byte)
BIGINT(N?)          // bigint(8-byte)

// 浮點數
FLOAT(n, n)         // float(4-byte)
DOUBLE(n, n)        // double(8-byte)
複製程式碼
// 布林值
BOOLEAN             // tinyint(1)
複製程式碼
// 日期
DATE(n?)            // datetime(8-byte)
TIME                // timestamp(4-byte)
NOW                 // 預設值為 current timestamp
複製程式碼
// 其他
ENUM( any,...)      // ENUM(`value1`, ...) length > 65535 
JSON                 // JSON
複製程式碼

integerbigintfloatdouble 都支援 unsignedzerofill 屬性

Sequelize.INTEGER(11).UNSIGNED.ZEROFILL
複製程式碼

驗證器

Model 為每個欄位都提供了驗證選項。


export default (sequelize, DataTypes) =>
  sequelize.define(`user`, {
    // ...
    email: {
      type: DataTypes.STRING(255),
      allowNull: true,
      validate: {
        isEmail: true
      }
    }
  })
複製程式碼

另外,可指定 argsmsg 自定義引數和錯誤訊息。

isEmail: {
    args: true, // 可省略,預設為 true
    msg: "郵箱格式不合法!"
}
複製程式碼

只有當建立(比如,呼叫 create() 方法)或更新(比如,呼叫 update() 方法)模型資料時,才會觸發驗證器,另外當設定 allowNull: true,且欄位值為 null 時,也不會觸發驗證器。僅當驗證器驗證通過時才會真實將操作同步到資料庫中。當驗證未通過時,會丟擲一個 SequelizeValidationError 異常物件(這也是為什麼,需要在資料庫操作的地方用 try catch 語句捕獲錯誤,防止 nodeJs 程式退出)。

以下列出了所有內建的驗證規則。☞ 更多詳情,請移步官方

validate: {
    is: ["^[a-z]+$",`i`],     // 只允許字母
    is: /^[a-z]+$/i,          // 與上一個示例相同,使用了真正的正規表示式
    not: ["[a-z]",`i`],       // 不允許字母
    isEmail: true,            // 檢查郵件格式 (foo@bar.com)
    isUrl: true,              // 檢查連線格式 (http://foo.com)
    isIP: true,               // 檢查 IPv4 (129.89.23.1) 或 IPv6 格式
    isIPv4: true,             // 檢查 IPv4 (129.89.23.1) 格式
    isIPv6: true,             // 檢查 IPv6 格式
    isAlpha: true,            // 只允許字母
    isAlphanumeric: true,     // 只允許使用字母數字
    isNumeric: true,          // 只允許數字
    isInt: true,              // 檢查是否為有效整數
    isFloat: true,            // 檢查是否為有效浮點數
    isDecimal: true,          // 檢查是否為任意數字
    isLowercase: true,        // 檢查是否為小寫
    isUppercase: true,        // 檢查是否為大寫
    notNull: true,            // 不允許為空
    isNull: true,             // 只允許為空
    notEmpty: true,           // 不允許空字串
    equals: `specific value`, // 只允許一個特定值
    contains: `foo`,          // 檢查是否包含特定的子字串
    notIn: [[`foo`, `bar`]],  // 檢查是否值不是其中之一
    isIn: [[`foo`, `bar`]],   // 檢查是否值是其中之一
    notContains: `bar`,       // 不允許包含特定的子字串
    len: [2,10],              // 只允許長度在2到10之間的值
    isUUID: 4,                // 只允許uuids
    isDate: true,             // 只允許日期字串
    isAfter: "2011-11-05",    // 只允許在特定日期之後的日期字串
    isBefore: "2011-11-05",   // 只允許在特定日期之前的日期字串
    max: 23,                  // 只允許值 <= 23
    min: 23,                  // 只允許值 >= 23
    isCreditCard: true,       // 檢查有效的信用卡號碼
}
複製程式碼

Getters & Setters

GettersSetters 可以讓你在獲取和設定模型資料時做一些處理。


export default (sequelize, DataTypes) =>
  sequelize.define(`user`, {
    // ...
    sex: {
      type: DataTypes.BOLLEAN,
      allowNull: true,
      get () {
        const sex = this.getDataValue(`sex`)
        return sex ? `男` : `女`
      },
      set (val) {
        this.setDataValue(`title`, val === `男`)
      }
    }
  })
複製程式碼

第三步,關聯

歡迎達到戰場的勇士(或是瘋狂滾動滑鼠的你)。模型建立好了嗎?沒的話,送你一個技能—— Ctrl+C Ctrl+V,偷懶後,記得在 index.js 中引入。

// models/test/index.js

// 匯入
export const User = sequelize.import(__dirname + `/User`)
export const Article = sequelize.import(__dirname + `/Article`)
export const ArticleLike = sequelize.import(__dirname + `/Article_like`)
export const ArticleStar = sequelize.import(__dirname + `/Article_star`)
export const ArticleComment = sequelize.import(__dirname + `/Article_comment`)
複製程式碼

關聯知識點簡要一覽。更多詳情,請移步官方

// models/test/index.js

// 在 source 上存在一對一關係的外來鍵關聯
source.belongsTo(target, {
    as: `role`  // 使用別名(可代替目標模型),
    foreignKey: `user_id`   // 外來鍵名,
    targetKey:  `id`        // 目標健,預設主鍵
})
// 在 target 上存在一對一關係的外來鍵關聯
source.hasOne(target)

// 在 target 上存在一對多 source 的外來鍵關聯
source.hasMany(target)

// 在 target 上存在多對多的外來鍵關係(必須通過另外一張資料表儲存關聯資料)
source.belongsToMany(target, {through: `UserProject`})
target.belongsToMany(source, {through: `UserProject`})
複製程式碼

接下來,我們來建立表與表之間的關聯。顯而易見,UserArticle 之間存在一對多的關係,每個使用者可先制定個小目標,先發它一億篇文章(UserArticle 為一對多,即用 hasMany 方法),反過來,一篇文章僅屬於某個使用者的私有財產(ArticleUser 為一對一,即用 belongsTo 方法)。

// models/test/index.js

// 外來鍵 uid 將會放到 Article 上
User.hasMany(Article, {foreignKey: `uid`})
// 同樣,還是把外來鍵放到 Article 上
Article.belongsTo(User, {foreignKey: `uid`})
複製程式碼

同步後,檢視資料表 Article 時,哇, o(゚Д゚)っ! 神奇呀!多出了一個欄位, 正是uid,這個就是外來鍵。

搭建 nodeJS 伺服器之(2)sequelize

那麼,問題就來了,為什麼需要建立表與表之間的關聯?它有何用?因為方便,如果你想要通過一次查詢就把文章資料和文章所有關的點贊、收藏和評論資料一起找出來,並且放在一個資料結構中,那麼關聯是不可被替代的。在你 sequelize 一次查詢多個表的關聯資料時,它本質上是生成了一個複雜的 mysql 連結串列查詢語句。而在 sequeliz 中你僅僅在需要時,指定即可。

UserArticleLikeArticleStarArticleComment 都存在與上述一樣的關聯關係,讓我們一起快樂地大喊:“貼上複製大法好”。

// models/test/index.js

User.hasMany(ArticleLike, {foreignKey: `uid`})
ArticleLike.belongsTo(User, {foreignKey: `uid`})

User.hasMany(ArticleStar, {foreignKey: `uid`})
ArticleStar.belongsTo(User, {foreignKey: `uid`})

User.hasMany(ArticleComment, {foreignKey: `uid`})
ArticleComment.belongsTo(User, {foreignKey: `uid`})
複製程式碼

另外, ArticleArticleLikeArticleStarArticleComment 之間也存在關聯關係,比如一條評論,你既要知道誰寫的評論(uid),還有知道評論了哪篇文章 (aid)

// models/test/index.js

Article.hasMany(ArticleLike, {foreignKey: `aid`})
ArticleLike.belongsTo(Article, {foreignKey: `aid`})

Article.hasMany(ArticleStar, {foreignKey: `aid`})
ArticleStar.belongsTo(Article, {foreignKey: `aid`})

Article.hasMany(ArticleComment, {foreignKey: `aid`})
ArticleComment.belongsTo(Article, {foreignKey: `aid`})
複製程式碼

同步後,看檢視資料表 ArticleComment時,正如所料,它多出兩個外來鍵欄位 uidaid

搭建 nodeJS 伺服器之(2)sequelize

同步後資料表中沒外來鍵欄位?(你這大騙子)
你不認真看教程,還怪起我了。哼, ̄へ ̄,自己翻到 sequelize.sync() 將模型同步到資料庫的三種方法和區別? 看最後一行字。哼哼哼!!!不想找?往上翻,快!

對不起  ̄ω ̄=,我為我的無理道歉,作為賠償,我手不停蹄(笑尿)的手繪(連線都畫不直的菜雞,手繪?你信嗎?)了一張關聯流程圖,梳梳它們剪不斷理還亂的關係吧。

搭建 nodeJS 伺服器之(2)sequelize

關聯使用

// 查詢文章資料,同時關聯評論資料
Article.findAll({
  // 通過 include 欄位,把需要關聯的模型指定即可。
  // 就辣麼簡單!
  include: [ArticleComment]
})

// 返回資料
{
    "id": 4,
    "title": "我是標題",
    "content": "我是內容",
    "createdAt": "2018-10-11T03:42:01.000Z",
    "updatedAt": "2018-10-11T03:42:01.000Z",
    "uid": 1,
    // 評論
    "article_comments": [/* */]
}
複製程式碼
// 帶上所有已建立關聯表的資料

Article.findAll({
  include: [{
    all: true  
  }]
})


// 返回資料
}
    "id": 4,
    "title": "我是標題",
    "content": "我是內容",
    "createdAt": "2018-10-11T03:42:01.000Z",
    "updatedAt": "2018-10-11T03:42:01.000Z",
    "uid": 1,
    // 使用者
    "user": {
        "id": 1,
        "username": "sunny",
        "password": "1234",
        "createdAt": "2018-10-11T03:38:54.000Z",
        "updatedAt": "2018-10-11T03:38:54.000Z"
    },
    // 點贊
    "article_likes": [/* */]],
    // 收藏
    "article_stars": [/* */]],
    // 評論
    "article_comments": [/* */]]
}
複製程式碼
// 甚至你還可以深度遞迴(小心死迴圈)

Article.findAll({
  include: [{
    all: true,
    nested: true
  }]
})
複製程式碼

更多詳情,請移步官方

第四步,介面邏輯

介面呢!也就是增刪改查,還有什麼好說的呢,直接上好代(酒)好(肉)碼招待著各位吧 ٩(๑❛ᴗ❛๑)۶

// app/controllers/Article.js

import {Article} from "../models/test"

export default class {
  // 增
  static async add (ctx) {
    const post = ctx.request.body
    let result
    try {
      // 簡單直了
      result = await Article.create(post)
    } catch (error) {
      return ctx.body = {success: false, error}
    }

    ctx.body = {success: true, data: result}
  }
  
   // 刪
  static async remove (ctx) {
    const {id, uid} = ctx.request.body
    let result
    try {
      // 必須同時指定 id 和 uid 才能刪除
      result = await Article.destroy({
        where: { id, uid }
      })
    } catch (error) {
      return ctx.body = {success: false, error}
    }

    ctx.body = {success: true, data: result}
  }

  // 改    
  static async update (ctx) {
    const post = ctx.request.body
    // 才不讓你改所屬的使用者呢
    delete post.uid
    let result
      // 改呢,必須通過 where 指定主鍵
      result = await Article.update(post, {where: {id: post.id}})

    ctx.body = {success: true, data: result}
  }

  // 查
  static async find (ctx) {
    const {id, uid} = ctx.query
    let result
    try {
      result = await Article.findAll({
        // 可選的 id(查詢指定文章資料) 和 uid(查詢指定使用者所有的文章資料)
        where: Object.assign({}, id && {id}, uid && {uid}),
        // 帶上所有的關聯資料
        include: [{
          all: true,
        }]
      })
    } catch (error) {
      return ctx.body = {success: false, error}
    }

    ctx.body = {success: true, data: result}
  }
}
複製程式碼

最後記得掛載到路由。

// app/router.js

import Article from `./controllers/Article`

router
  .get(`/article/find`, Article.find)
  .post(`/article/add`, Article.add)
  .post(`/article/update`, Article.update)
  .post(`/article/remove`, Article.remove)
複製程式碼

Op(查詢條件)

Op 物件集內建了一系列適用於 where 子句 查詢的操作符(查詢條件)。

// /models/test/index.js

//  匯出 Op
export const Op = Sequelize.Op
複製程式碼
// /models/controllers/User.js

import {User, Op} from `../models/test/`


export default class {
  static async findTest (ctx) {
    let result
    try {
      result = await User.findAll({
        // 查詢所有 id > 2 的使用者
        where: {
          id: {
            [Op.gt]: 2
          }
        }
      })
    } catch (error) {
      return ctx.body = {success: false, error}
    }

    ctx.body = {success: true, data: result}
  }
}
複製程式碼

以下列出了所有內建的 Op 操作符更多詳情,請移步官方

[Op.and]: {a: 5}           // 且 (a = 5)
[Op.or]: [{a: 5}, {a: 6}]  // (a = 5 或 a = 6)
[Op.gt]: 6,                // id > 6
[Op.gte]: 6,               // id >= 6
[Op.lt]: 10,               // id < 10
[Op.lte]: 10,              // id <= 10
[Op.ne]: 20,               // id != 20
[Op.eq]: 3,                // = 3
[Op.not]: true,            // 不是 TRUE
[Op.between]: [6, 10],     // 在 6 和 10 之間
[Op.notBetween]: [11, 15], // 不在 11 和 15 之間
[Op.in]: [1, 2],           // 在 [1, 2] 之中
[Op.notIn]: [1, 2],        // 不在 [1, 2] 之中
[Op.like]: `%hat`,         // 包含 `%hat`
[Op.notLike]: `%hat`       // 不包含 `%hat`
[Op.iLike]: `%hat`         // 包含 `%hat` (不區分大小寫)  (僅限 PG)
[Op.notILike]: `%hat`      // 不包含 `%hat`  (僅限 PG)
[Op.regexp]: `^[h|a|t]`    // 匹配正規表示式/~ `^[h|a|t]` (僅限 MySQL/PG)
[Op.notRegexp]: `^[h|a|t]` // 不匹配正規表示式/!~ `^[h|a|t]` (僅限 MySQL/PG)
[Op.iRegexp]: `^[h|a|t]`    // ~* `^[h|a|t]` (僅限 PG)
[Op.notIRegexp]: `^[h|a|t]` // !~* `^[h|a|t]` (僅限 PG)
[Op.like]: { [Op.any]: [`cat`, `hat`]} // 包含任何陣列[`cat`, `hat`] - 同樣適用於 iLike 和 notLike
[Op.overlap]: [1, 2]       // && [1, 2] (PG陣列重疊運算子)
[Op.contains]: [1, 2]      // @> [1, 2] (PG陣列包含運算子)
[Op.contained]: [1, 2]     // <@ [1, 2] (PG陣列包含於運算子)
[Op.any]: [2,3]            // 任何陣列[2, 3]::INTEGER (僅限PG)

[Op.col]: `user.organization_id` // = `user`.`organization_id`, 使用資料庫語言特定的列識別符號, 本例使用 PG
複製程式碼

為什麼不直接使用符號而是使用額外的封裝層 Op,據官方說法是為了防止 SQL 注入和其他一些安全檢測。另外,Op 物件集其實是一系列 Symbol 的集合。

Handle.js

Handle.js,一個基於 koa 和 sequelize 的中間庫,讓你只專注於介面邏輯。

它可以讓你以最少量的程式碼,編寫具有複用性和良好可讀性的複雜介面程式碼,我們看一個簡單的例子:

import Handle from `handle.js`
// 匯入 sequelize 模型
import { Article } from `../models/db`

// 把 article 傳入 Handle,並例項化
const article = new Handle(Article)

// 生成一個查詢當前模型所有資料的 koa 中介軟體
const find = article.findAll()

// 繫結到路由
router.get(`/article/find`, find)
複製程式碼

當你有一個分頁邏輯(你一定寫過),在 Handle 中你可以把它封裝成一個函式。

// 為什麼是偏函式?
// 因為我給分頁預留了預設的配置項
function pagination (defaultCount = 5, defaultPage = 0) => {
  return d => {
    const count = ~~d.count || defaultCount
    const page = ~~d.page || defaultPage
    return {
      limit: count,
      offset: page * count
    }
  }
}
複製程式碼

然後把它放進 scope 中即可。

article
    .scope(pagination(10))
    .findAll()
複製程式碼

你也許已經看出來,可複用性體現哪裡了?沒錯,就是通過 scope 你可以讓每個介面輕鬆實現分頁,你什麼也不用做,只需要把一籃食材丟給 scope 它就會給你做出一盤香噴噴的蓋澆米飯。scope 做了什麼?很簡單,它在內部深度合併了所有選項,就好像你把一個原本雜亂而負載過重的大胖子,拆分成了更小更輕快的小帥夥(這比喻有點奇怪),或者說你可以用搭積木的方式有序的易懂的(最重要是好看啊!)建築起一座摩天大廈。

另外, Handle 為使用者做了更多的事情,在 scope 的基礎上提供了一個工具集,涵蓋了一些常用的封裝,讓你真的就像在搭積木,輕輕鬆鬆就實現了一個複雜的介面。handle 還提供了一個全域性管理關聯資料的靜態類,並提供了對 Mock 的支援,並致力於讓所有的事情變得簡單有序。值得一提的是 Handle 並不依賴 Koa,過程方法可以讓它用在 express 專案中或者 websocket 應用中,Handle 更準確說是一個 sequelize 的包裝庫。

當你具有一定基礎後,配合使用 handle.js 可以幫助你提高開發效率和寫出可維護的程式碼。handle.js 已在 github 免費開源(地址:github.com/yeshimei/Ha…)如果你覺得對你有用歡迎 star,支援我更新和完善它,如果你它已經在你的專案中,更希望你能參與進來作為貢獻者之一,感謝大家。

相關文章