From c2d8e2bcead222c735ead5ad6105456f585851e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E5=A4=A9?= Date: Tue, 28 Feb 2017 18:27:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History.md | 7 + Readme.md | 255 ++++++++++++++++++++++++ index.js | 533 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 ++ 4 files changed, 808 insertions(+) create mode 100644 History.md create mode 100644 Readme.md create mode 100644 index.js create mode 100644 package.json diff --git a/History.md b/History.md new file mode 100644 index 0000000..1b2e094 --- /dev/null +++ b/History.md @@ -0,0 +1,7 @@ +1.0.0 / 2017-02-26 +================== + + * new project + + + diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..c2e63f7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,255 @@ +![module info](https://nodei.co/npm/mysqli.png?downloads=true&downloadRank=true&stars=true) +# mysqli +> 本模块基于node-mysql模块二次封装,对基础的增删改查,主从库等按js的特点进行了简化,并对SQL注入进行安全过滤,让没有SQL基础的人,也能顺利使用; +> 当然,一些复杂的查询,以及事务等,这些不在我的服务之内,而且会用到这些功能的童鞋,本身也有一定的SQL基础了; 所以,这类童鞋,请自行使用各自习惯的SQL模块,或手写实现。 + + + +## 使用npm安装 + +```bash +npm install mysqli +``` + + +## 实例化 +> 实例化可以传2种格式的配置,1是json对象,2是数组。 +> 只有一个数据库时,默认是主库; 多于1个数据库服务时,自动以第1个为主库,其他的从库,故实例化时,`注意顺序`。 + + +```javascript + +let Mysqli = require('mysqli') + +//传入json +let conn = new Mysqli({ + host: '', // IP/域名 + post: 3306, //端口, 默认 3306 + user: '', //用户名 + passwd: '', //密码 + charset: '', // 数据库编码,默认 utf8 【可选】 + db: '', // 可指定数据库,也可以不指定 【可选】 + }) + +// 传入数组 +let conn = new Mysqli([ + { + host: 'host1', // IP/域名 + post: 3306, //端口, 默认 3306 + user: '', //用户名 + passwd: '', //密码 + charset: '', // 数据库编码,默认 utf8 【可选】 + db: '', // 可指定数据库,也可以不指定 【可选】 + }, + { + host: 'host2', // IP/域名 + post: 3306, //端口, 默认 3306 + user: '', //用户名 + passwd: '', //密码 + charset: '', // 数据库编码,默认 utf8 【可选】 + db: '', // 可指定数据库,也可以不指定 【可选】 + }, + ]) + +``` + + +## API方法 + + +### 1. escape(val) +> 这是`node-mysql`的内部方法,用于进行SQL安全过滤,这里只是做了搬运工,把它暴露出来给外部调用而已。 + + +### 2. listDB() +> 顾名思义,该方法即用于列举当前账号权限范围内的所有的数据库名称,返回值是一个数组; +> +> **注:**`该方法配置await指令可得到纯粹的数据,否则返回的是一个Promise对象` + +```javascript + +async function(){ + let db = await conn.listDB() + console.log(db) +} + +// 不使用await指令时,返回的是Promise对象 +conn.listDB().then(db => { + console.log(db) +}) + + +``` + + +### 3. useDB(db[, slave]) +- db `` +- slave `` 可选 + +> 该方法用于切换数据库,仅限于同一台机器上的数据库; 在配置中没有指定数据库的情况下,必须先调用该方法才可以进行后续的增删改查等操作。 +> +> `db`即为要切换的数据库名; `slave`为是否从库查询,默认主库。 + +```javascript + +async function(){ + + let docs = await conn.useDB('xx').query(`select * from users limit 10`); + console.log(docs); + +} + +// 不使用await指令时,返回的是Promise对象 +conn.useDB('xx') + .query(`select * from users limit 10`) + .then(docs => { + console.log(docs) + }) + + +``` + + + +### 4. query(sql[, slave]) +- sql `` +- slave `` 可选 + +> 该方法用于当内置的方法满足不了需求时,可以自行编写`sql语句`执行; 但要注意防止`sql注入`,因为该方法是最基础的方法,模块不对传入的`sql语句`进行任何的安全过滤。 +> +> `sql`即为要执行的sql语句; `slave`为是否从库查询,默认主库。 + +```javascript + +async function(){ + + let docs = await conn.query(`select * from users limit 10`); + console.log(docs); + +} + +// 不使用await指令时,返回的是Promise对象 +conn.query(`select * from users limit 10`) + .then(docs => { + console.log(docs) + }) + +``` + + + +### 5. find(conf) +- conf `` + +> 该方法用于查询多条数据。无论结果是多少条,返回的都是`数组格式`; 详细请看下面代码示例: + +```javascript + +conn.find({ + table: '', // 要查询的表 + select: ['a', 'b'], //要返回的字段,不传默认返回所有 【可选】 + where: [{ //数组格式,可以组成多个条件,默认查询全表 【可选】 + join: 'OR', //条件关系 AND, OR + op: '>', //关系符,如 =, >, <, <=, >= + key: 'aa', + val: 23 + }], + sort: { //排序, key是要排序的字段,value是排序方式, 1顺序,-1逆序 【可选】 + a: 1, + b: -1 + }, + limit: { // 查询范围,可用于分页 【可选】 + start: 0, + size: 10 + }, + slave: false // 是否从库 【可选】 +}) + +// 其中,table这一项,还可以联表,但是仅限于 'left join',要使用其他的方式联表,请自行编写sql语句 +// where条件也可以直接使用sql语句,要注意防止注入。 + +conn.find({ + table: { + master: 'xx', + unite: [ + { + table: 'aa', + on: 'xx.id = aa.xid' + }, + //... 可以联多个表, 但都是 left join + ] + }, + where: `xx.id = 123` +}) + +``` + + + +### 6. findOne(conf) +- conf `` + +> 该方法与上面的`find`方法的使用一致,区别只是该方法只返回一条数据,且为`json格式`。 + + +### 7. count(conf) +- conf `` + +> 该方法与上面的`find`方法的使用一致,不过返回的是条目总数(``) + + +### 8. insert(conf) +- conf `` + +> 该方法与上面的`find`方法的使用类似,手于插入一条数据,具体请看下面代码; +> +> **注:**`该方法一次只能插入一条数据` + +```javascript + +// 如果主键是自增ID,则结果返回的是 刚插入的数据的自增ID +conn.insert({ + table: 'xx', + data: {}, //要插入的数据 +}) + +``` + + +### 9. update(conf) +- conf `` + +> 该方法与上面的`find`方法的使用类似,用于更新数据,具体请看下面代码; +> `该方法返回的是被修改的条目数量` + +```javascript + +// 如果修改成功,则返回被修改的数量 +conn.update({ + table: 'xx', + data: {}, //要修改的数据 + where: `id = 123` +}) + +``` + + +### 10. remove(conf) +- conf `` + +> 该方法与上面的`find`方法的使用类似,用于删除指定条件的数据; 具体请看下面代码; +> `该方法返回的是被删除的条目数量` + +```javascript + +// 如果修改成功,返回的是被删除的条目数量 +conn.update({ + table: 'xx', + data: {}, //要修改的数据 + where: `id = 123` +}) + +``` + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..b951999 --- /dev/null +++ b/index.js @@ -0,0 +1,533 @@ +/** + * mysql操作类 + * @authors yutent (yutent@doui.cc) + * @date 2015-11-24 11:31:55 + * + */ +"use strict"; +require('es.shim') +let mysql = require('mysql') +/** + * [parseWhere 格式化where条件] + * @param [array] arr [条件数组] + */ + +function parseWhere(arr){ + + if(typeof arr === 'string' && !!arr){ + return ' WHERE ' + arr + }else if(Array.isArray(arr) && arr.length > 0){ + + let where = '' + for(let it of arr){ + it.join = it.join || 'AND' + it.op = it.op || '=' + + if(!/(^\(SELECT\s+.*\)$)|^`/.test(it.val) && !['IN', 'BETWEEN'].includes(it.op)){ + + it.val = mysql.escape(it.val) + } + + where += `${it.join.toUpperCase()} ${it.key} ${it.op} ${it.val} ` + } + + where = ' WHERE ' + where.trim().replace(/^(AND|OR)/, ' ') + ' ' + return where + + }else{ + return ' ' + } + +} + +class Mysqli{ + + /** + * [constructor 构造数据库连接] + */ + constructor(conf){ + if(!Array.isArray(conf)) + conf = [conf] + + //是否有从库 + this.slave = conf.length > 1 + this.conn = null + + this.pool = mysql.createPoolCluster() + let idx = 0 + while(idx < conf.length){ + let cf = conf[idx] + cf.charset = cf.charset || 'utf8' + let name = idx === 0 ? 'MASTER' : ('SLAVE' + idx) + + idx++ + this.pool.add(name, { + host: cf.host, + port: cf.port, + user: cf.user, + password: cf.passwd, + charset: cf.charset, + collate: cf.charset + ((cf.charset === 'utf8mb4') ? '_unicode_ci' : '_general_ci'), + database: cf.db + }) + } + } + + //对外的escape方法 + escape(val){ + return mysql.escape(val) + } + + + //返回数据库列表 + listDB(){ + return new Promise((yes, no) => { + this.pool + .getConnection((err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query('SHOW databases', (err, docs) => { + + conn.release() + + if(err) + return no('SHOW databases ' + err) + + let res = [] + for(let it of docs){ + res.push(it.Database) + } + yes(res) + }) + }) + }) + } + + + //选择database + useDB(db, slave){ + slave = (slave && this.slave) ? 'SLAVE*' : 'MASTER' + + this.conn = (async () => { + return await new Promise((yes, no) => { + + this.pool + .getConnection(slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query('USE ' + db, (err) => { + + if(err) + return no('Select DB ' + err) + + yes(conn) + }) + }) + + }) + })() + return this + } + + + /** + * [query sql语句执行] + * @param {[type]} sql [sql语句] + * @param {boolean} slave [是否从库] + */ + query(sql, slave){ + slave = (slave && this.slave) ? 'SLAVE*' : 'MASTER' + + if(typeof sql !== 'string') + return Promise.reject(`query error, argument sql must be string. ${typeof sql} given`) + + return new Promise((yes, no) => { + + if(this.conn){ + this.conn.then(conn => { + + conn.query(sql, (err, res) => { + conn.release() + this.conn = null + + if(err) + return no(`Query ${err}; Last exec SQL: ${sql}`) + + yes(res) + }) + + }).catch(no) + }else{ + this.pool.getConnection(slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query(sql, (err, res) => { + + conn.release() + + if(err) + return no(`Query ${err}; Last exec SQL: ${sql}`) + + yes(res) + + }) + }) + } + + }) + } + + + + /** + * [find 基础的数据查询, 支持简单的联表查询] + * @param {[type]} conf [要查询的信息] + * + * e.g. + * .find({ + * table: '', + * select: ['a', 'b'], + * where: [{ //数组格式,可以组成多个条件,默认查询全表 [可选] + * join: 'OR', //条件关系 AND, OR + * op: '>', //关系符,如 =, >, <, <=, >= + * key: 'aa', + * val: 23 + * }], + * sort: { //排序, key是要排序的字段,value是排序方式, 1顺序,-1逆序 [可选] + * a: 1, + * b: -1 + * }, + * limit: { // 查询范围,可用于分页 [可选] + * start: 0, + * size: 10 + * } + * }) + */ + find(conf){ + conf.slave = (conf.slave && this.slave) ? 'SLAVE*' : 'MASTER' + + if(!conf.table) + return Promise.reject('Find Error: empty table') + + let fields = '' //-------要返回的字段 ---------------- + if(!conf.hasOwnProperty('select') || Object.empty(conf.select)) + fields = '*' + else + fields = conf.select.join(',') + + let sql = 'SELECT ' + fields + + let table = '' //---------要查询的表 ----------------- + if(typeof conf.table === 'string'){ //单表 + table = ' FROM ' + conf.table + }else{ //联表 + table = ' FROM ' + conf.table.master + for(let join of conf.table.unite){ + table += ` LEFT JOIN ${join.table} ON ${join.on} ` + } + } + sql += table + + //查询条件 --------------------------------------- + sql += parseWhere(conf.where) + + let sort = '' //排序 ---------------------------------- + if(conf.sort && typeof conf.sort === 'object'){ + sort = ' ORDER BY ' + for(let i in conf.sort){ + let c = '' + if(conf.sort[i] === -1) + c = 'DESC' + + sort += `${i} ${c},` + } + sort = sort.slice(0, -1) + } + sql += sort + + let limit = '' //--------查询范围 ---------- + if(conf.hasOwnProperty('limit')){ + let start = conf.limit.start || 0 + let size = (conf.limit.size && conf.limit.size > 0) ? conf.limit.size : 1 + limit = ` LIMIT ${start},${size} ` + } + sql += limit + + return new Promise((yes, no) => { + + if(this.conn){ + this.conn.then(conn => { + + conn.query(sql, (err, res) => { + conn.release() + this.conn = null + + if(err) + return no(`Find ${err}; Last exec SQL: ${sql}`) + + yes(res) + }) + + }).catch(no) + }else{ + this.pool.getConnection(conf.slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query(sql, (err, res) => { + + conn.release() + + if(err) + return no(`Find ${err}; Last exec SQL: ${sql}`) + + yes(res) + + }) + }) + } + + }) + } + + /** + * [findOne 查找一条记录, 参数同 find] + */ + findOne(conf){ + let res = (async () => this.find({ + table: conf.table, + select: conf.select || [], + where: conf.where || '', + sort: conf.sort, + slave: conf.slave, + limit: {start: 0, size: 1} + }))() + return new Promise((yes, no) => { + res.then(list => { + yes(list[0] || null) + }, no) + }) + } + + /** + * [count 计算结果总数, 参数同findOne] + */ + count(conf){ + let res = (async () => this.find({ + table: conf.table, + select: ['count(*) AS total'], + slave: conf.slave, + where: conf.where || '' + }))() + return new Promise((yes, no) => { + res.then(list => { + yes(list[0] && list[0].total || 0) + }, no) + }) + } + + /** + * [insert 插入数据,单条] + * @param {[object]} conf [要插入的信息,{table: '', data: {}} ] + * + * eg. + * .insert({ + * table: 'test', + * data: {aa: 123, bb: 456} + * }, function(id){...}) + */ + insert(conf){ + conf.slave = (conf.slave && this.slave) ? 'SLAVE*' : 'MASTER' + + if(!conf.table) + return Promise.reject('Insert Error: empty table') + + let sql = 'INSERT INTO ' + conf.table + ' (' + let keys = [] + let vals = [] + + for(let i in conf.data){ + keys.push(i) + vals.push(mysql.escape(conf.data[i])) + } + sql += `${keys.join(',')}) VALUES (${vals.join(',')})` + + return new Promise((yes, no) => { + + if(this.conn){ + this.conn.then(conn => { + + conn.query(sql, (err, res) => { + conn.release() + this.conn = null + + if(err) + return no(`Insert ${err}; Last exec SQL: ${sql}`) + + yes(res.insertId) + }) + + }).catch(no) + }else{ + this.pool.getConnection(conf.slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query(sql, (err, res) => { + + conn.release() + + if(err) + return no(`Insert ${err}; Last exec SQL: ${sql}`) + + yes(res.insertId) + + }) + }) + } + + }) + } + + /** + * [insert 基础的数据修改] + * @param {[object]} conf [要修改的信息, {table: '', where: [], data: {}}] + * + * eg. + * .update({ + * table: 'test', + * data: {aa: 123, bb: 456}, + * where: [{ //数组格式,可以组成多个条件 + * join: 'OR', //条件关系 AND, OR + * op: '>', //关系符,如 =, >, <, <=, >= + * key: 'aa', + * val: 23 + * }] + * }, function(nums){...}) + */ + update(conf){ + conf.slave = (conf.slave && this.slave) ? 'SLAVE*' : 'MASTER' + + if(!conf.table) + return Promise.reject('Update Error: empty table') + + let sql = 'UPDATE ' + conf.table + ' SET ' + + let fields = [] //要更新的字段 + for(let i in conf.data){ + fields.push(i + ' = ' + mysql.escape(conf.data[i])) + } + sql += fields.join(',') + sql += parseWhere(conf.where) + + return new Promise((yes, no) => { + + if(this.conn){ + this.conn.then(conn => { + + conn.query(sql, (err, res) => { + conn.release() + this.conn = null + + if(err) + return no(`Update ${err}; Last exec SQL: ${sql}`) + + yes(res.affectedRows) + }) + + }).catch(no) + }else{ + this.pool.getConnection(conf.slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query(sql, (err, res) => { + + conn.release() + + if(err) + return no(`Update ${err}; Last exec SQL: ${sql}`) + + yes(res.affectedRows) + + }) + }) + } + + }) + + } + + /** + * [remove 基础的数据删除] + * @param {[type]} conf [要删除的信息, {table: '', where: []}] + * + * eg. + * .update({ + * table: 'test', + * where: [{ //数组格式,可以组成多个条件 + * join: 'OR', //条件关系 AND, OR + * op: '>', //关系符,如 =, >, <, <=, >= + * key: 'aa', + * val: 23 + * }] + * }, function(nums){...}) + */ + remove(conf){ + conf.slave = (conf.slave && this.slave) ? 'SLAVE*' : 'MASTER' + + if(!conf.table) + return Promise.reject('Remove Error: empty table') + + let sql = 'DELETE FROM ' + conf.table + + if(conf.where) + sql += parseWhere(conf.where) + + return new Promise((yes, no) => { + + if(this.conn){ + this.conn.then(conn => { + + conn.query(sql, (err, res) => { + conn.release() + this.conn = null + + if(err) + return no(`Remove ${err}; Last exec SQL: ${sql}`) + + yes(res.affectedRows) + }) + + }).catch(no) + }else{ + this.pool.getConnection(conf.slave, (err, conn) => { + if(err) + return no(`MySQL connect ${err}`) + + conn.query(sql, (err, res) => { + + conn.release() + + if(err) + return no(`Remove ${err}; Last exec SQL: ${sql}`) + + yes(res.affectedRows) + + }) + }) + } + + }) + } + + + +} + + + +module.exports = Mysqli \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5276f90 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "mysqli", + "version": "2.0.0", + "description": "MySQL tool", + "main": "index.js", + "dependencies": { + "es.shim": "^0.0.2", + "mysql": "^2.13.0" + }, + "repository": "https://github.com/yutent/mysqli.git", + "author": "yutent", + "license": "MIT" +}