第5章 5.2 ORM 工具 Sequelize

⏱️ 学习时间:90 分钟
🎯 目标:能独立用 Sequelize 操作数据库


上一章我们学会了用 mysql2 直接写 SQL 语句操作数据库,那种感觉很「原生」——就像你亲自去菜市场买菜、讨价还价、拎回家自己处理。但你有没有觉得:每次查个数据要写一大串 SELECT,改个字段要小心翼翼怕写错,团队协作时大家的 SQL 风格还各不相同……

这一章我们学一个更优雅的方式:ORM。 简单说,就是让 JavaScript 对象和数据库表自动对应,你操作对象,SQL 自动生成。


🎯 开场 3 分钟:为什么要学这个?

场景再现

假设你在开发一个「图书管理系统」,需要这些操作:

  • 新增一本书
  • 查询某作者的所有书
  • 更新某本书的价格
  • 删除某本过时的书

用原生 SQL 写:

// 新增
connection.query("INSERT INTO books (title, author, price) VALUES (?, ?, ?)", 
['红楼梦', '曹雪芹', 59.9]);

// 查询
connection.query("SELECT * FROM books WHERE author = ?", ['曹雪芹']);

// 更新
connection.query("UPDATE books SET price = ? WHERE id = ?", [49.9, 5]);

// 删除
connection.query("DELETE FROM books WHERE id = ?", [5]);

用 Sequelize 写:

// 新增
await Book.create({ title: '红楼梦', author: '曹雪芹', price: 59.9 });

// 查询
await Book.findAll({ where: { author: '曹雪芹' } });

// 更新
await Book.update({ price: 49.9 }, { where: { id: 5 } });

// 删除
await Book.destroy({ where: { id: 5 } });

对比一下,哪个更像在写业务代码?

本章你能解决这些问题

  1. 不用手写 SQL 也能操作数据库
  2. 代码更易读、易维护
  3. 自动处理数据类型转换
  4. 轻松实现表关联查询

🧱 基础 25 分钟:核心概念

5.2.1 Sequelize 是什么?

生活类比:

想象你去餐厅吃饭。原生 SQL 就像你直接去厨房告诉厨师:「给我来一份宫保鸡丁,用的花生要新鲜的鸡肉切丁……」而 ORM 就像你跟服务员说:「来一份宫保鸡丁。」服务员帮你转达,厨房帮你做好。

Sequelize 就是这个「服务员」,它把你的 JavaScript 对象请求翻译成 SQL,让数据库执行。

5.2.2 安装与初始化

首先安装 Sequelize 和 MySQL2 驱动:

npm install sequelize mysql2

初始化连接:

const { Sequelize } = require('sequelize');

// 创建连接实例
const sequelize = new Sequelize('testdb', 'root', 'password', {
host: 'localhost',
dialect: 'mysql'
});

// 测试连接
async function test() {
try {
await sequelize.authenticate();
console.log('连接成功!');
} catch (error) {
console.error('连接失败:', error);
}
}

test();

运行后输出:

连接成功!

这行在干嘛:
- new Sequelize(...) 创建了一个数据库连接器
- authenticate() 尝试连接,失败会抛异常

5.2.3 定义模型(Model)

模型是 Sequelize 的核心——它把你的 JavaScript 类映射到数据库表。

const { DataTypes } = require('sequelize');

// 定义 Book 模型(对应 books 表)
const Book = sequelize.define('Book', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
author: {
type: DataTypes.STRING,
allowNull: false
},
price: {
type: DataTypes.DECIMAL(10, 2),
defaultValue: 0
},
publishYear: {
type: DataTypes.INTEGER
}
}, {
tableName: 'books',
timestamps: false  // 不自动管理 createdAt/updatedAt
});

console.log('Book 模型创建成功');
console.log(Book === sequelize.models.Book);  // true

输出:

Book 模型创建成功
true

这行在干嘛:
- sequelize.define('Book', {...}) 定义了一个模型类
- 第一个参数 'Book' 会自动对应 books 表(加 s,自动转小写)
- 第二个参数是字段定义
- 第三个参数是表级别配置

配图1 - 配图1

5.2.4 同步数据库

定义好模型后,需要把模型「同步」到数据库创建表:

