基础介绍
Sequelize是针对node.js和io.js提供的ORM框架。具体就是突出一个支持广泛,配置和查询方法统一。它支持的数据库包括:PostgreSQL、 MySQL、MariaDB、 SQLite 和 MSSQL。
基本使用
参考官方文档 :Sequelize
初始化 new Sequlize({})
配置
-
{ define }
及是 Model.init 配置。define 只能定义 Model.init 里 options 的相关内容 -
默认会给模型添加
id/createdAt/updatedAt
等字段,可以更改- 原始查询
const [results, metadata] = sequelize.query(mysql)
- 原始查询
-
sequelize.fn(fn, args) 用于创建数据库函数对象。在
where/order
中使用 -
sequelize.col(string) 创建数据库列对象。建议使用
table.colum
完整命名方式attributes: {[Sequelize.fn('DISTINCT', Sequelize.col('external_info.status_code')), 'statusCode']}
[Sequelize.fn('count', Sequelize.col('id')), 'num']
关联查询
A.hasOne(B, { /* 参数 */ });
A.belongsTo(B, { /* 参数 */ });
A.hasMany(B, { /* 参数 */ });
A.belongsToMany(B, { through: 'C', /* 参数 */ });
- 关联表之间定义顺序对结果是有影响的。在上述所有示例中
A
称为源模型,而B
称为目标模型。一个源可对应一个/多个目标,但一个目标只能对一个源。- _foreignKey _外键及可在
hasOne/hasMany
中,也可在belongsTo
中定义。并且也支持对象形式可指定如:type、allowNull、defaultValue 等参数
- _foreignKey _外键及可在
- 一对一:
hasOne
和belongsTo
关联一起使用; - 一对多:
hasMany
和belongsTo
关联一起使用;-
A.hasMany(B, {sourceKey, foreignKey})
-
A
中的记录一个对多个B
中的,外键在B
- 创建非主键字段的关联:_sourceKey _用于定义
B
中 foreignkey 外键,所对应A
中的实际字段名
-
-
B.belongsTo(A, {targetKey, foreignKey})
-
B
中有记录值属于A
,外键在B
- 创建非主键字段的关联:_targetKey _用于定义
B
中 _foreignKey _外键,所对应A
中的实际字段名
-
- 使用非主键字段的关联 hasMany/belongsTo 都需要写上 sourceKey/targetKey/_foreignKey _并为一个值。要不然 sequelize 还是会按照自己的方式拿而外的外键去查询
-
- 多对多(常用与中间表):两个
belongsToMany(, {through})
调用一起使用;-
{ through: 'xx' }
为关联的中间表(必填项) - 注意:实际关联表并不需要在
include:{}
内出现,它只是起到中间关联的作用
-
- options:
{ sourceKey, targetKey, foreignKey, as }
-
as 别名:当一个模型需要定义两个不同的关联时使用。as 可直接代替 foreignKey ,也可同时使用。
- 定义后在 include 时只能使用别名 as。
- hasOne/hasMany/belongsTo 中都可定义 as,但在 hasMany 中设置 as 返回的是 []
- 一般定义在 belongsTo 中
-
as 别名:当一个模型需要定义两个不同的关联时使用。as 可直接代替 foreignKey ,也可同时使用。
- 延迟加载/预先加载
- 延迟:指等用到了,再获取关联数据
- 预先(include):开始查询事,直接一次获取相关关联数据
- 注:关联查询出的数据是个单独的对象;
-
注:默认是
outer join/ left join
方式 -
实例关联后添加的特殊方法
- 一对一的
instance.createModelB({ 数据对象 })
getModelB/setModelB( id/instance )
- 一对多、多对多
instance.createModelB({ 数据对象 })
hasModelB/addModelB/removeModelB( id/instance )
-
instance.getModelBs/countModelBs/hasModelBs/addModelBs/removeModelBs/setModelBs([ id/instance ])
都是复数形式 - addModelBs/removeModelBs 是
- 一对一的
- 快速使用样例
// 基本配置
new Sequelize({
//...
define: {
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
createdAt: 'createTime',
updatedAt: 'updateTime',
deleteAt: 'deleteTime',
underscored: true, // _命名规则
freezeTableName: true // 强制表名称等于模型名称
}
});
// 数据列配置项
{ type, allowNull: true, defautlValue, unique, primaryKey, autoIncrement, comment, field, validate }
// 定义表 - 1:sequlize.define 内部实际调用的 Model.init
sequlize.define('modelName', { column_field }, { paranoid });
// 定义表 - 2
const { Model } = require('sequlize');
const sequelize = new Sequelize('sqlite::memory');
class User extends Model {
// 可以创建一些自定义 模块数据 函数
getFullName() {
return [this.firstname, this.lastname].join(' ');
}
}
User.init({ column_field }, {
// 这是其他模型参数
sequelize,
tableName: 'xxx_xxx'
})
以2024 corctf的erm为例题
源代码如下:
app.js
const express = require("express");
const hbs = require("hbs");
const app = express();
const db = require("./db.js");
const PORT = process.env.PORT || 5000;
app.set("view engine", "hbs");
// catches async errors and forwards them to error handler
// https://stackoverflow.com/a/51391081
const wrap = fn => (req, res, next) => {
return Promise
.resolve(fn(req, res, next))
.catch(next);
};
app.get("/api/members", wrap(async (req, res) => {
res.json({ members: (await db.Member.findAll({ include: db.Category, where: { kicked: false } })).map(m => m.toJSON()) });
}));
app.get("/api/writeup/:slug", wrap(async (req, res) => {
const writeup = await db.Writeup.findOne({ where: { slug: req.params.slug }, include: db.Member });
if (!writeup) return res.status(404).json({ error: "writeup not found" });
res.json({ writeup: writeup.toJSON() });
}));
app.get("/api/writeups", wrap(async (req, res) => {
res.json({ writeups: (await db.Writeup.findAll(req.query)).map(w => w.toJSON()).sort((a,b) => b.date - a.date) });
}));
app.get("/writeup/:slug", wrap(async (req, res) => {
res.render("writeup");
}));
app.get("/writeups", wrap(async (req, res) => res.render("writeups")));
app.get("/members", wrap(async (req, res) => res.render("members")));
app.get("/", (req, res) => res.render("index"));
app.use((err, req, res, next) => {
console.log(err);
res.status(500).send('An error occurred');
});
app.listen(PORT, () => console.log(`web/erm listening on port ${PORT}`));
db.js
const { Sequelize, DataTypes, Op } = require('sequelize');
const slugify = require('slugify');
const { rword } = require('rword');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'erm.db',
logging: false
});
const Category = sequelize.define('Category', {
name: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
}
});
const Member = sequelize.define('Member', {
username: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
secret: {
type: DataTypes.STRING,
},
kicked: {
type: DataTypes.BOOLEAN,
defaultValue: false,
}
});
const Writeup = sequelize.define('Writeup', {
title: {
type: DataTypes.STRING,
allowNull: false
},
slug: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
date: {
type: DataTypes.DATE,
allowNull: false
},
category: {
type: DataTypes.STRING,
}
});
Category.belongsToMany(Member, { through: 'MemberCategory' });
Member.belongsToMany(Category, { through: 'MemberCategory' });
Member.hasMany(Writeup);
Writeup.belongsTo(Member);
sequelize.sync().then(async () => {
const writeupCount = await Writeup.count();
if (writeupCount !== 0) return;
console.log("seeding db with default data...");
const categories = ["web", "pwn", "rev", "misc", "crypto", "forensics"];
const members = [
{ username: "FizzBuzz101", categories: ["pwn", "rev"] },
{ username: "strellic", categories: ["web", "misc"] },
{ username: "emh", categories: ["crypto"] },
{ username: "jammy", categories: ["misc", "forensics"] },
{ username: "pot", categories: ["crypto"] },
{ username: "plastic", categories: ["misc", "forensics"] },
];
for (const category of categories) {
await Category.create({ name: category });
}
for (const member of members) {
const m = await Member.create({ username: member.username });
for (const category of member.categories) {
const c = await Category.findOne({ where: { name: category } });
await m.addCategory(c);
await c.addMember(m);
}
}
// the forbidden member
// banned for leaking our solve scripts
const goroo = await Member.create({ username: "goroo", secret: process.env.FLAG || "corctf{test_flag}", kicked: true });
const web = await Category.findOne({ where: { name: "web" } });
await goroo.addCategory(web);
await web.addMember(goroo);
for (let i = 0; i < 25; i++) {
const challCategory = categories[Math.floor(Math.random() * categories.length)];
const date = new Date(Math.floor(Math.random() * 4) + 2020, Math.floor(Math.random() * 12), Math.floor(Math.random() * 31) + 1);
// most CTFs feel like they're just named with random words anyway
const ctfName = `${rword.generate(1, { capitalize: 'first', length: '4-6' })}CTF ${date.getFullYear()}`;
// same thing with challenge names
const challName = `${challCategory}/${rword.generate(1)}`;
const title = `${ctfName} - ${challName} Writeup`;
const content = rword.generate(1, { capitalize: 'first'}) + " " + rword.generate(500).join(" ") + ".<br /><br />Thanks for reading!<br /><br />";
const writeup = await Writeup.create({ title, content, date, slug: slugify(title, { lower: true }), category: challCategory });
const authors = members.filter(m => m.categories.includes(challCategory));
const author = await Member.findByPk(authors[Math.floor(Math.random() * authors.length)].username);
await writeup.setMember(author);
await author.addWriteup(writeup);
}
});
module.exports = { Category, Member, Writeup };
观察查询路由的代码发现,只有/api/writeups
的参数是可控的,并且又由上面/api/members
路由可以看到关联表查询使用的是include
知识点:
include是sequelize实现连表查询的一个语法,Sequelize 会根据关联模型的名称自动选择此字段的名称。
详细模型关联的解释
来自于官方文档以及个人总结
预先加载
预先加载是一次查询多个模型的数据的行为,是具有一个或多个 join 的查询
const User = sequelize.define('user', { name: DataTypes.STRING }, { timestamps: false });
const Task = sequelize.define('task', { name: DataTypes.STRING }, { timestamps: false });
const Tool = sequelize.define('tool', {
name: DataTypes.STRING,
size: DataTypes.STRING
}, { timestamps: false });
User.hasMany(Task);
Task.belongsTo(User);
User.hasMany(Tool, { as: 'Instruments' });
预先加载时,我们可以强制查询仅返回具有关联模型的记录,从而有效地将查询从默认的 OUTER JOIN
转为 INNER JOIN
. 这是通过 required: true
参数完成的,如下所示:
User.findAll({ include: { model: Task, required: true }});
注意:
include 自动使用的是left outer join
当在
include
内使用where
参数时,Sequelize 会自动将required
参数设置为true
. 这意味着,将执行INNER JOIN
而不是OUTER JOIN
,仅返回具有至少一个匹配子代的父代模型.
举例:
const city = await City.findByPk(1, {
include: Country,
});
console.log(city.toJSON());
结果:
SELECT
`City`.`id`,
`City`.`cityName`,
`City`.`CountryId`,
`Country`.`id` AS `Country.id`,
`Country`.`countryName` AS `Country.countryName`
FROM
`Cities` AS `City`
LEFT OUTER JOIN `Countries` AS `Country`
ON `City`.`CountryId` = `Country`.`id`
WHERE
`City`.`id` = 1;
假设你要join两个没有重复列的表,这是最常见的情况:
-
inner join A 和 B 获得的是A和B的交集(intersect),即韦恩图(venn diagram) 相交的部分.
-
outer join A和B获得的是A和B的并集(union), 即韦恩图(venn diagram)的所有部分.
在使用left join时,on and和on where条件的区别如下:
1、on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。
2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉,on后的条件用来生成左右表关联的临时表,where后的条件对临时表中的记录进行过滤。
获取单个关联元素
关联的用户加载所有任务
const tasks = await Task.findAll({ include: User });
console.log(JSON.stringify(tasks, null, 2));
打印结果:
[{
"name": "A Task",
"id": 1,
"userId": 1,
"user": {
"name": "张三",
"id": 1
}
}]
总结:
关联的模型被添加到名为 user
的新字段中. Sequelize 会根据关联模型的名称自动选择此字段的名称,在适用的情况下(即关联为 hasMany
或 belongsToMany
)使用该字段的复数形式
获取所有关联的元素
const users = await User.findAll({ include: Task });\
console.log(JSON.stringify(users, null, 2));
打印结果:
[{
"name": "张三",
"id": 1,
"tasks": [{
"name": "A Task",
"id": 1,
"userId": 1
}]
}]
关联是一对多的,因此访问器(结果实例中的tasks
属性)是复数的
获取别名关联
如果关联是别名的(使用as
参数),则在包含模型时必须指定此别名,但可以为对象提供两个选项:model
和 as
const users = await User.findAll({
include: { model: Tool, as: 'Instruments' }
});
console.log(JSON.stringify(users, null, 2));
打印结果:
[{
"name": "张三",
"id": 1,
"Instruments": [{
"name": "hammer",
"id": 1,
"userId": 1
}]
}]
也可以指定的关联别名相匹配的字符串
User.findAll({ include: 'Instruments' }); // 也可以正常使用
User.findAll({ include: { association: 'Instruments' } }); // 也可以正常使用
弄懂了include
我没再查看db.js代码,发现flag存在goroo
这个Member
模型的实例中(模型请看前面的基础知识说明)
那么我们的目标很清晰,就是通过Writeup
的模型关联查询来查询到Member
模型实例中的flag
构造payload如下:
/api/writeups?include[association]=Member&include[on][username]=goroo
对payload的说明:
include[association]=Member
可以正常工作,是因为Sequelize内部使用了模型名称来解析关联。即使没有显式地为关联定义别名,Sequelize仍然能够识别模型之间的关系,并基于这些关系用on来联合多表查询,因为用where的时候会自动转为inner join,而我们需要的是outer join
成功查询到flag: