当在使用sequelize时,我们发现中文文档中很多进阶的用法只是简单列出来,还有大部分的使用方法其实需要数据库知识的支撑才能实现 下列我们就列出一些隐藏的用法解决开发中会遇到的问题,尽量让想要的数据一次就能输出出来,尽量不出现循环查询以及多次查询实现目标的问题。
表结构以及模拟数据的生成
用于本篇文章重点是在于如何去查 使用大量的sequelize提供的方法 所以我们本次不做关联的操作
javascript
const Record = sequelize.define(
"record",
{
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
name: { type: STRING, allowNull: false, comment: "学生名称" },
subject: { type: STRING, allowNull: false, comment: "科目" },
seconds: { type: INTEGER, allowNull: false, seconds: "用时" },
score: { type: FLOAT, allowNull: false, comment: "分数" },
class_name: { type: STRING, allowNull: false, comment: "班级名称" },
school_name: { type: STRING, allowNull: false, comment: "学校名称" },
createdAt: {
type: DATE,
defaultValue: NOW,
comment: "创建时间",
get() {
return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss");
},
},
},
{ createdAt: true }
);
模拟数据的生成 共2个学校 每班30个学生 每个学生每个科目三次成绩
javascript
const SCHOOL_NAME = ["深职院", "深信息"];
const CLASS_NAME = ["1班", "2班", "3班"];
const SUBJECTS = ["语文", "数学", "英语"];
async function mockData() {
let count = await Record.count();
if (count) return;
let bulk = [];
const randomFrom = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/** 模拟数据 2*3*30*3*3=1620条 */
for (let school_name of SCHOOL_NAME) {
for (let class_name of CLASS_NAME) {
for (let j = 0; j < 30; j++) {
let name = randomName(randomFrom(0, 1));
for (let subject of SUBJECTS) {
for (let i = 0; i < 3; i++) {
bulk.push({
name,
subject,
seconds: randomFrom(10, 100),
score: randomFrom(0, 100),
class_name,
school_name,
});
}
}
}
}
}
await Record.bulkCreate(bulk);
}
例子以及思路
常用的FN函数使用
javascript
// fn("SUM") fn("COUNT") fn("AVG") fn("MIN") fn("MAX") fn("concat") fn("DATE_FORMAT")
// 1.fn用法一:统计班级每个学生的考试次数 通过fn函数操作
let student_test_count = await Record.findAll({
attributes: ["name", "class_name", "school_name", [seq.fn("COUNT", seq.col("id")), "count"]],
group: ["name", "class_name", "school_name"],
raw: true,
});
// [{ name: '黄美巧', class_name: '1班', school_name: '深职院', count: 9 },...]
// console.log(student_test_count);
// 2.fn用法二:统计两个学校考核用时 通过fn函数操作
let school_times = await Record.findAll({
attributes: ["school_name", [seq.fn("SUM", seq.col("seconds")), "seconds"]],
group: ["school_name"],
raw: true,
});
// [
// { school_name: "深职院", seconds: "45656" },
// { school_name: "深信息", seconds: "44474" },
// ];
// console.log(school_times);
// 3.我们知道其实findOne联动fn中的COUNT SUM AVG等函数可以直接统计某个字段
// 但是fn的findOne和findAll有区别的 如果存在group的情况 要用findAll 不然数据会缺失
let school_time_one = await Record.findOne({
attributes: ["school_name", [seq.fn("SUM", seq.col("seconds")), "seconds"]],
group: ["school_name"],
raw: true,
});
// { school_name: '深职院', seconds: '45656' }
// console.log(school_time_one);
// 4.fn搭配seq.where 获取深职院每个学生每个科目的考试次数 一次性统计出来
let student_subject_count = await Record.findAll({
where: {
school_name: "深职院",
},
attributes: ["name", ...SUBJECTS.map((subject) => [seq.fn("SUM", seq.where(seq.col("subject"), subject)), subject])],
group: ["name"],
raw: true,
});
//[ { name: '黄美巧', '语文': '3', '数学': '3', '英语': '3' },...]
// console.log(student_subject_count);
// 5.获取某个班级科目的分数 并且通过concat 函数拼接字符串 输出
let class_score = await Record.findAll({
where: {
school_name: "深职院",
class_name: "1班",
},
attributes: [
[seq.fn("CONCAT", seq.col("school_name"), "-", seq.col("class_name")), "full_name"],
[seq.fn("MAX", seq.col("score")), "max_score"],
[seq.fn("AVG", seq.col("score")), "avg_score"],
[seq.fn("MIN", seq.col("score")), "min_score"],
[seq.fn("COUNT", seq.col("score")), "count"],
"subject",
],
group: ["school_name", "class_name", "subject"],
raw: true,
});
// [
// {
// full_name: "深职院-1班",
// max_score: 100,
// avg_score: 54.25555555555555,
// min_score: 0,
// count: 90,
// subject: "语文",
// },...
// ];
// console.log(class_score);
// 11. 去重操作 获取所有人的名称 但是不重复 可以使用distinct函数
// 一般distinct 必须要放在首位 除非是使用函数的情况下就不需要放在首位了
// distinct 好文 https://blog.csdn.net/baomingshu/article/details/137016186
let student_distinct = await Record.findAll({
raw: true,
attributes: [[seq.fn("DISTINCT", seq.col("name")), "name"]],
});
// [ { name: '黄美巧' },...]
// console.log(student_distinct);
literal与where的使用
javascript
// 1.随机获取数据与where的进阶用法 查找某年某月某天的数据 不用between的操作符 可以用DATE_FORMAT函数来操作
// 已知生成了1620条模拟数据 创建都是2025-04-15的 所以我们要更改一些时间 更改200条 来测试
// 为了保证数据随机的更改 我们通过先find操作条数 然后再update操作 因为update不能直接限制修改多少条
let target = await Record.findAll({
order: seq.literal("rand()"),
limit: 200,
attributes: ["id"],
raw: true,
});
// console.log(target);
await Record.update({ createdAt: "2025-04-14 18:00:00" }, { where: { id: target.map((item) => item.id) }, transaction });
const time_data = await Record.count({
where: {
[Op.and]: [seq.where(seq.fn("DATE_FORMAT", seq.col("createdAt"), "%Y-%m-%d"), "2025-04-14")],
},
transaction,
});
200
console.log(time_data);
// 2.获取年份数据
let time_compare = await Record.count({
where: seq.where(seq.fn("YEAR", sequelize.col("createdAt")), 2025),
transaction,
});
// 1620
console.log(time_compare);
// 3.子查询 查出学生的每个科目考试最高分数 这部分就要自己手写了
// 关键点 seq.literal中的select必须要左右括号 并且 from 的表名要加 as xx 不然没法和原表比较
// 最好要加上索引 比如我们比较的name部分
// explain 生成的语句 发现使用了子查询 但是没有使用索引 type全为all 走了全表
let student_subject_max = await Record.findOne({
attributes: [
"name",
...SUBJECTS.map((subject) => [
seq.literal(`(SELECT MAX(score) FROM record as r WHERE r.name = record.name AND subject='${subject}')`),
subject,
]),
],
raw: true,
});
// { name: '黄美巧', '语文': 100, '数学': 16, '英语': 96 }
// console.log(student_subject_max);
// 4.通过case when 来判断返回数据
// 比如我们想判断成绩是否及格 及格的话返回 及格 不及格的话返回 不及格
// 可以使用case when 来实现 case when允许多个条件 这里我就写一个条件
let student_score_level = await Record.findAll({
raw: true,
attributes: ["*", [seq.literal(`CASE WHEN score >= 60 THEN '及格' ELSE '不及格' END`), "level"]],
});
// {
// id: 1,
// name: '黄美巧',
// subject: '语文',
// seconds: 81,
// score: 10,
// class_name: '1班',
// school_name: '深职院',
// createdAt: 2025-04-15T02:57:31.000Z,
// updatedAt: 2025-04-15T02:57:31.000Z,
// level: '不及格'
// }
console.log(student_score_level[0]);
原始查询搭配窗口函数 实现先排序后分组再排序
javascript
// 原始查询 针对无法使用sequelize的函数或者定制化的情况
// DQL 的编写顺序 SELECT * FROM Table as t GROUP BY t.`name` ORDER BY t.`score` DESC LIMIT 索引,条数;
// 比如说这种情况 要先排序后分组 的情况 但是我们看到上面的语句 是先分组后排序的 所以我们需要使用原始查询来实现
// 我想拿到每个学生 最新提交的科目成绩
// 普通查询
let student_latest_score = await Record.findAll({
group: ["name"],
order: [["id", "asc"]],
raw: true,
attributes: ["id", "name", "score"],
});
// [{ id: 1, name: '黄美巧', score: 10 },...]
// 会发现 拿到的id是最早的 先group再排序了 所以我们需要使用原始查询来实现
// console.log(student_latest_score);
// 最新的数据应该是{id:9,name:'黄美巧',score:60}
// mysql8.0以上才支持:利用窗口函数先排序再分组 拿到最新的成绩 就可以拿到每个人的最新成绩了
// 窗口函数解释好文 https://blog.csdn.net/frostlulu/article/details/130729113
let query = await sequelize.query(
`SELECT
*
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY NAME
ORDER BY
id DESC
) AS rn
FROM
record
) AS r
GROUP BY
r. NAME order by r.id asc`
);
// 输出每个人最新的成绩 并且按照id升序排序
// [{id:9,name:'黄美巧',score:60},...]
// console.log(query);
还在不断摸索更多的使用方式途中,了解更多的mysql知识才会有更多的用法,sequelize留给的开放度还是挺高的,针对最后一种的这种原始语句的操作,对官方提过了issue 还有没别的解决方案,他们回答后续会推出和TypeORM一样的QueryBuilder的语句构建方式。