// 同步所有模型(如果表不存在则创建)
async function initDB() {
await sequelize.sync();
console.log('数据库同步完成!');
}

initDB();

输出:

Executing (default): CREATE TABLE IF NOT EXISTS `books` (...)
数据库同步完成!

这行在干嘛:
- sequelize.sync() 根据模型定义自动创建/更新表
- 默认只创建不存在的表,不改变已有结构

5.2.5 增删改查(CRUD)

新增(Create)

async function createBook() {
// 创建单本书
const book1 = await Book.create({
title: '三国演义',
author: '罗贯中',
price: 45.50,
publishYear: 1522
});

console.log('创建的书:', book1.toJSON());

// 批量创建
const books = await Book.bulkCreate([
{ title: '水浒传', author: '施耐庵', price: 42.00, publishYear: 1589 },
{ title: '西游记', author: '吴承恩', price: 48.00, publishYear: 1592 }
]);

console.log('批量创建了', books.length, '本书');
}

createBook();

输出:

创建的书: { id: 1, title: '三国演义', author: '罗贯中', price: 45.5, publishYear: 1522 }
批量创建了 2 本书

查询(Read)

async function queryBooks() {
// 查询所有
const allBooks = await Book.findAll();
console.log('所有书籍:', allBooks.length, '本');

// 按条件查询
const cheapBooks = await Book.findAll({
where: { price: { [Op.lt]: 50 } }  // price < 50
});
console.log('50元以下的书:', cheapBooks.map(b => b.title));

// 查询单个
const oneBook = await Book.findOne({ where: { author: '罗贯中' } });
console.log('找到的书:', oneBook.title);

// 通过主键查询
const byId = await Book.findByPk(1);
console.log('ID为1的书:', byId.title);
}

queryBooks();

输出:

所有书籍: 3 本
50元以下的书: [ '三国演义', '水浒传', '西游记' ]
找到的书: 三国演义
ID为1的书: 三国演义

这行在干嘛:
- findAll() 查询所有匹配的记录
- where 子句用对象表示,Op.lt 是「小于」操作符
- findOne() 只返回第一条匹配的记录
- findByPk() 通过主键查询

更新(Update)

async function updateBook() {
// 方式1:先查再改
const book = await Book.findOne({ where: { title: '三国演义' } });
book.price = 39.90;
await book.save();
console.log('更新后的价格:', book.price);

// 方式2:直接批量更新
await Book.update(
{ price: 35.00 },  // 要更新的字段
{ where: { author: '罗贯中' } }  // 条件
);
console.log('批量更新完成');
}

updateBook();

输出:

更新后的价格: 39.9
批量更新完成

删除(Delete)

async function deleteBook() {
// 删除单条
const deleted = await Book.destroy({ where: { id: 3 } });
console.log('删除了', deleted, '条记录');

// 清空表(危险!慎用)
// await Book.destroy({ where: {}, truncate: true });
}

deleteBook();

输出:

删除了 1 条记录

配图2 - 配图2

5.2.6 运算符(Op)详解

where 子句里可以用各种运算符:

const { Op } = require('sequelize');

const conditions = {
price: {
[Op.gt]: 40,      // price > 40
[Op.lte]: 100     // price <= 100
},
author: {
[Op.like]: '%贯中%'  // author LIKE '%贯中%'
},
publishYear: {
[Op.in]: [1522, 1589, 1592]  // publishYear IN (1522, 1589, 1592)
}
};

常用 Op 列表:

Op 含义 SQL等价
Op.eq 等于 =
Op.ne 不等于 !=
Op.gt 大于 >
Op.lte 小于等于 <=
Op.like 模糊匹配 LIKE
Op.in 在列表中 IN

🔥 实战 35 分钟:3 个递进项目

📦 项目 1:图书 CRUD 小工具(5 分钟)

目标: 用 Sequelize 完成图书的增删改查

const { Sequelize, DataTypes, Op } = require('sequelize');

// 1. 连接数据库
const sequelize = new Sequelize('testdb', 'root', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: false  // 关闭SQL日志,方便查看
});

// 2. 定义模型
const Book = sequelize.define('Book', {
title: { type: DataTypes.STRING, allowNull: false },
author: { type: DataTypes.STRING, allowNull: false },
price: { type: DataTypes.DECIMAL(10, 2), defaultValue: 0 }
}, { timestamps: false });

