# 操作数据库
30 秒速记
- 关系型(
MySQL、PostgreSQL)表结构固定、支持JOIN和事务;文档型(MongoDB)存JSON风格文档,结构灵活 Node.js里一般用驱动(mysql2、mongodb)或ORM(Prisma、TypeORM、Mongoose)- 动态值一律走参数化查询,别拼字符串,防
SQL注入 - 服务里用连接池并复用,别每个请求都建连;脚本跑完记得关连接,不然进程不退出
- 查询要有分页上限和索引,多步写入要原子就用事务
在 Node.js 里操作数据库,核心就三件事:连接怎么管、查询怎么写安全、多步操作怎么保证一致。 连接方面,常驻服务用连接池,启动时建好一直复用;一次性脚本跑完要主动关,否则连接挂着进程不会退出。查询方面,用户输入一律走参数化,? 占位交给驱动转义,拼字符串迟早被注入。一致性方面,扣库存加下单这种必须同时成功的操作放进事务,失败就回滚。选型上要关联、要事务的业务用 MySQL,结构多变的日志、内容类数据用 MongoDB 比较顺手。
理解「操作数据库」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Node.js 操作数据库,不管用哪种库,套路都差不多:建连接(最好是连接池)→ 执行查询 → 处理结果和错误 → 归还或关闭连接。
以 mysql2 为例,一个比较规范的写法:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: 'shop',
connectionLimit: 10, // 池里最多 10 个连接
});
// 普通查询:pool.query 会自动取连接、用完归还
async function getUser(id) {
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
return rows[0] ?? null; // 查不到返回 null,不要当成错误
}
// 事务:必须拿一个固定连接
async function createOrder(userId, goodsId) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
await conn.query('UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock > 0', [goodsId]);
await conn.query('INSERT INTO orders (user_id, goods_id) VALUES (?, ?)', [userId, goodsId]);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release(); // 无论成败都归还
}
}
数据库访问要区分查询结果和执行错误:参数用 ? 占位;查不到数据返回 null,连接或语法错误则向上抛出。事务中的语句共用 getConnection() 拿到的连接,release() 放在 finally;一次性脚本结束时再调用 pool.end() 关闭连接池。
面试官追问
追问 1一个导数据的脚本写完库之后不退出,挂在那里,为什么?
连接或连接池还开着,事件循环里有活跃句柄,进程就不会退。在 finally 里 await pool.end() 或 client.close(),成功和失败都要走到。
追问 2批量导入有一条数据格式错了,脚本还是以退出码 0 结束,CI 判成功了,怎么改?
捕获到错误要 process.exitCode = 1,或者直接把异常抛出去让进程非零退出。允许跳过坏数据的话,至少把成功数和失败数打出来,失败数大于 0 就别返回 0。
追问 3下单要扣库存再插订单,第二步失败了,库存已经扣掉了怎么办?
这两步要放在同一个事务里:beginTransaction,两条都成功再 commit,任何一步出错 rollback。注意事务里的语句必须用同一个连接,用 pool.query 每次可能拿到不同连接,事务就失效了。
追问 4线上数据库连接数一直涨,重启服务就好了,像什么问题?
典型的连接泄漏,八成是某个分支拿了连接没还,比如 getConnection 之后提前 return 或者抛异常没走到 release()。把 release() 放进 finally,再看连接池的活跃数指标能不能稳住。
追问 5用了 ORM 是不是就不会有注入问题了?
正常用 API 时基本安全,但 ORM 也都提供原生查询,比如 Prisma 的 $queryRawUnsafe,里面拼字符串一样会被注入。原生查询用带参数的版本,比如 $queryRaw 的模板字符串写法。
# MongoDB 基础
30 秒速记
MongoDB是文档型数据库:库(database)→ 集合(collection)→ 文档(document),对应MySQL的库、表、行- 文档是
BSON(二进制JSON),同一个集合里字段可以不一样,不用提前建表 - 每个文档都有唯一的
_id,默认是ObjectId - 常用操作:
insertOne/find/updateOne/deleteOne,查询条件本身也是对象,如{ age: { $gt: 18 } } - 适合结构多变、读写量大的内容类数据;强关联、强事务的业务还是关系型更稳(
4.0起才支持多文档事务)
MongoDB 是文档型数据库,数据以类似 JSON 的文档存在集合里,不需要像 MySQL 那样先定义表结构。 对照着记最快:库还是库,表叫集合,行叫文档,每个文档自带一个唯一的 _id。查询条件也写成对象,比如 { age: { $gt: 18 } } 就是年龄大于 18。它的好处是字段灵活、嵌套结构天然好存,比如一篇文章连同评论数组放一个文档里;代价是关联查询弱,多文档事务从 4.0 才支持,账务类业务我一般不选它。
MongoDB教程 https://www.yiibai.com/mongodb
理解「MongoDB 基础」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 database 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
MongoDB 的几个基本概念和 MySQL 对照着记:
MySQL |
MongoDB |
|---|---|
| 数据库 | 数据库 |
| 表 | 集合(collection) |
| 行 | 文档(document) |
| 列 | 字段(field) |
| 主键 | _id(默认 ObjectId) |
文档存的是 BSON,比 JSON 多了日期、二进制、ObjectId 等类型。一个文档大致长这样:
{
_id: ObjectId('652f...'),
title: '前端面试题',
tags: ['js', 'css'],
author: { name: '小王', level: 3 },
createdAt: ISODate('2026-10-01T00:00:00Z')
}
在 mongosh 里的常用操作:
db.posts.insertOne({ title: 'hello', views: 0 })
db.posts.find({ views: { $gt: 100 } }).sort({ createdAt: -1 }).limit(10)
db.posts.updateOne({ title: 'hello' }, { $inc: { views: 1 } })
db.posts.deleteOne({ title: 'hello' })
db.posts.createIndex({ createdAt: -1 }) // 按时间排序的列表一定要建索引
查询操作符以 $ 开头:$gt 大于、$in 在列表里、$set 设置字段、$inc 自增。选型上,内容、日志、配置这类结构多变、嵌套多的数据用 MongoDB 很顺手;订单、账务这类强关联、强一致的数据,还是 MySQL、PostgreSQL 更稳妥。
面试官追问
追问 1按一个不存在的 _id 查商品,返回 null,接口该报 500 吗?
不该。查不到是正常结果,返回 404 或空数据就行;连不上库、命令执行报错才是 500。这两种情况混在一起,告警会被用户输错的 id 刷屏。
