数据库 - 985ch/egg-sachikawa GitHub Wiki
既然是后端框架,那就必须要能连接使用数据库。在为数众多的数据库连接方案中,sachikawa选择了基于sequelize的egg插件egg-sequelize来管理数据库。
数据库的配置为app.config.sequelize,该配置支持单数据库连接和多数据库连接两种配置方式。
在大多数中小型项目中,我们只需要连接一个数据库。这时候我们可以使用如下单数据库连接配置
// config/sequelize.js
exports.sequelize = {
dialect: 'mysql', // 连接的数据库类型,共支持: mysql, mariadb, postgres, mssql
database: 'test', // 数据库名
host: 'localhost', // 服务器地址
port: 3306, // 服务器端口,默认3306
username: 'root', // 连接到数据库的账号,实践中应避免将其直接写在配置中
password: '', // 连接到数据库的密码,实践中应避免将其直接卸载配置中
// delegate: 'myModel', // 加载对象名,用于生成同名的sequelize实例挂载到Application和Context对象下。可通过ctx[delegate]和app[delegate]访问,其默认值为`model`,即将对象挂载为ctx.model和app.model。
// baseDir: 'my_model', // 数据模型文件相对于app文件夹的路径,指定目录下的脚本文件将会作为数据模型文件被解析并挂载到对饮的sequelize实例下。其默认值为`model`,即从app/model加载数据模型文件。
// exclude: 'index.js', // 忽略的文件列表,指定的文件将不会作为数据模型文件被解析。可以是字符串或字符串数组。
// 可以添加更多的sequelize选项
};// config/config.{env}.js
config.sequelize = require('./sequelize');在大中型项目中,我们可能需要连接一个以上的数据库配置,这个时候我们可以按如下方式来配置数据库
// config/sequelize.js
exports.sequelize = {
datasources: [
{
delegate: 'model', // 设置对应sequelize实例为app.model和ctx.model
baseDir: 'model/database1', // 从app/model/database1目录加载数据模型
database: 'database1',
// 其他sequelize配置
},
{
delegate: 'admninModel', // 设置对应sequelize实例为app.admninModel和ctx.admninModel
baseDir: 'model/database2', // 从app/model/database2目录加载数据模型
database: 'database2',
// 其他sequelize配置
},
],
};// config/config.{env}.js
config.sequelize = require('./sequelize');在实际项目中,我们并不建议如上述示例一样配置数据库,这样配置有以下问题。
- 数据库的账号和密码作为明文直接写在配置中,既不利于在测试和正式环境中切换,也不利于数据库安全。通常我们会用环境变量来代替部分参数,以更好的管理权限。
- 在配置较长或者需要连接的数据库较多时,将所有配置都写在config文件中会导致config文件过长,而不利于维护。通常我们会单独针对数据库建立一个配置文件,并通过require将其引入到config配置中。
- 在多数据库连接中,大多数连接配置都相差无几,却要编写每个数据库的完整配置JSON。我们可以通过lodash的assgin方法可以简单的把多数据库里重复的配置省略掉,从而达到精简配置代码的目的。
由于数据库插件默认时启用的,即使在没有配置数据库的情况下,项目也会去尝试连接数据库,这会导致项目无法正常启动。因此,如果开发者需要一个不需要连接数据库的后端项目,或打算用其他数据库连接方案来替代egg-sequelize的话,可以通过禁用数据库插件来避免这个问题。
只要在config/plugin.js禁用数据库插件即可:
// config/plugin.js
exports.sequelize = {
enable: false,
package: 'egg-sequelize',
},在sequelize中,数据模型是一个非常重要的概念,数据模型可以把关系型数据库里的表作为对象来进行处理。随后,开发者就可以像操作普通的对象一样执行SQL语句,而不再需要在代码逻辑中用字符串去拼写一个个SQL语句。这既提升了代码的可维护性,也避免了由于开发者水平不足导致的注入漏洞。
一个简单的数据模型文件如下
'use strict';
module.exports = app => {
const DataTypes = app.Sequelize;
const model = app.ksbaoDB.define('users', {
id: { type: DataTypes.INTEGER(11), allowNull: false, primaryKey: true, autoIncrement: true }, // 用户ID
name: { type: DataTypes.STRING(1024), allowNull: false }, // 用户名
password: { type: DataTypes.STRING(100), allowNull: false }, // 密码
}, {
tableName: 'users',
});
return model;
};由于数据模型文件的结构比较复杂,人工编写这种数据模型文件会花费大量的时间。因此在实际开发中我们通常会使用工具来自动生成数据模型文件。通常,我们使用egg-sequelize-mg来完成这项工作。
- 由于框架并没有自带egg-sequelize-mg包,因此首先要安装这个包到devDependencies下
$ npm i egg-sequelize-mg --save-dev- 在项目目录下新建一个toolkit文件夹,并在该文件夹里添加一个脚本,名叫gen-models.js。内容如下:
// toolkit/gen-models.js
'use strict';
const path = require('path');
const generator = require('egg-sequelize-mg');
const config = require('../config/sequelize'); // 数据库配置脚本
const tables = { // 可以在这里配置每个数据库需要生成的数据模型,没有配置的数据库默认生成所有表的数据模型
// sample: ['table1', 'table2']
}
const fileHead = `'use strict';
const utils = require('egg-sachikawa').Utils;
module.exports = app => {
const DataTypes = app.Sequelize;
`;
const fileTail = `
utils.extendModel(model);
model.associate = function() {
};
return model;
};
`;
// 连接数据库,查询表信息并输出数据模型文件到指定目录
generator.generate(config, tables, path.join(__dirname, '../app/'), async (appRoot, config, tables) => {
const result = await generator.readMysql(appRoot, config, tables);
result.config.fileHead = fileHead;
result.config.fileTail = fileTail;
return result;
});- 修改package.json文件,在scripts里添加一行命令
"scripts": {
"gen-models": "node ./toolkit/gen-models"
},- 准备完成,以后直接执行命令
npm run gen-models就可根据开发者的配置,在对应的目录下生成或更新对应的数据模型文件。
由该方法生成的数据模型文件中,会有两行特别的注释:
// -------- begin sequelize-mg replace --------
// -------- end sequelize-mg replace --------在这两行注释中的部分,我们称之为覆盖区,而这两行注释则是覆盖区开始和结束的标记。为了便于开发者对数据模型进行扩展和配置,egg-sequelize-mg在重新生成模型时会对原本的数据模型文件进行判断,仅当数据模型有变化的时候才会更新覆盖区内的内容,而开发者在覆盖区外做的修改则不会受到影响。
由于覆盖区标记的特殊作用,所以在任何情况下都不要修改或删除这两行注释。
在完成数据库配置并生成数据模型之后,开发者就可以在代码中操作数据库了。
假设开发者配置了一个名为model的数据库,并且在对应的目录下生成了一个名为users.js的数据模型文件。我们就可以在代码中像这样访问这个数据模型:
app.model.Users
ctx.model.Users在上面的例子中,app和ctx分别是egg.js的Application和Context实例。我们注意到,尽管数据模型文件名都是小写,但是访问对应模型的时候第一个字母却变成了大写。这是egg-sequelize为了避免数据模型的名字和Sequelize实例的原有方法重名而做的处理。
egg-sequelize的文档还有更多的关于数据表的转换规则。
// 查询users表里所有name=985ch的数据,并返回其id字段
ctx.model.Users.findAll({
attributes: [ 'id' ],
where: { name: '985ch' },
raw: true // 原始查询,实践中很多时候只需要获得查询结果而不需要进行后续操作,因此一般都会加上这条限制以降低数据处理成本
});// 找到users表里id=1的数据并将其name设为985ch
ctx.model.Users.update({ name: '985ch' }, {
where: { id: 1 }
});// 往users添加一条name='985ch'并且password=''的数据
ctx.model.Users.create({ name: '985ch', password: '' });请尽可能的避免删除数据的操作,建议采用废弃标记来取代删除操作
关于sequelize的更多操作,请自行查找sequelize相关文档。
为了让数据模型使用起来更加方便,框架在工具包里提供了extendModel方法用于扩展数据模型。对某个数据模型进行扩展之后,就能直接在该数据模型上使用如simpleFind等便利方法。
该方法用于扩展数据模型,给数据模型追加simpleFind等方法使其更易于使用。
- model:要扩展的数据模型对象
- defAttr:使用扩展方法时,默认查询的字段。若省略该参数则默认查询所有字段
- filters:数据过滤器,是一个数组。通过该参数,可以指定在使用扩展方法进行查询时,如何处理指定的列。
filters每个成员都是一个JSON,结构如下
| 属性 | 类型 | 说明 |
|---|---|---|
| list | Array | 一个数组,列出所有使用该方法处理的列 |
| filter | Any => Any | 一个方法,该方法接受Sequelize查出来的原始数据,并返回处理后的数据 |
该方法没有返回
假设有数据模型Configs如下
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 自增ID |
| name | STRING | 配置名称 |
| config | STRING | 配置内容,是一个序列化之后的JSON |
| remark | STRING | 备注 |
const defAttr = [ 'id', 'name', 'config' ]; // 默认不查备注数据
// 对config字段执行JSON.parse方法。这样就能让出来的结果直接就是JSON而非字符串了
const remark = [{
list: [ 'config' ],
filter: raw => JSON.parse(raw),
}];
Utils.extendModel(Configs, defAttr, remark); // 执行之后,model就可以直接使用如下的simpleFind等方法进行查询了对findAll方法的封装,降低了拼写查找JSON的工作量,并且查出来的结果已经经过数据过滤器的处理。
- where:等同于查找JSON里的where。省略该参数则是查全表
- attributes:等同于查找JSON里的attributes。省略该参数则使用extendModel里配置的defAttr作为默认属性
- t:事务实例,仅当在事务中进行操作的时候需要传入该参数
返回一个数组,数组内是处理过后的查找结果
const rows = await Configs.simpleFind(); // 查询configs表全表,并返回默认属性id,name和config,其中config是JSON对象对findOne方法的封装,降低了拼写查找JSON的工作量,并且查出来的结果已经经过数据过滤器的处理。
- where:等同于查找JSON里的where。省略该参数则是查全表
- attributes:等同于查找JSON里的attributes。省略该参数则使用extendModel里配置的defAttr作为默认属性
- t:事务实例,仅当在事务中进行操作的时候需要传入该参数
返回一个JSON对象或是空值,取决于是否查到符合条件的数据
const result= await Configs.simpleFindOne({ name: [ 'default' ] }, [ 'id' ]); // 找到一条name='default'的数据并返回一个仅包含其id的JSON对象即调用extendModel时传入的defAttr,将其单独储存起来更便于在Sequelize原本的查找方法findAll和findOne中使用
该方法会把传入的obj经数据过滤器的处理之后输出,obj可以是单个JSON对象和数组,通常用于处理用findOne和findAll方法查找到的结果。
- obj:即需要处理的对象或数组,允许为undefined
返回处理之后的对象或数组,对于没有配置filters的模型,实际不会进行任何处理
const rows = await Configs.findAll({
attributes: Configs.defaultAttributes,
limit: 5,
raw: true,
});
const result = Configs.filterAll(rows);该方法传入一个finder对象,返回一个根据该对象生成的查询语句,通常用在条件较为复杂的查询中。
- finder:查询JSON,和普通的查询JSON相比,多了一个prefix参数,该参数用于往生成的查询语句前添加特定的关键字,比如"EXISTS"或者"NOT EXISTS"
返回生成语句对应的literal对象,该对象可以直接用在另一个查找语句的where属性中
const rows = await Persons.findAll({
attributes: [ 'name' ],
where: PersonBooks.subQuery({
prefix: 'EXISTS',
attributes: [ sequelize.literal('1') ],
where: {
bookName: [ '高等数学', '线性代数' ],
personID: { [sequelize.Op.eq]: sequelize.col('persons.id') },
},
limit: 1,
}),
raw: true,
});可以,但是需要在数据模型文件里预先配置好关联关系,并且使用起来比较麻烦。详情自行搜索 Sequelize 联表查询
可以,但是并不推荐。我们之所以选用Sequelize来管理数据库,就是为了尽可能避免自己拼写SQL语句导致的风险。如果非要写原生SQL语句,建议尽量写在数据模型中并且封装成一个扩展方法,并且确保编写这个方法的人具备足够的数据库知识和风险防范意识。