學會 Sequelize,讓你操作數據更絲滑

Sequelize 是一個基於 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有強大的事務支持, 關聯關係, 預讀和延遲加載, 讀取複製等功能。

一、前言

本文希望通過下面的知識講解及一些實戰經驗分享,給予即將入門或正在學習的同學一些幫助。

在之前剛接觸 Sequelize 的時候,遇到挺多問題,比如數據的聚合統計應該怎麼做?複雜的排序規則應該怎麼寫?索引這塊應該如何定義?性能如何衡量?等等這些問題,後來經過自己的琢磨及與後臺人員交流探討,這些問題都得予解決。

我們直接進入主題,學習目錄結構如下:

二、入門

1. 配置

module.exports = () => {
 const config = (exports = {})
 config.sequelize = {
  //支持的數據庫類型
  dialect: 'mysql',
  //連接數據庫的主機
  host: 'localhost',
  //連接數據庫的端口
  port: 3306,
  //數據庫名稱
  database: 'db_test',
  //數據庫用戶名
  username: 'root',
  //數據庫密碼
  password: 'xxxxxx',
  //設置標準時區
  timezone: '+08:00',
  //配置
  dialectOptions: {
    // 時間格式化,返回字符串
    dateStrings: true,
    typeCast(field, next) {
      if (field.type === 'DATETIME') {
        return field.string()
      }
      return next()
    }
  }
 }
 return config
}

注:如果沒有加 dialectOptions 配置上的 typeCast 屬性值爲 true的話,返回的時間是 ISO 標準時間日期字符。(如:'2022-04-16T15:02:08.017Z')

2. 創建模型

2.1 常用數據類型

b8mw8Q

像數字、字符類型默認都有一些長度的限定,有時候因爲長度問題導致接口出錯,所以需要根據情況而定。

還有其他類型就不一一列了,需要用到的可以參照文檔看看。Sequelize 數據類型 [1]

2.2 定義模型

module.exports = (app) => {
  const { STRING, INTEGER, DATE } = app.Sequelize

  const model = app.model.define(
    'student',
    {
     
      id: {
        //類型
        type: INTEGER(11),
        //是否允許爲空
        allowNull: false,
        //是否爲主鍵
        primaryKey: true,
        //自動自增
        autoIncrement: true,
        //備註
        comment: '學生id',
      },
      name: {
        type: STRING(50),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '學生姓名',
      },
      class_id: {
        type: INTEGER(11),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '班級',
      },
      cid: {
        type: STRING(50),
        allowNull: false,
        unique: 'cid',
        validate: {
          notEmpty: true,
        },
        comment: '身份證',
      }
    },
    {
      // 啓動時間,設置爲ture會自動生成創建和更新時間,默認字段名稱爲createAt、updateAt。
      timestamps: true,
      //對應的表名將與model名相同
      freezeTableName: true,
      //表備註
      comment: '表1',
      //創建時間字段別名或不展示
      createdAt: 'createTime',
      //更新時間字段別名或不展示
      updatedAt: 'updateTime'
    }
  )

  return model
}

使用 model.sync(options) 可自動執行 SQL 語句建表,但這個不建議用,第一這麼做容易出現問題,第二我們的規範也不允許這麼做。

2.3 表字段規範

這裏講的是我們公司的 SQL 規範,先來看一個案例:

CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '學生id',
  `name` varchar(50) NOT NULL COMMENT '學生姓名',
  `cid` varchar(50) NOT NULL COMMENT '身份證',
  `class_id` int NOT NULL COMMENT '班級',
  `create_time` datetime NOT NULL COMMENT '',
  `update_time` datetime NOT NULL COMMENT '更新時間',
  `mobile` varchar(50) DEFAULT NULL COMMENT '手機號',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid_unique` (`cid`),
  KEY `class_id` (`class_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '學生表';

審覈結果:

審覈結果

下面講一下基本的設計規範:

1) 禁用關鍵字