// 3. 主程序
async function main() {
await sequelize.sync();

// 清空旧数据
await Book.destroy({ where: {} });

// 添加测试数据
await Book.bulkCreate([
{ title: '活着', author: '余华', price: 28 },
{ title: '平凡的世界', author: '路遥', price: 65 },
{ title: '三体', author: '刘慈欣', price: 72 }
]);

// 查询并打印
const books = await Book.findAll();
console.log('当前图书:');
books.forEach(b => {
console.log(`  ${b.id}.《${b.title}》- ${b.author},¥${b.price}`);
});

// 更新价格
await Book.update({ price: 25 }, { where: { title: '活着' } });
const updated = await Book.findOne({ where: { title: '活着' } });
console.log(`\n更新后的《活着》价格:¥${updated.price}`);

// 删除
await Book.destroy({ where: { title: '三体' } });
console.log('已删除《三体》');

await sequelize.close();
console.log('\n✅ 操作完成!');
}

main().catch(console.error);

预期输出:

当前图书:
1.《活着》- 余华,¥28
2.《平凡的世界》- 路遥,¥65
3.《三体》- 刘慈欣,¥72

更新后的《活着》价格:¥25
已删除《三体》

✅ 操作完成!

一句话解释: 这个项目展示了 Sequelize 的基本 CRUD 操作顺序:连接 → 定义模型 → sync → 增删改查。


📦 项目 2:从 JSON 文件批量导入图书(15 分钟)

目标: 读取 JSON 文件,用 Sequelize 批量写入数据库

准备一个 books.json 文件:

[
{ "title": "Python编程", "author": "张三", "price": 89 },
{ "title": "JavaScript高级", "author": "李四", "price": 79 },
{ "title": "Node.js实战", "author": "王五", "price": 69 },
{ "title": "数据结构", "author": "赵六", "price": 55 },
{ "title": "算法图解", "author": "孙七", "price": 45 }
]

批量导入脚本:

const { Sequelize, DataTypes } = require('sequelize');
const fs = require('fs');

const sequelize = new Sequelize('testdb', 'root', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: false
});

const Book = sequelize.define('Book', {
title: { type: DataTypes.STRING, allowNull: false },
author: { type: DataTypes.STRING, allowNull: false },
price: { type: DataTypes.DECIMAL(10, 2), defaultValue: 0 }
}, { timestamps: false });

async function importBooks() {
// 1. 读取JSON文件
const jsonData = fs.readFileSync('books.json', 'utf-8');
const booksToImport = JSON.parse(jsonData);

console.log(`读取到 ${booksToImport.length} 本书准备导入...\n`);

// 2. 同步数据库
await sequelize.sync({ force: true });  // force: true 会删除旧表重建
console.log('数据库已重建');

// 3. 批量导入
const imported = await Book.bulkCreate(booksToImport);
console.log(`成功导入 ${imported.length} 本书!\n`);

// 4. 验证结果:按价格排序查看
const allBooks = await Book.findAll({
order: [['price', 'ASC']]  // 按价格升序
});

console.log('导入后的图书(按价格排序):');
allBooks.forEach(b => {
const status = b.price > 70 ? '🔥畅销' : '📚普通';
console.log(`  ${status} 《${b.title}》- ${b.author},¥${b.price}`);
});

await sequelize.close();
}

importBooks().catch(console.error);

预期输出:

读取到 5 本书准备导入...

Executing (default): DROP TABLE IF EXISTS `books`;
Executing (default): CREATE TABLE `books` ...
数据库已重建
成功导入 5 本书!

导入后的图书(按价格排序):
📚普通 《算法图解》- 孙七,¥45
📚普通 《数据结构》- 赵六,¥55
📚普通 《Node.js实战》- 王五,¥69
🔥畅销 《JavaScript高级》- 李四,¥79
🔥畅销 《Python编程》- 张三,¥89

一句话解释: bulkCreate() 一次插入多条记录,配合 order 实现排序查询。


📦 项目 3:图书查询小工具(15 分钟)

目标: 做一个带交互的图书查询工具,支持多条件筛选

