安装mysql
npm i --save mysql
基本用法
创建管理一个连接
var mysql = require('mysql');
var connection = mysql.createConnection({
host: 'aliyuncs.com',
user: 'admin',
password: 'pwd'
});
connection.connect(function(err) {
if (err) {
console.error('error connecting: ' + err.stack);
return;
}
connection.query('SELECT * FROM ...', ..., function (error, results, fields) {
if (error) throw error;
//do something
connection.end();
});
});
转义
为了避免SQL注入攻击,在SQL查询中使用任何用户提供的数据之前,始终应该对其进行转义。
手动转义
mysql提供了mysql.escape(), connection.escape() 和 pool.escape()
var userId = 'user input values';
var sql = 'SELECT * FROM users WHERE userId = ' + connection.escape(userId);
connection.query(sql, function (error, results, fields) {
if (error) throw error;
//do something
});
自动转义
使用?字符作为要转义的值的占位符
connection.query('UPDATE users SET name = ?, address = ? WHERE userId = ?', [inputName, inputAddress, userId], function (error, results, fields) {
if (error) throw error;
//do something
});
连接和连接池
当我们一个api接口需要连接到数据库进行增删改查的时候,如果每次去创建连接然后去关闭,会大大的消耗数据库服务的性能。
连接池
连接是由池延迟创建的,基本流程是pool.getConnection() -> connection.query() -> connection.release(),有助于共享连接状态以用于后续查询。
var mysql = require('mysql');
var pool = mysql.createPool({
connectionLimit: 10,
host: 'aliyuncs.com',
user: 'admin',
password: 'pwd',
database: 'db',
timezone: 'Asia/Shanghai'
});
pool.getConnection(function(err, connection) {
if (err) throw err;
connection.query('SELECT * FROM ...', ..., function (error, results, fields) {
connection.release(); //释放连接,以便其他请求使用
if (error) throw error;
//do something
});
});
连接池组
其实就是多个连接池的组合
创建一个连接池组
var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();
poolCluster.add('pool1', {
connectionLimit: 10,
host: 'aliyuncs.com',
user: 'admin',
password: 'pwd',
database: 'db',
timezone: 'Asia/Shanghai',
database: 'db1'
});
poolCluster.add('pool2', {
connectionLimit: 10,
host: 'aliyuncs.com',
user: 'admin',
password: 'pwd',
database: 'db',
timezone: 'Asia/Shanghai',
database: 'db2'
});
使用连接池
poolCluster.getConnection('pool1', function (err, connection) { //使用pool1池
if (err) throw err;
connection.query('SELECT * FROM ...', ..., function (error, results, fields) {
connection.release();
if (error) throw error;
//do something
});
});
特别注意:因为nodejs事件循环机制,连接池组会一直保持活跃,直到mysql服务器关闭,所以当nodejs进程退出时要手动关闭mysql链接池组
process.on('exit', (code) => {
poolCluster.end(function (err) {});
});
事务组
多个连接请求“合并”成一个流程,来保证程序的正确运行
为什么需要事务组?
场景:当前有一个抽奖活动,用户点击前端抽奖按钮后调用api接口
后端逻辑:去道具表扣掉用户道具 -> 去奖品表修改奖品为已发放给用户发放奖品 -> 再中奖记录表中生成一条中奖记录
试想一下,上面的三个步骤,任何一个步骤出问题都将导致重大bug
比如:道具扣掉了,但是礼物没发放,又比如礼物发放了没有记录
如何创建事务组
......
connection.beginTransaction(function(err) {
if (err) throw err;
connection.query('UPDATE props SET ...?', ..., function (error, results, fields) {
if (error) {
return connection.rollback(function() {
throw error;
});
}
connection.query('UPDATE SET ...', ..., function (error, results, fields) {
if (error) {
return connection.rollback(function() {
throw error;
});
}
connection.query('INSERT INTO record ...', ..., function (error, results, fields) {
if (error) {
return connection.rollback(function() {
throw error;
});
}
connection.commit(function(err) {
if (err) {
return connection.rollback(function() {
throw err;
});
}
//do something
});
});
});
});
});
以上代码,任何一个连接操作出现问题,执行回滚,整个流程都不会执行,只有当三个连接操作都成功,执行提交,改动才生效。
beginTransaction(), commit() 和 rollback() 分别对应了 START TRANSACTION, COMMIT, 和 ROLLBACK
事务的隔离级别
业务当中的事务并发是一个不可控点,这个时候会引发一系列的bug
事务并发带来的问题
- 脏读:事务1读取了事务2中未提交的数据
- 虚读(不可重复读):事务1多次读取一条数据得到了不同的结果(事务2也修改了当前数据)
- 幻读:事务1读取到了事务2插入或者删除的数据
MySql中的事务的隔离级别
隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(RU) | 允许读取尚未提交的数据变更 | 是 | 是 | 是 |
读已提交(RC) | 允许读取并发事务已经提交的数据 | 否 | 是 | 是 |
可重复读(RR) | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改 | 否 | 否 | 否 |
串行化 | 最高的隔离级别,完全服从ACID的隔离级别 | 否 | 否 | 否 |
事务的隔离级别和并发度成反比的
需要注意的是与其他数据库不同的是,InnoDB 存储引擎在 可重复读(RR)事务隔离级别下使用的是Next-Key Lock 锁算法,实现了串行化隔离级别的功能,同时没有严重影响并发。
常见的防并发语句
防重复插入
INSERT INTO SELECT
INSERT INTO A(name1, name2, status) SELECT 1, 2, 0 WHERE NOT EXISTS (SELECT * FROM A WHERE create_time > '2020-01-01 00:00:00')
防重复修改
UPDATE WHERE带上初始条件
UPDATE A SET status = 1 WHERE name1 = 1 AND status = 0
利用Promise封装一个事务组方法
......
var transactionQuery = (database, callback) => {
return new Promise((resolve, reject) => {
poolCluster.getConnection(database, function (err, connection) {
if (err) {
reject({
msg: "系统内部错误,请稍后再试"
});
return;
}
connection.beginTransaction(function (err) {
if (err) {
reject({
msg: "系统内部错误,请稍后再试"
});
return;
}
callback.apply(null, [connection, resolve, reject]);
});
});
});
}
//调用
const res = await common.transactionQuery('pool1', (connection, resolve, reject) => {
const rollback = (msg) => {
connection.rollback(() => {
if (msg) {
reject({
msg: msg
});
}
connection.release();
});
}
connection.query('UPDATE props SET ...?', ..., function (error, results, fields) {
if (error) {
rollback('系统内部错误:10001');
return;
}
connection.query('UPDATE lucky SET ...', ..., function (error, results, fields) {
if (error) {
rollback('系统内部错误:10002');
return;
}
connection.query('INSERT INTO record ...', ..., function (error, results, fields) {
if (error) {
rollback('系统内部错误:10003');
return;
}
connection.commit(() => {
resolve();
connection.release();
});
});
});
});
});
console.log(res);
UNION 与 UNION ALL
UNION会去重,UNION ALL 则不会,搭配GROUP BY 和 HAVING 可以执行很多复杂的查询
UNION要求执行的多条SELECT查询字段必须相同,当某张表不存在某个字段时可以使用AS去指定
eg: 查询订单表A和订单表B每个用户的支付总金额
poolCluster.getConnection('pool1', function (err, connection) { //使用pool1池
if (err) throw err;
connection.query('SELECT user_id, SUM(amount) AS total FROM A GROUP BY user_id UNION ALL SELECT uid AS user_id, SUM(amount) AS total FROM B GROUP BY user_id', [], function (error, results, fields) {
connection.release();
if (error) throw error;
//do something
});
});
eg: 查询订单表A和订单表B每个用户的支付总金额,并支付金额大于100的用户
poolCluster.getConnection('pool1', function (err, connection) { //使用pool1池
if (err) throw err;
connection.query('SELECT user_id, SUM(amount) AS total FROM A GROUP BY user_id UNION ALL SELECT uid AS user_id, SUM(amount) AS total FROM B GROUP BY user_id HAVING total > 100', [], function (error, results, fields) {
connection.release();
if (error) throw error;
//do something
});
});
SELECT嵌套
很多场景我们需要在多张表中查询数据,嵌套写法可以优化性能
- SELECT查询执行了SELECT查询的表(因为SELECT查询执行后其实也是一张表,所以可以进行二次SELECT)
SELECT table1.name, table2.total FROM (SELECT name, COUNT(DISTINCT user_id) AS total FROM temp WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2021-08-12' GROUP BY name) AS table2, table1 WHERE table2.name = table1.name
- SELECT带WHERE查询执行了SELECT查询的结果集(SELECT查询执行后的结果集,可以进行二次SELECT)
SELECT * FROM (SELECT * FROM table2 WHERE all_number IN(SELECT number FROM table1 WHERE number = 51)) AS table3 WHERE table3.name LIKE '%亚%'
JOIN
LEFT JOIN:以左表为主,返回左表全部记录和右表满足ON条件的记录,右表没有的会以null填充
RIGHT JOIN:和LEFT JOIN相反
INNER JOIN:仅返回两个表中满足ON条件的记录
JOIN中ON和WHERE的注意事项:
WHERE是对已经搜索出的结果的过滤条件
ON是两者JOIN形成新表时用到的匹配条件
INNER JOIN中WHERE在查询语句中使用或者在ON后面使用,没有区别
eg: 查询订单表A每笔订单购买的产品名称
poolCluster.getConnection('pool1', function (err, connection) { //使用pool1池
if (err) throw err;
connection.query('SELECT A.order_num, B.product_name FROM A LEFT JOIN B ON A.product_id = B.product_id', [], function (error, results, fields) {
connection.release();
if (error) throw error;
//do something
});
});
GROUP BY
用于对SELECT查询后的数据进行分组,可以理解为先执行SELECT再进行GROUP BY
例子,当有如下用户消费金额表table
id | date | name | cost |
---|---|---|---|
1 | 2019-08-10 12:00:00 | 小明 | 5 |
2 | 2019-08-11 04:00:00 | 小明 | 200 |
3 | 2019-08-11 05:00:00 | 小红 | 100 |
4 | 2019-08-11 06:00:00 | 小李 | 90 |
现需要查询所有用户的总消费额
执行sql:
SELECT name, SUM(cost) AS total FROM `table` GROUP BY name
分析
SELECT省略
- GROUP BY name
查询后的表结构为:
name | cost |
---|---|
小明 | 5, 200 |
小红 | 100 |
小李 | 90 |
SUM(cost) AS total
SUM在GROUP BY中是对分组后的每一组cost做了一个和的处理最终结果
name | total |
---|---|
小明 | 205 |
小红 | 100 |
小李 | 90 |
这里还可以使用HAVING对聚合结果进行筛选
执行sql:
SELECT name, SUM(cost) AS total FROM `table` GROUP BY name HAVING total > 200
最终结果就会变成:
name | total |
---|---|
小明 | 205 |
HAVING和WHERE的区别主要在于:
WHERE用于对数据表中字段的过滤,不可以用字段别名(可以用表别名),后面不能跟聚合函数
HAVING用于对分组过滤,可以用字段别名,后面可以跟聚合函数
使用别名
有些时候数据表里面的数据不符合我们的分组规则,这个时候就可以使用别名进行分组,因为是先SELECT再GROUP BY
现需要查询当日消费的用户总数:
执行sql:
SELECT DATE_FORMAT(cur_time, '%Y年%m月%d日') AS time, COUNT(DISTINCT name) AS total FROM `table` GROUP BY time
DISTINCT
DISTINCT进行去重的主要原理是通过先对要进行去重的数据进行分组操作,然后从分组后的每组数据中取一条返回
分析
- SELECT DATE_FORMAT(cur_time, '%Y年%m月%d日') AS time
查询后的time字段:
time |
---|
2019年08月10日 |
2019年08月11日 |
2019年08月11日 |
2019年08月11日 |
- GROUP BY time
查询后的表结构为:
time | name |
---|---|
2019年08月10日 | 小明,小明 |
2019年08月11日 | 小红,小李 |
COUNT(DISTINCT name) AS total
COUNT在GROUP BY中是对分组后的每一组去重和统计的处理最终结果
time | total |
---|---|
2019年08月10日 | 1 |
2019年08月11日 | 2 |