在設計的時候注意不要用到關鍵字,比如 nametypestatus 等字段。

常見關鍵字:

關鍵字

2) 字段禁止默認值爲 null

從上面的案例中 mobile 字段使用了允許爲 null 值,審覈平臺這塊沒有強限制,但不建議用 null 爲默認值,很容易出現問題。

3) 需要添加字段註釋和表註釋

CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '學生id',
  `username` varchar(50) NOT NULL COMMENT '學生姓名',
  `cid` varchar(50) NOT NULL COMMENT '身份證',
  `class_id` int NOT NULL COMMENT '班級',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  `update_time` datetime NOT NULL COMMENT '更新時間',
  `mobile` varchar(50) DEFAULT NULL COMMENT '手機號',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  KEY `iclass_id` (`class_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '學生表';

4)創建時間和更新時間字段

該表中必須含有 create_timeupdate_time 字段,如果沒有該 2 個字段的話審覈會不通過。

5) 索引命名

唯一索引:必須以 uniq_ 前綴命名。

普通索引:必須以 idx_ 前綴命名。

索引命名

6) 其它

以上是常用的基本規範,更多根據自己公司情況而定。

3. 增刪改查

3.1 create

創建數據

const values = {
 username: '張三',
 age: 18
}
const result = await this.model.create(values)
console.log(result)

3.2 findAll

查詢數據並返回數組

const filter = {
 username: '張三',
 age: 18
}
const result = await this.model.findAll({
 where: filter
})
console.log(result)

3.3 findAndCountAll

查詢數據並返回總數和數組列表

const filter = {
 username: '張三',
 age: 18
}
const result = await this.model.findAndCountAll({
 where: filter
})
console.log(result)

3.4 findOne

查詢某一條數據返回對象

const filter = {
 id: 20
}
const result = await this.model.findOne({
 where: filter
})
console.log(result)

3.5 count

返回總條數

const total = await this.model.count()
console.log(total)

3.6 update

更新數據

const value = {
 id: 2,
 username: '張三',
 age: 20
}
const result = await this.model.update(value)
console.log(result)

3.7 destroy

刪除數據

const filter = {
 id: 1
}
const result = await this.model.destroy({
 where: filter
})
console.log(result)

3.8 其它

還有一些其他的方法,具體可以看看 Sequelize 方法文檔 [2]。

4. 查詢條件

這個具體的查詢用法就不一一說了,根據自己需要的查詢條件看下對應的查詢功能即可。

this.Op = this.app.Sequelize.Op
this.model.findAll({
  where: {
    [Op.and]: [{ a: 5 }, { b: 6 }],            // (a = 5) AND (b = 6)
    [Op.or]: [{ a: 5 }, { b: 6 }],             // (a = 5) OR (b = 6)
    someAttribute: {
      // 基本
      [Op.eq]: 3,                              // = 3
      [Op.ne]: 20,                             // != 20
      [Op.is]: null,                           // IS NULL
      [Op.not]: true,                          // IS NOT TRUE
      [Op.or]: [5, 6],                         // (someAttribute = 5) OR (someAttribute = 6)

      // 使用方言特定的列標識符 (以下示例中使用 PG):
      [Op.col]: 'user.organization_id',        // = "user"."organization_id"

      // 數字比較
      [Op.gt]: 6,                              // > 6
      [Op.gte]: 6,                             // >= 6
      [Op.lt]: 10,                             // < 10
      [Op.lte]: 10,                            // <= 10
      [Op.between]: [6, 10],                   // BETWEEN 6 AND 10
      [Op.notBetween]: [11, 15],               // NOT BETWEEN 11 AND 15

      // 其它操作符

      [Op.all]: sequelize.literal('SELECT 1'), // > ALL (SELECT 1)

      [Op.in]: [1, 2],                         // IN [1, 2]
      [Op.notIn]: [1, 2],                      // NOT IN [1, 2]

      [Op.like]: '%hat',                       // LIKE '%hat'
      [Op.notLike]: '%hat',                    // NOT LIKE '%hat'
      [Op.startsWith]: 'hat',                  // LIKE 'hat%'
      [Op.endsWith]: 'hat',                    // LIKE '%hat'
      [Op.substring]: 'hat',                   // LIKE '%hat%'
      [Op.iLike]: '%hat',                      // ILIKE '%hat' (不區分大小寫) (僅 PG)
      [Op.notILike]: '%hat',                   // NOT ILIKE '%hat'  (僅 PG)
      [Op.regexp]: '^[h|a|t]',                 // REGEXP/~ '^[h|a|t]' (僅 MySQL/PG)
      [Op.notRegexp]: '^[h|a|t]',              // NOT REGEXP/!~ '^[h|a|t]' (僅 MySQL/PG)
      [Op.iRegexp]: '^[h|a|t]',                // ~* '^[h|a|t]' (僅 PG)
      [Op.notIRegexp]: '^[h|a|t]',             // !~* '^[h|a|t]' (僅 PG)

      [Op.any]: [2, 3],                        // ANY ARRAY[2, 3]::INTEGER (僅 PG)
      [Op.match]: Sequelize.fn('to_tsquery', 'fat & rat') // 匹配文本搜索字符串 'fat' 和 'rat' (僅 PG)

      // 在 Postgres 中, Op.like/Op.iLike/Op.notLike 可以結合 Op.any 使用:
      [Op.like]: { [Op.any]: ['cat', 'hat'] }  // LIKE ANY ARRAY['cat', 'hat']
    }
  }
});

三、進階

1. 聯表查詢

1.1 一對一

比如學生表和身份證表,一名學生只會有一個身份證號碼,一張身份證只會對應一名學生。

這種就是一對一的關係,如圖:

一對一

假如學生表作爲主表,身份證表作爲副表,那邊這張副表的外鍵就是學生表的 id。

在使用 Sequelize 庫中,我們想通過學生表關聯到相應的身份證表信息,可通過 hasOne 方法做關聯:

student.hasOne(card, {foreignKey : 'id'});

如果我們想通過身份證表反向關聯到學生表,這是可以用 belongsTo 方法做關聯:

card.belongsTo(student, {foreignKey : 'cid'});

總結一下 hasOnebelongsTo 的區別:

7EyQcU

1.2 一對多

比如學生表和班級表,一名學生只有一個班級,一個班級由多名學生組成,這種是一對多(多對一)的關係,如圖:

一對多

在使用 Sequelize 庫中,可通過 hasMany 方法做關聯:

class.hasMany(student, {foreignKey : 'class_id'});

1.3 多對多

比如學生表和課程表,一名學生可以有多門課程,一門課程可以由多名學生參與,這種是多對多的關係,如圖:

多對多

在使用 Sequelize 庫中,可通過 belongsToMany 方法做關聯:

const Student = sequelize.define('Student', { name: DataTypes.STRING });
const Course = sequelize.define('Course', { name: DataTypes.STRING });
const StudentCourse = sequelize.define('StudentCourse', {
  studentId: {
    type: DataTypes.INTEGER,
    references: {
      model: Student,
      key: 'id'
    }
  },
  courseId: {
    type: DataTypes.INTEGER,
    references: {
      model: Course,
      key: 'id'
    }
  }
});
Student.belongsToMany(Course, { through: StudentCourse });
Course.belongsToMany(Student, { through: StudentCourse });

1.4 聯表查詢優劣

優點:聯表查詢用起來很方便,不需要做太多了處理,尤其在 B 端場景會用的比較多。

缺點:對於 C 端這種流量較大場景,使用聯表查詢效率會很低下,可能會導致服務器崩潰,所以現在大多數在 C 端都不採用 SQL 自帶的聯表查詢功能,一般會通過代碼邏輯來處理,大大減少查詢效率。

有興趣的可以瞭解下 mysql 聯表查詢的步驟是怎樣的,這樣對比起來會更加直觀一點。

2. 分組

舉個例子,你想對訂單各個狀態的數量進行一個統計,你應該會怎麼做?

在之前不知道分組這個功能時,我是這麼做的:

const obj = {}
obj.problemAmoumt = await this.model.count()
obj.resolvedAmoumt = await this.model.count({
  where: {
    problemStatus:2
  }
})
return ctx.returnStatus.SUCCESS(obj)

這似乎看起來很粗暴。

後來發現可通過 SQL 語句中的 group 進行一個分組,group 是指定要進行分組的字段,示例如下:

this.model.count({
 attributes: ['order_status'],
 group: 'order_status'
})

最終轉換成 SQL 語句如下:

SELECT `order_status`, count(*) AS `count` FROM `order_list` AS `order_list` GROUP BY `order_status`;

返回結果

[
 {
     "order_status": 1,
     "count": 4
 },
 {
     "order_status": 2,
     "count": 2
 },
 {
     "order_status": 3,
     "count": 2
 }
]

思考:

  1. 分組在應用時會不會有坑?自己在使用時可以看看。

  2. 如果想對 order_status 進行一個別名 bbb 字段返回,這時的查詢語句應該如何調整?

3. 聚合函數

3.1 count

統計總數,比如統計各個訂單的數量:

this.model.findAll({
  attributes: [
    'orderStatus',
    [sequelize.fn('count', sequelize.col('order_status')), 'total']
  ],
  group: 'orderStatus'
})

3.2 sum

求和,比如統計全部同學的成績總數:

this.model.findAll({
  attributes: [
    [sequelize.fn('sum', sequelize.col('score')), 'score']
  ]
})

3.3 max

查詢最大值,比如找到這個分數最高的同學:

this.model.findAll({
  attributes: [
    [sequelize.fn('max', sequelize.col('score')), 'score']
  ]
})

3.4 min

查詢最小值,比如找到這個分數最低的同學:

this.model.findAll({
  attributes: [
    [sequelize.fn('min', sequelize.col('score')), 'score']
  ]
})

當然這些方法也可以通過 this.model.方法(字段名稱) 來實現,參數爲指定的字段。

4 索引

索引就是一種將數據庫中的記錄按照特殊形式存儲的數據結構。通過索引,能夠提高數據查詢的效率,從而提升服務器的性能。

4.1 主鍵索引

一個表只能由一個主鍵索引,且不爲空。

可通過 primaryKey 屬性來設置主鍵,通常會把 id 設爲該表的主鍵,當然你也可以指定其它字段來作爲主鍵,該字段值必須是唯一的。

 const student = app.model.define(
    'student',
    {
      id: {
        type: INTEGER(11),
        allowNull: false,
        primaryKey: true,
        autoIncrement: true,
        default: 10000,
        comment: '學生id',
      },
      name: {
        type: STRING(50),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '學生姓名',
      }
    }
 )
 return student

4.2 唯一索引

唯一索引的列的值必須唯一,但是允許出現空值。

可通過 unique 屬性設置爲唯一索引,值爲 true 的話,索引名稱會自動生成,也可以設置爲自定義索引名稱。

 const student = app.model.define(
    'student',
    {
      id: {
        type: INTEGER(11),
        allowNull: false,
        primaryKey: true,
        autoIncrement: true,
        default: 10000,
        comment: '學生id',
      },
      name: {
        type: STRING(50),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '學生姓名',
      },
      cid: {
        type: STRING(50),
        allowNull: false,
        unique: 'cid_unique',
        validate: {
          notEmpty: true,
        },
        comment: '身份證',
      }
    }
 )
 return student

4.3 組合索引

用多個列組合構建的索引,這多個列中的值不允許有空值。

const student = app.model.define(
    'student',
    {
      id: {
        type: INTEGER(11),
        allowNull: false,
        primaryKey: true,
        autoIncrement: true,
        default: 10000,
        comment: '學生id',
      },
      name: {
        type: STRING(50),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '學生姓名',
      },
      class_id: {
        type: INTEGER(11),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '班級',
      },
      cid: {
        type: STRING(50),
        allowNull: false,
        unique: 'cid',
        validate: {
          notEmpty: true,
        },
        comment: '身份證',
      }
    },
    {
      indexes: [{
        name: 'name',
        fields: ['name']
      }, {
        name: 'cid',
        fields: ['cid']
      }]
    }
  )
  return student

4.4 普通索引

用表中的普通列構建的索引,沒有任何限制。

用法只要在 indexed 屬性裏不設置 unique 字段即可。

const student = app.model.define(
    'student',
    {
      id: {
        type: INTEGER(11),
        allowNull: false,
        primaryKey: true,
        autoIncrement: true,
        default: 10000,
        comment: '學生id',
      },
      name: {
        type: STRING(50),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '學生姓名',
      },
      class_id: {
        type: INTEGER(11),
        allowNull: false,
        validate: {
          notEmpty: true,
        },
        comment: '班級',
      },
      cid: {
        type: STRING(50),
        allowNull: false,
        unique: 'cid',
        validate: {
          notEmpty: true,
        },
        comment: '身份證',
      }
    },
    {
      indexes: [{
     unique: true,
        name: 'name',
        fields: ['name']
      }, {
     unique: true,
        name: 'class_id',
        fields: ['class_id']
      }]
    }
  )
  return student

4.5 加與不加索引的區別?

優點:增加索引會提供查詢效率

缺點:增加內存空間

我們來看一條查詢語句:

explain select * from `experience_problem_list` where `experience_problem_list`.`page_type` = 2;

先看看沒加索引的執行結果:

WECmDl

表中總共有 16416 條數據,掃描行數也是 16416 條。

再看下加了索引的執行結果:

gXcftF

我們來看下 rows 字段的對比,加了索引的掃描行數只有 4 條,由此可以看到,加索引的查詢效率大大高於普通查詢。

5. 事務

可以理解爲一個事務對應的是一組完整的業務,並且在這個事務中所作的一切操作要麼全部成功,要麼全部失敗,只要有一個操作沒成功,整個事務都將回滾到事務開始前。

這裏簡單放一個官方使用案例吧:

// 首先,我們開始一個事務並將其保存到變量中
const t = await sequelize.transaction();
try {
  // 然後,我們進行一些調用以將此事務作爲參數傳遞:
  const user = await User.create({
    firstName: 'Bart',
    lastName: 'Simpson'
  }, { transaction: t });
  await user.addSibling({
    firstName: 'Lisa',
    lastName: 'Simpson'
  }, { transaction: t });
  // 如果執行到此行,且沒有引發任何錯誤.
  // 我們提交事務.
  await t.commit();
} catch (error) {
  // 如果執行到達此行,則拋出錯誤.
  // 我們回滾事務.
  await t.rollback();
}

四、實戰演練

1. 查詢數據不想返回內置的數據結構,只想單純返回純數據結構怎麼處理?

來看下默認返回查詢的數據結果:

{
  count: 8,
  rows: [
    student {
      dataValues: [Object],
      _previousDataValues: [Object],
      uniqno: 1,
      _changed: Set(0) {},
      _options: [Object],
      isNewRecord: false
    }
  ]
}

這裏 rows 裏面對象返回了很多層,在業務處理時可能取值會不太方便,我們可以在調用方法的參數里加上 raw 等於 true,就返回正常的數據格式。

const result = await this.model.findAll({
 where: filter,
 raw: true
})

返回:

{
  count: 8,
  rows: [
    {
      id: 8,
      name: '張三',
      cid: 'xxx',
      createTime: '2022-04-08 18:06:52',
      updateTime: '2022-04-08 18:06:52'
    }
  ]
}

2. 如果列表篩選涉及到主副表應該怎麼查詢合適?

有幾種方案:

1)直接用 mysql 語句做表關聯即可,但性能比較低下。

2)用代碼邏輯處理,但寫起來可能有點繞,但效率比直接用 mysql 表關聯效率高。

3)如果主表字段固定且不多的話,可以直接冗餘副表中,但如果後續主表加字段的話,副表更新是個問題。

4)採用 ES,將這 2 張表的數據合併同步到 ES 的一張表裏,但 ES 使用場景一般都是量很大的,加上 nodejs 新增其它數據庫交互,有額外的開銷成本。

綜合以上考慮,如果是內部項目,量也不是很多的話,查詢比較複雜的話可以直接採用第一種方法,如果邏輯相對比較簡單,也可以採用第二種方法。

3. 分組 group 遇到的問題。

group 只能填寫已有的查詢字段。比如你表中有這個字段 aaa,但查詢返回的字段 aaa 被過濾了,這時不能以 aaa 進行分組。

4. 如何獲取今日、最近 7 天、一個月的數據?

可通過字符串函數 DATE_FORMAT 實現:

1)首先通過時間選擇器獲取到今日、最近 7 天、一個月的開始時間和結束時間。

2)然後使用對改模型使用 count 進行查詢,這時獲取到的知識該時期的總條數。

3)對改數據進行分組,使用 group 屬性,字段爲創建的時間,但我們創建的時間是包含時分秒的,如果這麼聚合的話,生成的數據並不是我們想到的,我們需要的是針對年月日,所以需要對這個時間進行一個處理。

[
    {
        "create_time": "2022-04-12 17:02:36",
        "count": 1
    },
    {
        "create_time": "2022-04-14 17:02:38",
        "count": 1
    },
    {
        "create_time": "2022-04-14 17:03:38",
        "count": 1
    },
    {
        "create_time": "2022-04-14 17:03:58",
        "count": 1
    }
]

這個顯然不是我們想要的一個結果。

4)然後函數 DATE_FORMAT,將時間格式化爲年月日。

總體實現如下:

const countArr = await this.model.count({
  where: filter,
  attributes: [
    [sequelize.fn('DATE_FORMAT', sequelize.col('create_time'), '%Y-%m-%d'), 'createTime'],
  ], group: 'createTime',
})

假設我想獲取 4 月 10 日 - 4 月 14 日的數據,正常返回結構如下:

[
    {
        "createTime": "2022-04-12",
        "count": 1
    },
    {
        "createTime": "2022-04-14",
        "count": 3
    }
]

5)因爲表中只有 12 號和 14 號的數據,10、11、13 號沒有,這塊需要自己對代碼進行一個處理,把其它日期沒有的數據爲 0 即可。

最終希望達到的效果:

[
 {
     "time": "2022-04-10",
     "count": 0
 },
 {
     "time": "2022-04-11",
     "count": 0
 },
 {
     "time": "2022-04-12",
     "count": 1
 },
 {
     "time": "2022-04-13",
     "count": 0
 },
 {
     "time": "2022-04-14",
     "count": 3
 }
]

五、問題思考

  1. 如果想要獲取一年中每個月的訂單量,應該怎麼查詢?

答案:關注  " 大轉轉 FE"公衆號,回覆" 統計 "、" 一年 " 其中一個即可

  1. 如何根據查詢條件的順序,返回相應的數據順序?

答案:關注  " 大轉轉 FE"公衆號,,回覆"filed"、" 排序 " 其中一個即可

  1. 如何進行動態分表?

答案:關注  " 大轉轉 FE"公衆號,回覆" 分表 " 即可。

注:如果您還有其它問題想交流,歡迎在底部留言。

參考資料

[1] Sequelize 數據類型: https://www.sequelize.com.cn/core-concepts/model-basics

[2] Sequelize 方法文檔: https://www.sequelize.com.cn/core-concepts/model-querying-finders

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9kTtbzo1UEODIW9avI1l0w