文中完整示例代码已上传至 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 关联 userapps
-> 存储应用,通过 userId 关联 usertasks
-> 存储任务,通过 appId 关联 app,通过 userId 关联 usergroups
-> 存在群组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