const { Sequelize, DataTypes, Op } = require('sequelize');

const sequelize = new Sequelize('testdb', 'root', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: false
});

const Book = sequelize.define('Book', {
title: { type: DataTypes.STRING, allowNull: false },
author: { type: DataTypes.STRING, allowNull: false },
price: { type: DataTypes.DECIMAL(10, 2), defaultValue: 0 },
category: { type: DataTypes.STRING, defaultValue: '文学' }
}, { timestamps: false });

// 模拟查询函数(实际项目中会接收用户输入)
function queryBooks(options = {}) {
const { keyword, minPrice, maxPrice, category, sortBy = 'title' } = options;

const where = {};

// 关键词搜索(书名或作者)
if (keyword) {
where[Op.or] = [
  { title: { [Op.like]: `%${keyword}%` } },
  { author: { [Op.like]: `%${keyword}%` } }
];
}

// 价格区间
if (minPrice !== undefined) {
where.price = { ...where.price, [Op.gte]: minPrice };
}
if (maxPrice !== undefined) {
where.price = { ...where.price, [Op.lte]: maxPrice };
}

// 分类
if (category) {
where.category = category;
}

return Book.findAll({
where,
order: [[sortBy, 'ASC']]
});
}

// 测试各种查询场景
async function main() {
await sequelize.sync();

// 初始化测试数据
await Book.destroy({ where: {} });
await Book.bulkCreate([
{ title: 'JavaScript权威指南', author: '张三', price: 99, category: '技术' },
{ title: 'Python入门', author: '李四', price: 59, category: '技术' },
{ title: '活着', author: '余华', price: 28, category: '文学' },
{ title: '三体', author: '刘慈欣', price: 68, category: '科幻' },
{ title: '数据结构', author: '王五', price: 45, category: '技术' }
]);
console.log('测试数据已准备\n');

// 场景1:搜索关键词
console.log('=== 搜索"数据"相关 ===');
const r1 = await queryBooks({ keyword: '数据' });
r1.forEach(b => console.log(`  《${b.title}》`));

// 场景2:价格区间
console.log('\n=== 50-70元的书 ===');
const r2 = await queryBooks({ minPrice: 50, maxPrice: 70 });
r2.forEach(b => console.log(`  《${b.title}》¥${b.price}`));

// 场景3:分类筛选
console.log('\n=== 技术类书籍(按价格升序)===');
const r3 = await queryBooks({ category: '技术', sortBy: 'price' });
r3.forEach(b => console.log(`  《${b.title}》¥${b.price}`));

// 场景4:组合查询
console.log('\n=== 技术类且价格在60元以上的书 ===');
const r4 = await queryBooks({ category: '技术', minPrice: 60 });
r4.forEach(b => console.log(`  《${b.title}》¥${b.price}`));

await sequelize.close();
}

main().catch(console.error);

预期输出:

测试数据已准备

=== 搜索"数据"相关 ===
《JavaScript权威指南》
《数据结构》

=== 50-70元的书 ===
《Python入门》¥59
《三体》¥68

=== 技术类书籍(按价格升序)===
《数据结构》¥45
《Python入门》¥59
《JavaScript权威指南》¥99

=== 技术类且价格在60元以上的书 ===
《JavaScript权威指南》¥99

一句话解释: 这个工具把查询条件封装成函数,通过组合不同的筛选参数实现灵活查询。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:异步陷阱——忘了 await

// ❌ 错误:没有 await,数据还没返回就打印
const books = Book.findAll();
console.log(books);  // 打印的是 Promise 对象,不是数据

// ✅ 正确:加上 await
const books = await Book.findAll();
console.log(books);  // 打印的是真实数据

坑 2:模型定义时机——在连接前就定义

// ❌ 错误:在连接创建前就定义模型
const Book = sequelize.define('Book', { ... });  // sequelize 还是 undefined!

const sequelize = new Sequelize(...);

// ✅ 正确:先连接,再定义模型
const sequelize = new Sequelize(...);
const Book = sequelize.define('Book', { ... });

坑 3:Op.or 的写法

// ❌ 错误:当 Op.or 是唯一条件时
where: { [Op.or]: [{ author: '余华' }, { author: '刘慈欣' }] }

