# async/await封装使用mysql
30 秒速记
- 目标:把回调风格的
connection.query包成返回Promise的query(sql, values),业务里直接await - 三种做法:手写
new Promise/util.promisify(pool.query).bind(pool)/ 直接用mysql2/promise await出错会抛异常,要用try/catch接,或者交给Koa的错误中间件统一处理- 互不依赖的查询用
Promise.all并发,别一个个await排队 await只是写法像同步,底层还是异步,不会阻塞事件循环
async/await 封装就是把 mysql 的回调接口包成 Promise,这样查数据库能写成 const rows = await query(sql),读起来像同步代码。 好处不只是好看:多步查询不用嵌套回调,错误可以用 try/catch 一处接住,在 Koa 里还能直接被错误中间件捕获。现在其实不太需要自己手写封装了,mysql2/promise 原生就支持。要留意的是,几个互不相关的查询别依次 await,用 Promise.all 一起发会快很多。
理解「async/await封装使用mysql」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
把回调包成 Promise 有三种写法,效果一样,复杂度递减:
// 写法一:手写 Promise(理解原理用)
const query = (sql, values) =>
new Promise((resolve, reject) => {
pool.query(sql, values, (err, rows) => (err ? reject(err) : resolve(rows)))
})
// 写法二:util.promisify,一行搞定
const util = require('util')
const query2 = util.promisify(pool.query).bind(pool) // 不 bind 会丢 this
// 写法三:直接换 mysql2/promise,不用封装
const mysql = require('mysql2/promise')
const pool3 = mysql.createPool({ /* ... */ })
const [rows] = await pool3.query('SELECT 1 AS ok') // rows: [ { ok: 1 } ]
用起来都是这样:
async function getUserPage(id) {
// 两个查询互不依赖,并发执行
const [users, posts] = await Promise.all([
query('SELECT * FROM user WHERE id = ?', [id]),
query('SELECT * FROM post WHERE user_id = ? LIMIT 10', [id]),
])
return { user: users[0], posts }
}
async/await 让代码「看起来」像同步,但每个 await 都是一次异步等待。理解这一点,就不会担心它阻塞服务,也会主动把可以并行的查询放进 Promise.all。
面试官追问
追问 1await 一个查询的时候,Node 是不是就卡住了,处理不了别的请求?
不会。await 只是把当前函数后面的代码挂起,事件循环照常处理其他请求,等查询结果回来再继续。真正会卡住的是 fs.readFileSync 这类同步 API。
追问 2一个页面要查用户信息和文章列表,两个 await 写下来慢了一倍,怎么改?
两个查询互不依赖,就用 const [user, posts] = await Promise.all([getUser(id), getPosts(id)]) 并发发出去,总耗时取决于慢的那个。
追问 3封装后的 query 出错了,在 Koa 里怎么统一处理?
在最外层写一个错误中间件:try { await next() } catch (err) { ctx.status = 500; ctx.body = { message: '服务异常' } },再按 err.code 细分。业务里不用每处都 try/catch。
追问 4手写封装、util.promisify、mysql2/promise 怎么选?
新项目直接 mysql2/promise,零封装。老项目已经用着 mysql,util.promisify(pool.query).bind(pool) 一行搞定,注意要 bind,不然 this 丢了会报错。
追问 5for 循环里 await 插入 1000 条数据很慢,怎么优化?
逐条 await 就是 1000 次往返。改成一条批量插入:INSERT INTO user (email) VALUES ?,参数传二维数组 [[a], [b]],一次搞定。
# async/await封装使用mysql
# 前言
30 秒速记
- 痛点:
mysql的操作全是回调,多步查询会嵌套成回调地狱,错误要每层手动传 - 思路:包一层
Promise,配合async/await写成同步的样子 await只暂停当前async函数,不阻塞事件循环,其他请求照常处理Koa 2中间件本身就是async函数,数据库层返回Promise才能自然地await
这一章要解决的问题就是回调写法太难读,用 Promise 包一下,再配上 async/await,数据库操作就能按顺序往下写。 比如「查用户 → 查他的订单 → 查订单商品」,回调写法要嵌三层,每层都要判 err;改成 await 之后就是三行,出错统一被 try/catch 接住。在 Koa 2 里这一点尤其重要,中间件都是 async 函数,数据库层返回 Promise 才能和它配合起来。
由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行,现在有了async/await,就可以用同步的写法去操作数据库
理解「前言」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1说「用同步的写法操作数据库」,那数据库操作变成同步的了吗?
没有,只是写法像同步。await 后面的代码会被放到 Promise 完成之后执行,等待期间线程照样去处理别的事。
追问 2回调写法嵌三层,哪里最容易出 bug?
错误处理。每层都要写 if (err) return cb(err),漏一层错误就被吞掉了,或者回调被调用两次。Promise 链里任何一步 reject 都会直接跳到 catch。
追问 3Koa 2 里如果数据库层还是回调风格,会出什么问题?
中间件 await next() 之后就返回了,回调还没执行,ctx.body 没赋值,客户端拿到 404。数据库层必须返回 Promise,中间件 await 它,Koa 才知道要等。
追问 4async 函数里忘了写 await,会怎样?
拿到的是一个 Promise 对象而不是查询结果,rows.length 是 undefined。而且这个 Promise 如果 reject,没人接住就是一个未处理的 rejection。
# Promise封装mysql模块
30 秒速记
- 封装:
new Promise里pool.getConnection→connection.query→ 成功resolve(rows)、失败reject(err)→connection.release() release要放在resolve/reject之外、无论成败都执行;getConnection失败时没有连接可还- 简单查询其实可以直接用
pool.query,它内部就是借连接、查询、还连接 - 第二个参数
values用来填?占位符,防注入全靠它 - 调用方
await query(sql, values),出错用try/catch接
Promise 封装就是在回调里调 resolve 和 reject,把「借连接、执行 SQL、还连接」包成一个能 await 的函数。 写这种封装最重要的是连接一定要还:不管查询成功还是失败,connection.release() 都要执行,不然连接池很快被借光,所有请求都卡在等连接上。另外这个函数其实就是 pool.query 加一层 Promise,所以更简洁的写法是 new Promise 里直接调 pool.query,或者 util.promisify。
