在 Egg 中使用 sequelize 操作 mysql

3,630 阅读5分钟

文中完整示例代码已上传至 GitHub, guchongxi/egg-sequelize-demo

环境

2019.10.11

  • MacOS 10.14.6
  • Node 10.16.3
  • Yarn 1.17.3
  • egg 2.24.0
  • egg-bin 4.13.2
  • docker 19.03.1
  • chrome 77.0.3865.90

引入 sequelize

直接使用 egg-sequelize 插件即可

yarn add egg-sequelize mysql2 -S

启用插件

// config/plugin.js

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize'
}

添加配置

// config/config.{env}.js

exports.sequelize = {
  // 数据库类型
  dialect: 'mysql',
  // 数据库地址
  host: '127.0.0.1',
  // 端口
  port: 3306,
  // 数据库名
  database: 'test-database',
  // 用户名,默认为 root
  username: 'root',
  // 密码,默认为空
  password: '',
  // sequelize 配置
  define: {
    // 添加createAt,updateAt,deleteAt时间戳
    timestamps: true,
    // 使用软删除,即仅更新 deleteAt 时间戳 而不删除数据
    paranoid: true,
    // 不允许修改表名
    freezeTableName: true,
    // 禁止驼峰式字段默认转为下划线
    underscored: false,
  },
  // 由于orm用的UTC时间,这里必须加上东八区,否则设置的时间相差8小时
  timezone: '+08:00',
  // mysql2 配置
  dialectOptions: {
    // 让读取date类型数据时返回时间戳而不是UTC时间
    dateStrings: true,
    typeCast(field, next) {
      if (field.type === 'DATETIME') {
        return new Date(field.string()).valueOf();
      }

      return next();
    },
  },
}

使用 Docker Compose 启动数据库

如果仅使用 docker 启动 mysql 可参考 mysql

docker 安装与其他操作请自行 google

建议使用 docker compose,更方便扩展其他容器

新建 docker-compose.yml

version: '3.7'
services:
  mysql:
    image: mysql:5.7
    volumes:
      - data-mysql:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      MYSQL_DATABASE: 'test-database'
    ports:
      - '3306:3306'
    restart: unless-stopped
volumes:
  data-mysql:

启动容器

docker-compose up -d

停止容器

docker-compose down

停止并销毁数据

docker-compose dowm -v

初始化表结构

设计表

实体

  • user 用户
  • app 应用 由用户创建
  • task 任务 可能由用户创建指定应用的任务
  • info 用户信息
  • group 应用群组 群组和包含多个应用,应用可加入多个群组

表结构

  • users -> 存储用户
  • infos -> 存储用户信息,使用 userId 关联 user
  • apps -> 存储应用,通过 userId 关联 user
  • tasks -> 存储任务,通过 appId 关联 app,通过 userId 关联 user
  • groups -> 存在群组
  • group_app -> 存储群组和应用关系,通过 appId 关联 app,通过 groupId 关联 group

表关系

  • users
    • 与应用是 一对多
    • 与任务是 一对多
    • 与信息是 一对一
  • infos
    • 与用户是 一对一
  • apps
    • 与用户是 多对一
    • 与任务是 一对多
    • 与群组是 多对多
  • tasks
    • 与用户是 多对一
    • 与应用是 多对一
  • groups
    • 与应用是 多对多

生成初始化数据模型

为方便之后对数据模型进行变更和简化操作,我们使用 sequelize-cli 来做数据库初始化

yarn add sequelize-cli -D

npx sequelize init

更改配置

  • database/config.json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "test-database",
    "host": "127.0.0.1",
    "port": 3306,
    "dialect": "mysql",
    "timezone": "+08:00",
    "define": {
      "charset": "utf8",
      "dialectOptions": {
        "collate": "utf8_general_ci"
      }
    }
  }
}
  • .sequelizerc
'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model')
};

定义users表

npx sequelize model:generate --name users --attributes username:string,email:string
// database/migrations/{time}-create-users.js

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable(
      'users',
      {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER,
        },
        username: {
          allowNull: false,
          type: Sequelize.STRING,
        },
        email: {
          allowNull: false,
          type: Sequelize.STRING,
        },
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE,
        },
        updatedAt: {
          allowNull: false,
          type: Sequelize.DATE,
        },
        deletedAt: {
          type: Sequelize.DATE,
        },
      },
      {
        charset: 'utf8',
      },
    );
  },
  down: (queryInterface) => {
    return queryInterface.dropTable('users');
  },
};

定义tasks表

npx sequelize model:generate --name tasks --attributes appId:integer,userId:integer,status:integer,description:string
// database/migrations/{time}-create-tasks.js

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable(
      'tasks',
      {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER,
        },
        appId: {
          allowNull: false,
          type: Sequelize.INTEGER,
        },
        // 因为可能不是用户操作,允许为 null
        userId: {
          type: Sequelize.INTEGER,
        },
        status: {
          allowNull: false,
          type: Sequelize.INTEGER,
        },
        description: {
          type: Sequelize.STRING,
        },
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE,
        },
        updatedAt: {
          allowNull: false,
          type: Sequelize.DATE,
        },
        deletedAt: {
          type: Sequelize.DATE,
        },
      },
      {
        charset: 'utf8',
      },
    );
  },
  down: (queryInterface) => {
    return queryInterface.dropTable('tasks');
  },
};

其他表参照 users 表生成即可

生成初始化数据

为方便测试,我们可以预先将模拟数据一并生成

npx sequelize seed:generate --name users
// database/seeders/{time}-users.js

'use strict';

