前言
《搭建 nodeJS 服務器之(2)sequelize》是系列教程的第二部分。同時,本系列教程將會帶你從零架構一個五臟俱全的後端項目。
QQ答疑交流羣:
600633658
我們的鏈接:
Mysql 與 Sequelize 的關係
開始之前,我們先要對 ORM 有個大致的瞭解!何爲 ORM
ORM(Object Relational Mapping),稱爲對象關係映射,用於實現面向對象編程語言裏不同類型系統的數據之間的轉換。對象關係映射(Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程序設計技術,用於實現面向對象編程語言裏不同類型系統的數據之間的轉換。
Sequelize 呢!是一個基於 Promise 的 NodeJs ORM 庫,目前支持 ostgres, MySQL, SQLite 和 Microsoft SQL Server 等數據庫程序。Sequelize 相當一箇中間人負責兩者,誰呢?js 和 mysql 之間的交流。
讓我們看一下Sequelize中各部分於Mysql概念上的對應關係
- 實例化 Sequelize 連接到 Database: 通過實例化 Sequelize 類,連接到數據庫程序指定的數據庫。
- 定義 Model 映射 Table: 通過模型映射數據表的定義並代理操作方法
- 指定 DataTypes 聲明 Data Types: 把數據庫的數據類型變成在 js 上下文中更合適的用法。
- 使用 Op 生成 Where 子句 Operators: 爲選項對象提供強大的解耦和安全檢測。
- 關聯 Association 替代複雜的 Foreign Key 和 多表查詢: 用一套簡單的方法管理複雜的多表查詢。
- 調用 Transcation 封裝 Transation : 對事務一層簡單而必要的封裝。
從一個小項目開始
開始之前,千萬別忘了先把 Sequelize 以及依賴包安裝到本地
npm i sequelize mysql2 -D
第一步,連接到數據庫
Sequelize 是庫的入口類,可做兩件事情:
- 連接到數據庫
- 設置數據表的全局配置。
所以暫且可把 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。
第二步,建立模型
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;
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(或者其他接口調試工具)發起請求。請求成功後查看 Mysql。看是否有新的內容插入
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')
模型方法
除了用戶模型外,我們還需定義文章模型(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
integer,bigint,float 和 double 都支持 unsigned 和 zerofill 屬性
Sequelize.INTEGER(11).UNSIGNED.ZEROFILL
驗證器
Model 爲每個字段都提供了驗證選項。
export default (sequelize, DataTypes) =>
sequelize.define('user', {
// ...
email: {
type: DataTypes.STRING(255),
allowNull: true,
validate: {
isEmail: true
}
}
})
另外,可指定 args 和 msg 自定義參數和錯誤消息。
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, // 檢查郵件格式 ([email protected])
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
Getters 和 Setters 可以讓你在獲取和設置模型數據時做一些處理。
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 === '男')
}
}
})
第三步,關聯
// 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'})
接下來,我們來建立表與表之間的關聯。顯而易見,User 和 Article 之間存在一對多的關係,每個用戶可先制定個小目標,先發它一億篇文章(User 到 Article 爲一對多,即用 hasMany 方法),反過來,一篇文章僅屬於某個用戶的私有財產(Article 到 User 爲一對一,即用 belongsTo 方法)。
// models/test/index.js
// 外鍵 uid 將會放到 Article 上
User.hasMany(Article, {foreignKey: 'uid'})
// 同樣,還是把外鍵放到 Article 上
Article.belongsTo(User, {foreignKey: 'uid'})
同步後,查看數據表 Article 時,多出了一個字段, 正是uid,這個就是外鍵。
那麼,問題就來了,爲什麼需要建立表與表之間的關聯?它有何用?因爲方便,如果你想要通過一次查詢就把文章數據和文章所有關的點贊、收藏和評論數據一起找出來,並且放在一個數據結構中,那麼關聯是不可被替代的。在你 sequelize 一次查詢多個表的關聯數據時,它本質上是生成了一個複雜的 mysql 鏈表查詢語句。而在 sequeliz 中你僅僅在需要時,指定即可。
User 與 ArticleLike,ArticleStar 和 ArticleComment 都存在與上述一樣的關聯關係,複製粘貼即可
// 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'})
另外, Article 與 ArticleLike,ArticleStar 和 ArticleComment 之間也存在關聯關係,比如一條評論,你既要知道誰寫的評論(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時,正如所料,它多出兩個外鍵字段 uid 和 aid。
關係圖如下:
關聯使用
// 查詢文章數據,同時關聯評論數據
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 的集合。