// ✅ 正确:外层包装
where: {
[Op.or]: [
{ author: '余华' },
{ author: '刘慈欣' }
]
}

坑 4:bulkCreate 默认忽略空值

// ❌ 错误:字段值为 null 或 undefined 时不会写入
await Book.bulkCreate([
{ title: '书1', author: '作者1', price: null }  // null 会被忽略
]);

// ✅ 正确:使用 defaultValue 或显式传值
// 在模型中设置 defaultValue
price: { type: DataTypes.DECIMAL(10, 2), defaultValue: 0 }

坑 5:findByPk vs findOne

// findByPk(1) 查找主键为1的记录,找不到返回 null
// findOne({ where: { id: 1 } }) 效果相同,但更明确
// findAll({ where: { id: 1 } }) 返回数组,即使只有一条

性能小贴士:只查需要的字段

// ❌ 低效:查询所有字段
const books = await Book.findAll();

// ✅ 高效:只查需要的字段
const books = await Book.findAll({
attributes: ['id', 'title', 'author']  // 不查 price 和 category
});

调试技巧:开启 SQL 日志

const sequelize = new Sequelize('testdb', 'root', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: console.log  // 打印每条SQL,方便调试
});

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):单本新增
- 输入:添加一本书《围城》,作者钱钟书,价格 38 元
- 预期输出:控制台打印创建的书对象
- 提示:使用 Book.create() 方法

练习 2(2 分钟):条件查询升级
- 输入:在项目 1 基础上,查询价格小于 50 元的书
- 预期输出:只显示符合条件的书
- 提示:使用 where: { price: { [Op.lt]: 50 } }

练习 3(2 分钟):处理新数据
- 输入:创建一个新的 books2.json,包含 3 本书,用项目 2 的方式导入
- 预期输出:数据库中新增 3 条记录
- 提示:直接复用项目 2 的代码,只改文件路径

练习 4(2 分钟):合并查询
- 输入:用项目 3 的查询函数,实现「搜索含有关键词 + 价格在某个区间 + 指定分类」
- 预期输出:符合所有条件的书
- 提示:queryBooks({ keyword: 'x', minPrice: 20, maxPrice: 80, category: '文学' })

练习 5(2 分钟):报错分析
- 输入:运行以下代码,分析为什么报错

async function errorDemo() {
const Book = sequelize.define('Book', {
title: DataTypes.STRING  // 忘记引号!
});
await Book.create({ title: '测试书' });
}
  • 预期输出:报错并说明原因
  • 提示:检查 DataTypes 的使用方式

作业题(30 分钟 - 2 小时)

作业:做一个「个人图书收藏管理器」

  • 需求描述: 用 Sequelize 开发一个图书管理小工具,可以添加、查看、筛选、删除自己的藏书

  • 功能点:
    1. 从 JSON 文件导入初始图书数据
    2. 显示所有图书(支持按书名、作者、价格排序)
    3. 多条件筛选(价格区间、分类)
    4. 添加新书、删除图书

  • 加分项:
    1. 支持修改图书信息
    2. 统计藏书数量和总价值

  • 验收标准:

  • 能跑起来,不报错
  • 所有功能能输出预期结果
  • 代码有适当注释

  • 提交方式: 评论区贴代码或 GitHub 链接


📚 总结 + 资源

本章 3 个核心点

  1. Sequelize 是对象和数据库表的翻译官 —— 你操作对象,SQL 自动生成
  2. 模型定义是核心 —— 先定义好模型,才能进行 CRUD 操作
  3. 异步操作记得 await —— Sequelize 所有数据库操作都返回 Promise

延伸学习资源

  1. Sequelize 官方文档 — 最权威的参考资料
  2. 《Node.js 实战》 — 实战型 Node.js 书籍
  3. B站:Node.js + Sequelize 实战教程 — 视频学习更直观

互动钩子: 你在项目中用原生 SQL 多还是 ORM 多?遇到过什么坑?评论区聊聊,帮你解答!


📌 下章预告: 学会了用 MySQL 存数据,但你是否想过——如果数据没有固定结构怎么办?比如用户的个性化标签、聊天记录这种「说不清有多少字段」的数据?下一章我们聊聊另一种数据库:MongoDB,它和关系型数据库完全不同思维。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。