和同事一块儿有一个公司内部平台的项目,平台须要对于用户上传的图片,视频等资源进行管理和存储。html
在项目一期,因为申请DB资源的流程比较复杂,因此咱们仅仅将用户上传的内容记录存储在了localStorage
中,固然这是很不安全的,很是容易丢失,因此在二期的时候,咱们开始了接入DB的工做。前端
下面的整篇文章都会和这部分描述的技术栈相关,固然,sequelize
在实际业务场景的使用相关内容,其实自己和技术栈的关系并非很是大,若是你在sequelize
的使用过程当中遇到了问题,这里或许会有解答~node
采用mysql做为数据存储引擎,其实自己也是无奈之举,由于DBA告诉咱们目前MongoDB的资源不足,而且对于这种结构化数据的存储,mysql对于将来将平台扩展到整个公司使用,甚至对外开放,则是必须的。mysql
做为一款比较成熟的node.js开发框架,公司toC端不少业务架构都使用了基于egg的ReactSSR,egg对于sequelize
的支持仍是比较好的,提供了专门的插件来辅助使用sequelize
进行DB接入。sql
这才是本文的核心,sequelize
目前能够说是目前最为成熟的node.js ORM框架了。CRUD操做不可能彻底使用SQL语句进行,这样很容易出现各类SQL漏洞,一个成熟的ORM框架能够帮咱们避免掉这些风险,而且将CRUD操做封装成对象函数方法以后,操做起来也更加方便,可是这样会提高必定的开发学习成本。数据库
总体的方案都出来了,剩下的就是爬坑。因为之前仍是作过一些和数据库有关的工做,SQL语句和部分ORM的实现还有过一点接触,可是。。。我依然在坑里栽了好久,长成了参天大树,提及参,我就想到西游记里面的人参果。。。文体两开花。npm
接入DB,首先须要考虑就是如何设计数据模型。固然这对于前端来讲,仍是有一些难度的。因而请教了最近在合做的后端大哥。后端
通常来讲,一个系统都须要有特定的用户进行登陆。又须要对于用户进行分组,一个用户能够加入多个组,一个组能够有多个用户。n:m的关联关系须要在设计数据模型的时候就体现出来。api
生产环境中,n:m的关联是须要中间表来辅助的,来存储例如用户和用户组之间的映射关系。安全
关联能够经过设置foreignKey
来进行关联,可是被后端大哥批斗了,为了保证数据库的性能,通常不多采用外键关联两个数据模型,而是采用逻辑关联,经过开发者人工保证写入和删除的顺序。
索引是必不可少的,咱们在提交DBA工单的时候,必需要创建索引,尤为是有些数据表会存储很是多的记录,这时对于主键创建索引,能够大幅提升查找的效率。
根据上面的三个重点,完成了个人数据表设计。这里能够给出一个简单的栗子数据库模型。后面的实现也是根据这个栗子进行的。
CREATE TABLE `group` (
`id` INTEGER PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`db_create_time` DATETIME,
`db_update_time` DATETIME
);
CREATE TABLE `user` (
`id` INTEGER PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`db_create_time` DATETIME,
`db_update_time` DATETIME
);
CREATE TABLE `group_users` (
`user` INTEGER NOT NULL,
`group` INTEGER NOT NULL,
CONSTRAINT `pk_group_users` PRIMARY KEY (`user`, `group`)
);
CREATE INDEX `idx_group_users` ON `group_users` (`group`);
复制代码
egg框架自己提供了不少即插即用的plugin
,官方最基本的插件集中就有egg-sequelize
插件。插件配置起来很是简单。
下面可能不会说的很是详细,我主要讲一讲本身在进行开发时候遇到的各类坑。
|-- app // node服务端相关代码
|-- controller
|-- api // node端接口controller
|-- group.js // 组相关controller
|-- user.js // 用户相关controller
|-- extend
|-- helper.js // helper扩展
|-- middleware
|-- model // sequelize数据模型
|-- user.js
|-- group.js
|-- group_user.js
|-- service // 可复用的数据处理及查询方法
|-- utils // service中拿不到helper,部分utils放在这里
|-- router.js // 路由
|-- build // 构建代码
|-- client // 客户端相关代码
|-- config // 配置文件
复制代码
// config.local.js
module.exports = {
sequelize: {
// 数据库类型
dialect: 'mysql',
// 数据库名
database: 'swiss',
// 数据库IP和端口
host: '127.0.0.1',
port: '3306',
// 数据库链接的用户和密码
username: 'root',
password: '123',
// 是否自动进行下划线转换(这里是由于DB默认的命名规则是下划线方式,而咱们使用的大多数是驼峰方式)
underscored: true,
// 时区,sequelize有不少自动时间的方法,都是和时区相关的,记得设置成东8区(+08:00)
timezone: '+08:00',
},
}
复制代码
各类配置项一目了然,记得要设置好timezone
,不然你全部默认为当前时间的值都会出错。underscored
表示自动将驼峰表示法转换为mysql的下划线表示法(固然后面会说到,他的转换机制有些时候让我感受费解,但愿了解的大佬们能够帮我解释一下~)。
直接将启动项配置写死在配置文件里面不是不能够,可是若是须要和同事一块儿合做开发的话,这样写死可能不够灵活。能够将某些配置项提取出来,经过命令行传入参数,来进行开发环境的动态配置。
~ npm run dev -- --u=root --p=123
// config.local.js
let DB_USER = 'root';
let DB_PASSWORD = '123';
const ARGV_2 = JSON.parse(process.argv[2] || {});
DB_USER = (ARGV_2 && ARGV_2.u) || 'root';
DB_PASSWORD = (ARGV_2 && ARGV_2.p) || '123';
module.exports = {
sequelize: {
// ....
username: `${DB_USER}`,
password: `${DB_PASSWORD}`,
// ....
}
};
复制代码
model
egg-sequelize会自动将sequelize
实例挂载到app.model
上面,而后静态方法和属性则会直接被绑定到app
上,经过app.Sequelize
进行获取。
model
层做为MVC的最底层,须要注意到数据模型的pure,model
文件也应该是纯净的,这个文件里面应该是和数据库中的表一一对应,一个model
文件对应一个DB中的表,这个文件中不该该包含任何和逻辑相关的代码,应该彻底是数据模型的定义。
// app/model/user.js
module.exports = app => {
// egg-sequelize插件会将Sequelize类绑定到app上线,从里面能够取到各类静态类型
const { TEXT, INTEGER, NOW } = app.Sequelize;
const User = app.model.define(
'user',
{
name: TEXT,
createAt: {
type: DATE,
// 能够重写某个字段的字段名
field: 'db_create_time',
allowNull: false,
defaultValue: NOW,
},
updateAt: {
type: DATE,
field: 'db_update_time',
allowNull: false,
defaultValue: NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: 'users',
underscored: true,
}
);
// 定义关联关系
User.associate = () => {
// 定义多对多关联
User.belongsToMany(app.model.Groups, {
// 中间表的model
through: app.model.groupUser,
// 进行关联查询时,关联表查出来的数据模型的alias
as: 'project',
// 是否采用外键进行物理关联
constraints: false,
});
// 这里若是一个模型和多个模型都有关联关系的话,关联关系须要统必定义在这里
};
return User;
};
复制代码
上面的代码有很是多须要注意的地方,咱们经过这个文件定义了一个数据模型,这个模型能够映射到数据库中的某一个表,这里就是映射到了users
表,用来存储用户信息。
id
字段会被设置为主键,而且是AUTO_INCREMENT
的,不须要咱们本身声明;timestamps
字段能够表示是否采用默认的createAt
和updateAt
字段,咱们经过field
字段重写了这两个字段的字段名;associate
字段能够用来设置数据模型的关联关系,若是一个数据模型关联了多个数据模型,那么这个方法里面也能够定义多个关系;belongsToMany
表示n:m的关系映射,这个在官方文档中描述的很是清楚了;as
能够为这个映射设置别名,这样在进行查询的时候,获得的结果就是以别名来标识的;constraints
:这个属性很是重要,能够用来表示这个关联关系是否采用外键关联。在大多数状况下咱们是不须要经过外键来进行数据表的物理关联的,直接经过逻辑进行关联便可;through
:这个属性表示关联表的数据模型,也就是保存关联关系的数据库表的模型。上面的这些属性,在开发过程当中多多少少都消耗了我一些时间**-1s**,模型的设置和数据库表之间的关系很是紧密,必定要保证你的数据模型和数据表之间没有歧义。
一样地,咱们能够定义到关联表和中间表的模型:
// app/model/group.js
module.exports = app => {
const { TEXT, INTEGER, NOW } = app.Sequelize;
const Group = app.model.define(
'group',
{
name: TEXT,
createAt: {
type: DATE,
field: 'db_create_time',
allowNull: false,
defaultValue: NOW,
},
updateAt: {
type: DATE,
field: 'db_update_time',
allowNull: false,
defaultValue: NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: 'groups',
underscored: true,
}
);
// 定义关联关系
Group.associate = () => {
Group.belongsToMany(app.model.User, {
through: app.model.groupUser,
as: 'partner',
constraints: false,
});
};
return Group;
};
// app/model/group_user.js
// 中间表不须要定义关联关系
module.exports = app => {
const { INTEGER } = app.Sequelize;
const GroupUser = app.model.define(
'group_user',
{
user_id: INTEGER,
group_id: INTEGER,
},
{
timestamps: false,
freezeTableName: true,
tableName: 'group_user',
underscored: true,
}
);
return GroupUser;
};
复制代码
controller
在egg中,controller
模块的做用相似于MVC模式中的控制器,进行从model
到view
的转换,而在提供接口的时候,controller
负责的是提供从model
到api
的转换,通过model
从数据库中查询出来的结果,将在controller
里面进行包装,而后返回给接口的调用者。
在进行数据访问的时候,不少的接口请求均可以拆分为几个相似的CRUD操做,好比:
service
里面。而controller
只负责请求的响应处理。当一个接口请求跨过了middleware
的处理,通过了router
的分发以后:
// app/router.js
module.exports = app => {
app.get('/api/user/get', app.controller.api.user.get);
app.post('/api/group/set', app.controller.api.group.set);
}
复制代码
会被转发到对应的controller
进行处理。
// app/controller/user.js
module.exports = class UserController extends Controller {
async get = () => {
const { uuid } = this.ctx.session;
if (!uuid) {
ctx.body = {
code: 401,
message: 'unauthorized',
};
return;
}
const userInfo = await this.ctx.service.user.getUserById({ id: uuid });
if (userInfo) {
ctx.body = {
code: 200,
message: 'success',
data: userInfo
}
} else {
ctx.body = {
code: 500,
message: 'error',
}
}
}
}
复制代码
service
egg官方文档对于service的描述是这样的:
简单来讲,Service 就是在复杂业务场景下用于作业务逻辑封装的一个抽象层,提供这个抽象有如下几个好处:
- 保持 Controller 中的逻辑更加简洁。
- 保持业务逻辑的独立性,抽象出来的 Service 能够被多个 Controller 重复调用。
- 将逻辑和展示分离,更容易编写测试用例。
也就是controller
中要尽可能保持clean,而后,能够复用的业务逻辑被统一抽出来,放到service
中,被多个controller
进行复用。
咱们将CRUD操做,所有提取到service
中,封装成一个个通用的CRUD方法,来提供给其余service
进行嵌套的时候调用,或者提供给controller
进行业务逻辑调用。
好比:读取用户信息的过程:
// app/service/user.js
module.exports = class UserService extends Service {
// 经过id获取用户信息
async getUserById = ({
id,
}) => {
const { ctx } = this;
let userInfo = {};
try {
userInfo = await ctx.model.User.findAll({
where: {
id,
},
// 查询操做的时候,加入这个参数能够直接拿到对象类型的查询结果,不然还须要经过方法调用解析
raw: true,
});
} catch (err) {
ctx.logger.error(err);
}
return userInfo;
}
}
复制代码
sequelize
事务以前有说到,在创建模型的时候,咱们创建了User
和Group
之间的关联关系,而且经过了一个关联表进行二者之间的关联。
因为咱们没有创建二者之间的外键关联,因此在写入的时候,咱们要进行逻辑的关联写入。
若是咱们须要新建一个用户,而且为这个用户新建一个默认的group
,因为组和用户有着多对多的关系,因此这里咱们采用belongsToMany
来创建关系。一个用户能够属于多个组,而且一组也能够包含多个用户。
在创建的时候,须要按照必定的顺序,写入三张表,一旦某个写入操做失败以后,须要对于以前的写入操做进行回滚,防止DB中产生垃圾数据。这里须要用到事务机制进行写入控制,而且人工保证写入顺序。
// app/service/user.js
module.exports = class UserService extends Service {
async setUser = ({
name,
}) => {
const { ctx } = this;
let transaction;
try {
// 这里须要注意,egg-sequelize会将sequelize实例做为app.model对象
transaction = await ctx.model.transaction();
// 建立用户
const user = await ctx.model.User.create({
name,
}, {
transaction,
});
// 建立默认组
const group = await ctx.model.Group.create({
name: 'default',
}, {
transaction,
});
const userId = user && user.getDataValue('id');
const groupId = group && group.getDataValue('id');
if (!userId || !groupId) {
throw new Error('建立用户失败');
}
// 建立用户和组之间的关联
const associate = await ctx.mode.GroupUser.create({
user_id: userId,
group_id: groupId,
}, {
transaction,
});
await transaction.commit();
return userId;
} catch (err) {
ctx.logger.error(err);
await transaction.rollback();
}
}
}
复制代码
经过sequelize
提供的事务功能,能够将串联写入过程当中的错误进行回滚,保证了每次写入操做的原子性。
既然咱们已经建立了关联关系,那么若是经过关联关系,查询到对应的数据库内容呢?
在多对多的关联条件下,若是咱们要查询某个用户的全部分组信息,须要经过用户id来查询其关联的全部group
。
// service
async getGroupByUserId = ({
id,
}) => {
const { ctx } = this;
const group = await ctx.model.User.findAll({
attributes: ['project.id', 'project.name'],
include: [
{
model: ctx.model.Group,
as: 'project',
// 指定关联表查询属性,这里表示不须要任何关联表的属性
attributes: [],
through: {
// 指定中间表的属性,这里表示不须要任何中间表的属性
attributes: []
}
}
],
where: {
id,
},
raw: true,
// 这个须要和上面的中间表属性配合,表示不忽略include属性中的attributes参数
includeIgnoreAttributes: false,
});
}
复制代码
经过上面的关联查询方法,能够获得这样的一条SQL语句:
SELECT `project`.`id`, `project`.`name` FROM `users` AS `user` LEFT OUTER JOIN ( `group_user` AS `project->group_user` INNER JOIN `groups` AS `project` ON `project`.`id` = `project->group_user`.`group_id`) ON `user`.`id` = `project->group_user`.`user_id` WHERE `user`.`id` = 1;
复制代码
对应的查询结果:
[ { id: 1, name: 'default' } ]
复制代码
而在一对多和多对一的关系下,其本质和多对多基本上是一致的,是在多的方向存储一个冗余字段,来保存其对应的惟一元素的主键,不管是何种关系,其默认在sequelize
中实现的数据模型,都是范式化的,若是须要反范式来提升数据库效率,仍是须要本身去作冗余的。
hooks
在数据库查询的过程当中,不免须要在真正的CRUD先后进行一些数据的处理。
考虑到这样的一个场景:
在客户端,咱们前端存储的用户名并非经过name
来表示的,而是经过nickName
字段来进行表示的,在每次进行读写操做以前,例如这里容许用户本身修改本身的名字,当请求发送到服务端以后,交予service
进行处理。
// app/model/user
const User = app.model.define({
// ...
}, {
hooks: {
beforeUpdate: (user, options) => {
const name = user.nickName;
delete user.nickName;
user.name = name;
}
}
});
复制代码
咱们在定义模型的时候,直接定义好这个hook
,beforeUpdate
会在User
模型每次调用update
以前,调用这个hook。这个hook会传入update操做传入的参数实例,能够直接对这个实例进行修改,保证明际update
操做的实例是正确的。
hooks
可使用的地方不少,这里只是简单介绍一下使用的方法,hooks
中间也能够包含异步操做,可是要注意,若是包含异步操做的话,须要返回一个Promise
。咱们还能够在进行具备反作用的操做以前,对于用户权限进行校验。
hooks
的使用是须要了解到其功能,而后根据本身的业务场景,灵活地进行使用的。
egg提供了很是多的可扩展空间,除了使用其做为前端页面的部署环境以外,还能够承担一些model
层的工做,有兴趣的小伙伴能够试下经过egg实现先后端分离的全栈开发工做~
在实际业务场景的实践过程当中,sequelize
的不少解决方案都要从官方文档中一个字一个字的查找,有些问题甚至须要去翻issue
才能找到对应的处理方法,不知道为何官方文档会有那么多版的中文翻译。。实践的方案却特别少,前端的小伙伴们大部分仍是热衷于MongoDB。确实关系型数据的操做相较于NoSQL仍是比较复杂的。不过解决问题的过程虽然烦恼,可是结果总仍是愉悦的。
sequelize
还有不少须要挖掘的地方,它自己提供的不少功能在此次迭代的过程当中都没有用到。好比scope
、migration
,有机会能够尝试下一些新的功能和实现方案。