之前我们学习了如何通过 Navicat 来连接并操作数据库,并且学习了一些 sql 语句的使用(详见《MySQL 之下载安装及单张表的操作》、《MySQL 之多张表的操作》)。而在使用 node.js 编写的服务器应用中,想要通过 js 代码来实现对数据库的操作,可以使用 mysql2 这个库实现,之所以名字后面有个 2,是因为之前还有个库叫做 mysql,只不过有阵子没维护了,mysql2 兼容了 mysql 并提供了更多的功能,比如 SQL 预处理和对 Promise API 的支持等。
安装
shell
pnpm add mysql2
连接数据库
安装完毕后即可导入使用,同在 Navicat 中一样,想要操作数据库的前提是得连接上数据库。可以通过 createConnection
方法,传入选项对象创建连接:
javascript
const mysql = require('mysql2')
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'juejin'
})
注意,如果端口号不是默认的 3306,选项对象中可以添加 port 配置。另外还有很多配置,可查看 mysql 这个库的使用说明。
查询
单表查询
连接上数据库后,就可以进行查询了,比如我们想查询表 members 的信息:
可以将 sql 语句作为第 1 个参数传给 connection.query()
,并通过第 2 个参数来接收查询结果:
javascript
connection.query('SELECT * FROM `members`', function (err, results, fields) {
if (err) {
console.log(err)
return
}
console.log(results) // 结果集
console.log(fields) // 额外的元数据(如果有的话)
})
查询结果中,results
是一个数组,其子项为表 members 的每一条数据转为的对象 :
fields
也是一个数组,子项是每个字段的一些信息:
多表查询
上面是对单张表的查询,如果查询多张表,执行的 sql 语句如下:
sql
SELECT * FROM members
JOIN singer_and_city sac ON members.id = sac.singer_id
JOIN regions ON sac.city_id = regions.id WHERE members.id = 1;
则在 Navicat 中的结果如下:
通过 mysql2 在 node 程序中执行:
javascript
connection.query(
'SELECT * FROM members JOIN singer_and_city sac ON members.id = sac.singer_id JOIN regions ON sac.city_id = regions.id WHERE members.id = 1;',
function (err, results) {
console.log(results)
}
)
结果如下:
可以看到出问题了,比如有 3 个字段的名称都为 id,最终就展示了表 regions 的 id;有 2 个字段的名称都为 name,最终展示的是 regions 的 name。为了解决这个问题,我们可以通过给字段设置别名的方式解决,但其实关于地点的信息,也就是存储在 regions 表的信息,应该另外包裹成一个 region 对象,放置在每条数据对象中。
将查询结果转换为对象
如下,使用聚合函数 JSON_OBJECT()
,创建一个对象,通过 AS
命名为 region
,将 regions 的 id 信息赋值给 region
的 id
,将 regions 的 name 信息赋值给 region
的 name
:
sql
SELECT
members.id,
members.`name`,
JSON_OBJECT('id', regions.id, 'name', regions.name) AS region
FROM members
JOIN singer_and_city sac ON members.id = sac.singer_id
JOIN regions ON sac.city_id = regions.id WHERE members.id = 1;
在 Navicat 执行上述语句得到的结果如下:
在 node 程序中运行:
javascript
connection.query(
"SELECT members.id, members.`name`, JSON_OBJECT('id', regions.id, 'name', regions.name) AS region FROM members JOIN singer_and_city sac ON members.id = sac.singer_id JOIN regions ON sac.city_id = regions.id WHERE members.id = 1;",
function (err, results, fields) {
console.log(results)
}
)
得到的结果如下:
将查询结果转换为数组
为了方便演示,所以上面的演示中我只查看了 Jay 的相关信息,如果去掉查询条件 WHERE members.id = 1
,得到的结果通过 node 执行将是:
在 Navicat 中查询则是:
可以看到,关于歌手的 id 和 name 信息是存在重复的,我们可以对查询结果进一步地组织,按照歌手 id 进行分组,然后对 region 字段使用聚合函数 JSON_ARRAYAGG()
处理,转成数组:
sql
SELECT
members.id,
members.`name`,
JSON_ARRAYAGG(JSON_OBJECT('id', regions.id, 'name', regions.name)) AS regionList
FROM members
JOIN singer_and_city sac ON members.id = sac.singer_id
JOIN regions ON sac.city_id = regions.id
GROUP BY members.id;
这样在 Navicat 中得到的结果如下:
通过 node 执行得到的就是如下所示的数组:
通过接口的形式在浏览器中查看,可以看到结果是我们所希望的:
预处理语句(Prepared Statements)的使用
mysql2 的一个特性就是可以使用如下所示的,在查询条件部分使用了 ?
来代替具体值的预处理语句:
sql
SELECT * FROM members WHERE `level` > ?;
在 mysql2 中,通过 connection.execute()
来执行预处理语句,在第 2 个参数中传入具体的查询参数,因为可能有多个条件,所以是个数组:
javascript
connection.execute(
'SELECT * FROM members WHERE `level` > ?;',
[1],
function (err, results, fields) {
console.log(results)
}
)
执行结果如下:
使用预处理语句的好处
提高性能
预处理语句会被 MySQL 编译并储存,当给 ?
提供了具体的值时再真正执行查询。所以如果多次执行同样的查询,可以有效地节省准备查询时间,获得更高的性能。
防止 SQL 注入
假设客户端网站有个输入框,只有输入正确的 name(比如 Jay)才能获取对应的信息,服务器端会使用如下语句进行查询:
sql
SELECT * FROM members WHERE `name` = 'Jay';
那么法外狂徒张三就会开动他的小脑筋,尽管他不知道表 members 里到底存储了哪些 name,但他只要想办法让查询时的语句如下:
sql
SELECT * FROM members WHERE `name` = 'Jay' OR 'a'!='b';
也就是在查询条件中多注入了一个判断条件 OR 'a'!='b'
,那么对于 members 中的每一条记录来说,'a'!='b'
都是成立的,他就可以轻松获取 members 里的所有数据了。 但是如果使用了预处理语句:
sql
SELECT * FROM members WHERE `name` = ?;
MySQL 在解析时就知道查询只有一个条件,即 name 是否等于某值,张三想要再使用 OR
添加判断条件的企图就难以得逞。
连接池(connection pools)的使用
同一时间我们的服务器可能被多个客户端访问,如果使用 mysql.createConnection()
与数据库只建立一个连接,就可能导致某个请求发起时,连接正被占用。解决这一问题,我们可以使用 mysql2 提供的连接池:
连接池通过重用以前的连接来帮助减少连接到 MySQL 服务器所花费的时间,当你完成它们时让它们保持打开而不是关闭。
创建方法如下:
javascript
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: '123456',
database: 'juejin',
connectionLimit: 10
})
其中 connectionLimit
用于指定创建连接的最大数量。之后的使用和建立单个连接的情况是一样的,只需要把 connection
替换成 pool
即可。如此,当有新的请求时,就会去连接池里寻找闲置的连接来查询数据库。
判断连接是否成功
我们可以通过 pool.getConnection()
来从池中获取连接:
javascript
pool.getConnection((err, conn) => {
// 获取连接错误
if (err) {
console.log(err)
return
}
// 连接获取未报错
conn.connect(err => {
if (err) {
console.log(err)
return
}
console.log('数据库连接成功')
})
})
使用 conn.connect()
来进行连接,如果没有报错,说明可以成功连接上数据库。
请求结果的 Promise 封装
之前请求到的数据都是直接通过传入的回调函数获取,容易产生回调地狱,mysql2 支持 Promise API,只需要调用 connection
或 pool
的 promise()
函数,然后再继续调用 query()
或 execute()
,返回的就是一个 promise 对象,查询结果可以通过 .then()
的方式接收:
javascript
const promisePool = pool.promise()
promisePool
.execute('SELECT * FROM members WHERE `level` > ?;', [1])
.then(res => console.log(res))
.catch(err => console.log(err))
得到的 res
是一个数组,里面包含了 2 个数组,第一个数组对应之前使用回调形式获取结果时的 results
,第二个数组对应的是 fields
,可以直接在获取结果时进行解构:
javascript
.then(([rows, fields]) => {
console.log(rows)
console.log(fields)
})
结果如下: