• Sequelize
  • 准备工作
  • 初始化项目
  • 初始化数据库和 Migrations
  • 编写代码
  • 单元测试
  • 完整示例
  • 脚手架

    Sequelize

    前面的章节中,我们介绍了如何在框架中通过 egg-mysql 插件来访问数据库。而在一些较为复杂的应用中,我们可能会需要一个 ORM 框架来帮助我们管理数据层的代码。而在 Node.js 社区中,sequelize 是一个广泛使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。

    本章节我们会通过开发一个对 MySQL 中 users 表的数据做 CURD 的例子来一步步介绍如何在 egg 项目中使用 sequelize。

    准备工作

    在这个例子中,我们会使用 sequelize 连接到 MySQL 数据源,因此在开始编写代码之前,我们需要先在本机上安装好 MySQL,如果是 MacOS,可以通过 homebrew 快速安装:

    1. brew install mysql
    2. brew services start mysql

    初始化项目

    通过 npm 初始化一个项目:

    1. $ mkdir sequelize-project && cd sequelize-project
    2. $ npm init egg --type=simple
    3. $ npm i

    安装并配置 egg-sequelize 插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和 mysql2 模块:

    • 安装
    1. npm install --save egg-sequelize mysql2
    • config/plugin.js 中引入 egg-sequelize 插件
    1. exports.sequelize = {
    2. enable: true,
    3. package: 'egg-sequelize',
    4. };
    • config/config.default.js 中编写 sequelize 配置
    1. config.sequelize = {
    2. dialect: 'mysql',
    3. host: '127.0.0.1',
    4. port: 3306,
    5. database: 'egg-sequelize-doc-default',
    6. };

    我们可以在不同的环境配置中配置不同的数据源地址,用于区分不同环境使用的数据库,例如我们可以新建一个 config/config.unittest.js 配置文件,写入如下配置,将单测时连接的数据库指向 egg-sequelize-doc-unittest

    1. exports.sequelize = {
    2. dialect: 'mysql',
    3. host: '127.0.0.1',
    4. port: 3306,
    5. database: 'egg-sequelize-doc-unittest',
    6. };

    完成上面的配置之后,一个使用 sequelize 的项目就初始化完成了。egg-sequelize 和 sequelize 还支持更多的配置项,可以在他们的文档中找到。

    初始化数据库和 Migrations

    接下来我们先暂时离开 egg 项目的代码,设计和初始化一下我们的数据库。首先我们通过 mysql 命令在本地快速创建开发和测试要用到的两个 database:

    1. mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-default`;'
    2. mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-unittest`;'

    然后我们开始设计 users 表,它有如下的数据结构:

    1. CREATE TABLE `users` (
    2. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
    3. `name` varchar(30) DEFAULT NULL COMMENT 'user name',
    4. `age` int(11) DEFAULT NULL COMMENT 'user age',
    5. `created_at` datetime DEFAULT NULL COMMENT 'created time',
    6. `updated_at` datetime DEFAULT NULL COMMENT 'updated time',
    7. PRIMARY KEY (`id`)
    8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user';

    我们可以直接通过 mysql 命令将表直接建好,但是这并不是一个对多人协作非常友好的开发模式。在项目的演进过程中,每一个迭代都有可能对数据库数据结构做变更,怎样跟踪每一个迭代的数据变更,并在不同的环境(开发、测试、CI)和迭代切换中,快速变更数据结构呢?这时候我们就需要 Migrations 来帮我们管理数据结构的变更了。

    sequelize 提供了 sequelize-cli 工具来实现 Migrations,我们也可以在 egg 项目中引入 sequelize-cli。

    • 安装 sequelize-cli
    1. npm install --save-dev sequelize-cli

    在 egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在 database 目录下,所以我们在项目根目录下新建一个 .sequelizerc 配置文件:

    1. 'use strict';
    2. const path = require('path');
    3. module.exports = {
    4. config: path.join(__dirname, 'database/config.json'),
    5. 'migrations-path': path.join(__dirname, 'database/migrations'),
    6. 'seeders-path': path.join(__dirname, 'database/seeders'),
    7. 'models-path': path.join(__dirname, 'app/model'),
    8. };
    • 初始化 Migrations 配置文件和目录
    1. npx sequelize init:config
    2. npx sequelize init:migrations

    执行完后会生成 database/config.json 文件和 database/migrations 目录,我们修改一下 database/config.json 中的内容,将其改成我们项目中使用的数据库配置:

    1. {
    2. "development": {
    3. "username": "root",
    4. "password": null,
    5. "database": "egg-sequelize-doc-default",
    6. "host": "127.0.0.1",
    7. "dialect": "mysql"
    8. },
    9. "test": {
    10. "username": "root",
    11. "password": null,
    12. "database": "egg-sequelize-doc-unittest",
    13. "host": "127.0.0.1",
    14. "dialect": "mysql"
    15. }
    16. }

    此时 sequelize-cli 和相关的配置也都初始化好了,我们可以开始编写项目的第一个 Migration 文件来创建我们的一个 users 表了。

    1. npx sequelize migration:generate --name=init-users

    执行完后会在 database/migrations 目录下生成一个 migration 文件(${timestamp}-init-users.js),我们修改它来处理初始化 users 表:

    1. 'use strict';
    2. module.exports = {
    3. // 在执行数据库升级时调用的函数,创建 users 表
    4. up: async (queryInterface, Sequelize) => {
    5. const { INTEGER, DATE, STRING } = Sequelize;
    6. await queryInterface.createTable('users', {
    7. id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    8. name: STRING(30),
    9. age: INTEGER,
    10. created_at: DATE,
    11. updated_at: DATE,
    12. });
    13. },
    14. // 在执行数据库降级时调用的函数,删除 users 表
    15. down: async queryInterface => {
    16. await queryInterface.dropTable('users');
    17. },
    18. };
    • 执行 migrate 进行数据库变更
    1. # 升级数据库
    2. npx sequelize db:migrate
    3. # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
    4. # npx sequelize db:migrate:undo
    5. # 可以通过 `db:migrate:undo:all` 回退到初始状态
    6. # npx sequelize db:migrate:undo:all

    执行之后,我们的数据库初始化就完成了。

    编写代码

    现在终于可以开始编写代码实现业务逻辑了,首先我们来在 app/model/ 目录下编写 user 这个 Model:

    1. 'use strict';
    2. module.exports = app => {
    3. const { STRING, INTEGER, DATE } = app.Sequelize;
    4. const User = app.model.define('user', {
    5. id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    6. name: STRING(30),
    7. age: INTEGER,
    8. created_at: DATE,
    9. updated_at: DATE,
    10. });
    11. return User;
    12. };

    这个 Model 就可以在 Controller 和 Service 中通过 app.model.User 或者 ctx.model.User 访问到了,例如我们编写 app/controller/users.js

    1. // app/controller/users.js
    2. const Controller = require('egg').Controller;
    3. function toInt(str) {
    4. if (typeof str === 'number') return str;
    5. if (!str) return str;
    6. return parseInt(str, 10) || 0;
    7. }
    8. class UserController extends Controller {
    9. async index() {
    10. const ctx = this.ctx;
    11. const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
    12. ctx.body = await ctx.model.User.findAll(query);
    13. }
    14. async show() {
    15. const ctx = this.ctx;
    16. ctx.body = await ctx.model.User.findByPk(toInt(ctx.params.id));
    17. }
    18. async create() {
    19. const ctx = this.ctx;
    20. const { name, age } = ctx.request.body;
    21. const user = await ctx.model.User.create({ name, age });
    22. ctx.status = 201;
    23. ctx.body = user;
    24. }
    25. async update() {
    26. const ctx = this.ctx;
    27. const id = toInt(ctx.params.id);
    28. const user = await ctx.model.User.findByPk(id);
    29. if (!user) {
    30. ctx.status = 404;
    31. return;
    32. }
    33. const { name, age } = ctx.request.body;
    34. await user.update({ name, age });
    35. ctx.body = user;
    36. }
    37. async destroy() {
    38. const ctx = this.ctx;
    39. const id = toInt(ctx.params.id);
    40. const user = await ctx.model.User.findByPk(id);
    41. if (!user) {
    42. ctx.status = 404;
    43. return;
    44. }
    45. await user.destroy();
    46. ctx.status = 200;
    47. }
    48. }
    49. module.exports = UserController;

    最后我们将这个 controller 挂载到路由上:

    1. // app/router.js
    2. module.exports = app => {
    3. const { router, controller } = app;
    4. router.resources('users', '/users', controller.users);
    5. };

    针对 users 表的 CURD 操作的接口就开发完了,为了验证代码逻辑是否正确,我们接下来需要编写单元测试来验证。

    单元测试

    在编写测试之前,由于在前面的 egg 配置中,我们将单元测试环境和开发环境指向了不同的数据库,因此需要通过 Migrations 来初始化测试数据库的数据结构:

    1. NODE_ENV=test npx sequelize db:migrate:up

    有数据库访问的单元测试直接写起来会特别繁琐,特别是很多接口我们需要创建一系列的数据才能进行,造测试数据是一个非常繁琐的过程。为了简化单测,我们可以通过 factory-girl 模块来快速创建测试数据。

    • 安装 factory-girl 依赖
    1. npm install --save-dev factory-girl
    • 定义 factory-girl 的数据模型到 test/factories.js
    1. // test/factories.js
    2. 'use strict';
    3. const { factory } = require('factory-girl');
    4. module.exports = app => {
    5. // 可以通过 app.factory 访问 factory 实例
    6. app.factory = factory;
    7. // 定义 user 和默认数据
    8. factory.define('user', app.model.User, {
    9. name: factory.sequence('User.name', n => `name_${n}`),
    10. age: 18,
    11. });
    12. };
    • 初始化文件 test/.setup.js,引入 factory,并确保测试执行完后清理数据,避免被影响。
    1. const { app } = require('egg-mock/bootstrap');
    2. const factories = require('./factories');
    3. before(() => factories(app));
    4. afterEach(async () => {
    5. // clear database after each test case
    6. await Promise.all([
    7. app.model.User.destroy({ truncate: true, force: true }),
    8. ]);
    9. });

    接下来我们就可以开始编写真正的测试用例了:

    1. // test/app/controller/users.test.js
    2. const { assert, app } = require('egg-mock/bootstrap');
    3. describe('test/app/controller/users.test.js', () => {
    4. describe('GET /users', () => {
    5. it('should work', async () => {
    6. // 通过 factory-girl 快速创建 user 对象到数据库中
    7. await app.factory.createMany('user', 3);
    8. const res = await app.httpRequest().get('/users?limit=2');
    9. assert(res.status === 200);
    10. assert(res.body.length === 2);
    11. assert(res.body[0].name);
    12. assert(res.body[0].age);
    13. });
    14. });
    15. describe('GET /users/:id', () => {
    16. it('should work', async () => {
    17. const user = await app.factory.create('user');
    18. const res = await app.httpRequest().get(`/users/${user.id}`);
    19. assert(res.status === 200);
    20. assert(res.body.age === user.age);
    21. });
    22. });
    23. describe('POST /users', () => {
    24. it('should work', async () => {
    25. app.mockCsrf();
    26. let res = await app.httpRequest().post('/users')
    27. .send({
    28. age: 10,
    29. name: 'name',
    30. });
    31. assert(res.status === 201);
    32. assert(res.body.id);
    33. res = await app.httpRequest().get(`/users/${res.body.id}`);
    34. assert(res.status === 200);
    35. assert(res.body.name === 'name');
    36. });
    37. });
    38. describe('DELETE /users/:id', () => {
    39. it('should work', async () => {
    40. const user = await app.factory.create('user');
    41. app.mockCsrf();
    42. const res = await app.httpRequest().delete(`/users/${user.id}`);
    43. assert(res.status === 200);
    44. });
    45. });
    46. });

    最后,如果我们需要在 CI 中运行单元测试,需要确保在执行测试代码之前,执行一次 migrate 确保数据结构更新,例如我们在 package.json 中声明 scripts.ci 来在 CI 环境下执行单元测试:

    1. {
    2. "scripts": {
    3. "ci": "eslint . && NODE_ENV=test npx sequelize db:migrate && egg-bin cov"
    4. }
    5. }

    完整示例

    更完整的示例可以查看 eggjs/examples/sequelize。

    脚手架

    我们也提供了 sequelize 的脚手架,集成了文档中提供的 egg-sequelize, sequelize-cli 与 factory-girl 等模块。可以通过 npm init egg --type=sequelize 来基于它快速初始化一个新的应用。