module.exports = {
  up: (queryInterface) => {
    return queryInterface.bulkInsert(
      'users',
      [
        {
          username: 'test user',
          email: 'test@test.com',
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ],
      {},
    );
  },

  down: (queryInterface) => {
    return queryInterface.bulkDelete('users', { email: 'test@test.com' }, {});
  },
};

其他表按照相同步骤生成即可

执行初始化

# 初始化表结构
npx sequelize db:migrate
# 初始化表数据
npx sequelize db:seed:all

这样数据库中的表和数据就建好了

Mac OS 用户可使用 Sequel Pro 可视化查看数据库

定义 Model & 表间关系

在初始化数据模型时,同时生成的还有 app/model/*.js

users.js

'use strict';

module.exports = (app) => {
  const { Sequelize, model } = app;
  const Users = model.define(
    'users',
    {
      // 用户名
      username: Sequelize.STRING,
      // 邮箱
      email: Sequelize.STRING,
    },
    {
      // 默认不输出 deletedAt 字段
      // 参考 https://sequelize.org/master/class/lib/model.js~Model.html#static-method-scope
      defaultScope: {
        attributes: {
          exclude: ['deletedAt'],
        },
      },
    },
  );

  Users.associate = function () {
    // 与应用是 一对多
    app.model.Users.hasMany(app.model.Apps, {
      foreignKey: 'userId',
    });
    // 与任务是 一对多
    app.model.Users.hasMany(app.model.Tasks, {
      foreignKey: 'userId',
    });
    // 与信息是 一对一
    app.model.Users.hasOne(app.model.Infos, {
      foreignKey: 'userId',
    });
  };

  return Users;
};

apps.js

'use strict';

module.exports = (app) => {
  const { Sequelize, model } = app;
  const Apps = model.define(
    'apps',
    {
      // 创建用户 id
      userId: Sequelize.INTEGER,
      // 应用中文名
      name: Sequelize.STRING,
      // 应用仓库地址,ssh 风格
      gitUrl: Sequelize.STRING,
    },
    {
      defaultScope: {
        attributes: {
          exclude: ['deletedAt'],
        },
      },
    },
  );

  Apps.associate = function () {
    // 与用户是 多对一
    app.model.Apps.belongsTo(app.model.Users, {
      foreignKey: 'userId',
    });
    // 与任务是 一对多
    app.model.Apps.hasMany(app.model.Tasks, {
      foreignKey: 'appId',
    });
    // 与群组是 多对多
    app.model.Apps.belongsToMany(app.model.Groups, {
      through: app.model.GroupApp,
      foreignKey: 'appId',
      otherKey: 'groupId'
    });
  };

  return Apps;
};

其他 model 参考 egg-sequelize-demo/app/model/

Model 关系

参考:associations

model 中我们定义了表间的关联关系可以看出主要是三种关系:一对一,一对多,多对多

一对一

在一对一中,一般会有主从关系,示例为 user(主) 和 info(从)

对于主使用 hasOne(Model, { foreignKey: '从表中关联主表的字段名,示例为 userId' })

对于从使用 belongsTo(Model ,{ foreignKey: '从表中关联主表的字段名,示例为 userId' })

一对多

在一对多中,一为主,多为从,示例为 user(主) 和 app(从)

对于主使用 hasMany(Model, { foreignKey: '从表中关联主表的字段名,示例为 userId' })

对于从使用 belongsTo(Model, { foreignKey: '从表中关联主表的字段名,示例为 userId' })

多对多

在多对多中,不需要区分主从的概念

双方都使用 belongsToMany(Model, { through: MiddleModel, foreignKey: '中间表中关联自己的字段名,如果在 group 中定义则为 groupId', otherKey: '中间表中关联另一方的字段名,如果在 group 中定义则为 appId' })

多表联查

通常,我们会在 service 中处理与数据库的交互

示例1:获取 app 列表,app 对象包含对应 user,包含所有 task,和所属 group

// app/service/apps.js
'use strict';

const Service = require('egg').Service;

class Apps extends Service {
  async list() {
    return this.ctx.model.Apps.findAll({
      include: [{
        model: this.ctx.model.Users,
        as: 'user',
      }, {
        model: this.ctx.model.Tasks,
        as: 'tasks',
      }, {
        model: this.ctx.model.Groups,
        as: 'groups',
        // 配置 groups 元素输出 name,desc 字段
        attributes: ['name', 'desc'],
      }],
    });
  }
}

module.exports = Apps;

示例2:获取 group 列表,group 对象包含所属 app,app 对象包含对应 user

// app/service/groups.js
'use strict';

const Service = require('egg').Service;

class Groups extends Service {
  async list() {
    return this.ctx.model.Groups.findAll({
      include: [{
        model: this.ctx.model.Apps,
        as: 'apps',
        include: [{
          model: this.ctx.model.Users,
          as: 'user'
        }]
      }],
    });
  }
}

module.exports = Groups;

示例3:获取 task 列表,task 对象包含对应 app,包含对应 user

// app/service/tasks.js

'use strict';

const Service = require('egg').Service;

class Tasks extends Service {
  async list() {
    return this.ctx.model.Tasks.findAll({
      include: [{
        model: this.ctx.model.Users,
        as: 'user',
      }, {
        model: this.ctx.model.Apps,
        as: 'app',
      }],
    });
  }
}

module.exports = Tasks;

示例4:获取 user 列表,user 对象包含所有 app,包含所有 task,包含唯一 info

// app/service/users.js

'use strict';

const Service = require('egg').Service;

class Users extends Service {
  async list() {
    return this.ctx.model.Users.findAll({
      include: [{
        model: this.ctx.model.Apps,
        as: 'apps',
      }, {
        model: this.ctx.model.Tasks,
        as: 'tasks',
      }, {
        model: this.ctx.model.Infos,
        as: 'info',
      }],
    });
  }
}

module.exports = Users;

END