在项目增量开发时,会出现开发环境因为集成了新功能或修改了旧功能,导致数据库中数据表字段修改或数据已经更新的情况。这时如果其他环境想要使用新功能,除了更新服务程序外,还需要将开发环境新增的数据整理成sql进行数据同步。
-
NodeJS连接Mysql
因为项目使用的SQL数据库是Mysql,所以NodeJS需要有连接Mysql并操作数据库的能力。这里官方提供了一个mysqljs,只需要通过 npm install mysql进入js项目即可:
npm install mysql
-
js中引入mysqljs模块
const Mysql = require('mysql');
-
连接配置信息
const conn = Mysql.createConnection({
host: '127.0.0.1',//IP
port: '3306',//端口号
user: 'user',//用户
password: 'password',//密码
database: 'database'//数据库
});
-
连接
conn.connect();
-
查询
查询示例:
/**
* @param {string} sql //查询的sql
* @param {Array} options //占位符
* @param {Function} callback //查询结果回调
*/
conn.query(sql, options, (error, results, fields) => {
if (error) throw error;
console.log(results);//返回查询结果
});
返回结果示例:
//无数据
[]
//有数据
[ RowDataPacket {
id: 363014473,
name: 'sun',
AGE: 'uid5866',
Date: 2020-05-13T02:06:54.000Z,
isrequired: null,
} ]
查询无数据时,返回空数组。有数据时,返回RowDataPacket数组,RowDataPacket对象属性与数据库中的字段名-字段值对应,并且字段名与Mysql中存储保持一致。
通过js的typeof进行测试RowDataPacket对象的字段值类型对应大致如下:数字是number,字符串是string,日期和null是object。
-
结束连接
conn.end();
-
字段值转化
由于目的是为了将查询到的结果转化为sql语句,又根据上述查询返回的结果,字段名本身与Mysql中对应,并且类型已经是string,不需要进行加工,所以只需要将返回的字段值转化为正确的string值即可。
const Moment = require('moment');//导入日期格式化模块,npm install moment
/**
* @param {*} e //字段值
* @return {string} v
*/
function toSqlValue(e) {
let v = '';
if (typeof (e) == 'string') {
if (e.search(/'/) != -1)
v += '"' + e + '"';//"'e'"
else
v += '\'' + e + '\'';//'e' 或 '"e"'
} else if (typeof (e) == 'number') {
v += e;//e
} else if (typeof (e) == 'object') {
if (e == null) {
v += 'NULL';//NULL
}else if(e instanceof Date){
v += Moment().format('YYYY-MM-DD HH:mm:ss');//'YYYY-MM-DD HH:mm:ss'
}
}
return v;
}
-
INSERT语句生成
Mysql中INSERT的语法大致如下:
INSERT INTO `table_name` (`column1`,`column2`, ...) VALUES ('value1', 'value2', ...)
INSERT语句生成比较简单,只需要遍历RowDataPacket对象,将属性依次填入column,将属性值转化后填入value即可。
/**
* @param {string} table //表名
* @return {object} row //RowDataPacket对象
*/
function insertSql(table, row){
let sql = 'INSERT INTO `' + table + '`(';
for (col in row) {
sql += '\`' + col + '\`, ';
}
sql = sql.substring(0, sql.lastIndexOf(','));
sql += ') VALUES (';
for (col in row) {
sql += toSqlValue(row[col]) + ', ';
}
sql = sql.substring(0, sql.lastIndexOf(','));
sql += ');'
return sql;
}
function bathInsertSql(table, rows){
let sql = '';
for (let i = 0; i < rows.length; i++) {
sql += insertSql(table, rows[i]) + '\r\n';
}
return sql;
}
-
UPDATE语句生成
Mysql中UPDATE的语法大致如下:
UPDATE `table_name` SET `column1` = 'value1',`column2` = 'value2', ...
WHERE `column1` = 'value1' AND `column2` = 'value2'
UPDATE中的WHERE可能会有其他复杂情况,但目前本人没有遇到复杂的场景,所以只实习了上述的UPDATE语句生成。
/**
*
* @param {string} table //表名
* @param {Array} updateCols //更新字段
* @param {object} row //RowDataPacket对象
* @param {Array} whereCols //条件
* @param {Array} whereConects //连接词
*/
function updateSql(table, updateCols, row, whereCols, whereConects){
let sql = 'UPDATE `' + table + '` SET ';
for (let i = 0; i < updateCols.length; i++) {
let col = updateCols[i];
sql += '`' + col + '` = ' + toSqlValue(row[col]) + ', ';
}
sql = sql.substring(0, sql.lastIndexOf(','));
if (whereCols == undefined) {
sql += ';';
} else {
sql += '\r\n';
sql += 'WHERE ';
let wclen = whereConects.length;
for (let i = 0; i < wclen; i++) {
let col = whereCols[i];
let con = whereConects[i];
sql += '`' + col + '` = ' + toSqlValue(row[col]) + ' ' + con + ' ';
}
let col = whereCols[whereCols.length - 1];
sql += '`' + col + '` = ' + toSqlValue(row[col]) + ';\r\n';
}
return sql;
}
-
创建mapper模块
//mapper.js
const mapper = {
tableName:{
getTableNameByID: `select * from table_name where id = ?`,
... ...
},
... ...
};
module.exports = mapper;//模块导出
-
创建table模块
//table.js
const table = {
tableName: `table_name`,
... ...
}
module.exports = table;//模块导出
-
sql整理(只提供INSERT生成示例,UPDATE同理)
利用上述提供的方法可以已经可以进行简单的sql整理了,如下:
const Mysql = require('mysql');
const fs = require('fs');
const Util = require('./utils');//包含上述字段值转化、ISNERT和UPDATE语句生成方法
const Table = require('./table');
const Sql = require('./mapper');
const Config = require('./config');//一些配置信息
const conn = Mysql.createConnection(Config.mysql);
conn.connect();
let id = Config.tableName.id;
conn.query(Sql.tableName.gettableNameByID, [id], (error, results, fields) => {
if (error) throw error;
let inserts = Util.bathInsertSql(Table.tableName, results);
//生成sql文件
fs.writeFile(Config.FILE_PATH, inserts, 'utf8', (err) => {
if (err) throw err;
console.log(`${Config.FILE_PATH}脚本生成完成`);
});
conn.end();//必须关闭,否则代码不会停止
});
!但是,对于一次需要同时更新几张表的情况,往往会出现一个查询语句的条件依赖于上一个查询语句的结果值。这时会出现query的回调中又嵌套着一个或者几个query,这时Mysql连接断开的时机不太好判断,代码会变得非常不好维护。
conn.query(Sql.tableName1.gettableName1ByID, [id], (error, results, fields) => {
if (error) throw error;
//生成脚本...
... ...
for(let r in results){
conn.query(Sql.tableName2.gettableName2ByName, [results[r][name]], (error, results, fields) => {
if (error) throw error;
//生成脚本...
... ...
});
}
});
优化:
JS中通过async关键字申明异步方法,async function fn(){},这类方法会将返回值封装为Promise对象作为返回。其中还有await关键字(需要在async方法中使用),代码执行值await处时,将会等待await的表达式执行结束后再往下执行,将异步代码转为同步执行。
-
使用Promise对象对query方法改造
Promise的回调函数有resolve和reject两个参数,resolve用于将内部异步方法执行完成后的需要的结果值传递至Promise对象,reject则是将代码错误传递至Promise对象。
function query(sql, options) {
return new Promise((resolve, reject) => {
conn.query(sql, options, (error, results, fields) => {
if (error) throw reject(error);
resolve(results);
});
});
}
-
回调嵌套优化
//将sql整理整合为一个方法
async function generate(id, callback) {
let inserts = '';
let table1 = await query(Sql.tableName1.gettableName1ByID, [id]);//代码将会阻塞直到获得query结果
inserts += `${Util.bathInsertSql(Table.tableName1, table1)}\r\n`;
for(let r in results){
let table2 = await query(Sql.tableName2.gettableName2ByName, [results[r][name]]);
inserts += `${Util.bathInsertSql(Table.tableName2, table2)}\r\n`;
}
callback(inserts);
}
generate(ID, (res) => {
conn.end();//这时只需要在方法执行结束后关闭连接即可
fs.writeFile(Config.FILE_PATH, res, 'utf8', (err) => {
if (err) throw err;
console.log(`${Config.FILE_PATH}脚本生成完成`);
});
});