为Node.js加一个DB类

1,282 阅读3分钟

背景

作为前端工程师的我们,可能不懂PHPJava等语言,但也想创建自己的服务,那么Node.js是一个非常好的选择。

而在学习或者使用Node.js的时候,不免会与MySQL等数据库进行连接交互。而笔者在学习Node之前,使用过ThinkPHP5,其中内置的DB类让我半吊子“后端”用起来十分舒服,其中的链式调用更是方便无比。

那么此文,我们也尝试着去封装一个DB类,毕竟手写SQL太容易出现错误辣。

明确需求

这个环节有点像PM跟我们阐述一个功能需求。假设已经存在这么一个类,我们希望这个类是如何为我所用的?以下为我自己的几个需求,后面的编码就从这几个需求出发,一步步完善我们的类即可。

链式调用

例如TP5中,如果要查询user表中id为1的用户,我们常常这样子写:

DB::table('user')->where('id',1)->find();

一开始得确定传入的表名,也算是我们约定的一个规范吧。而中间环节可以有多个where,类自动帮我们拼接好查询条件,在最后调用的应该是以下几个常用的方法:

  • find 查询一条记录
  • select 查询符合条件的记录
  • count 返回记录的条数
  • update 更新操作
  • delete 删除操作
  • insert 插入操作

例如如果我们的类写好了,描述上述语法,我希望这样使用:

DB.table('user').where('id',1).find();

基于Promise封装

我们知道Node里面查询数据库是一个异步操作,而我在使用这个类的时候,希望越简洁越好。不要让我去思考一些异步的流程。比如可以像下面这样:

const result = await DB.table('user').select();

这种使用async/await的写法我个人是比较喜欢的,看起来代码逻辑较为清晰一些。

其他的小偏好

find/select方法我希望可以按需取字段,insert/update的时候我希望传入一个Object来描述,where传参的时候如果不穿操作符,默认就是等号,还能支持原生SQL查询......

常用方法编码

那大概捋了一下思路之后,我们就开始进行编码吧。先搭起来一个架子:

class Orm {
    constructor(debug = 0) {
        this.sql = ''
        this._table = ''
        this.filter = ''
        this.debug = debug
    }
    count() {}
    find() {}
    select() {}
    insert() {}
    delete() {}
    update() {}
    where(){}
    table(){}
}
module.exports = {
    Orm
}

上述除了待会要实现的具体方法之外,还有三个我们这个类所用到的变量。

  • sql 最后拼接成的SQL语句
  • _table 一开始暂存的表名
  • filter 过滤条件字符串
  • debug debug模式,如果为1则会打出完整SQL语句

而在使用这个类的时候尽量是单例模式,这样与JS单线程的特点结合起来,就不会出现什么冲突。

select 方法实现

在真正开始动手实现我们的第一个查询方法时,还是回到我们之前说的。如果这个类写好了,我们希望怎样去使用它。我希望像下述一样去使用:

const Orm = require('./orm/index').Orm
const orm = new Orm()
async function select() {
    const result = await orm.table('good').select()
    console.log('result',result)
}

基于上述最简单版本的select方法,我们可以开始以下的编码。 首先table方法的实现就是缓存一下表名即可,而这些需要链式调用的方法,只需在最后加上一句return this即可。

table(table){
    this._table = table
    return this
}

select的具体实现如下,其实就是拼接一下字符串。传列名则按需取字段,不传就取所有字段。

select(..._cols) {
    let cols = [..._cols]
    if (cols.length != 0) {
        let c = ''
        for (let i = 0; i < cols.length; i++) {
            let char = cols[i]
            if (i != cols.length - 1) {
                c += `${char},`
            } else {
                c += `${char}`
            }
        }
        this.sql = `SELECT ${c} FROM ${this._table} ${this.filter}`
    } else {
        this.sql = `SELECT * FROM ${this._table} ${this.filter}`
    }
    //run 方法则为真正去跟数据库交互的方法
    return this.run()
}

find方法与select方法的区别只是find方法在最后加上了LIMIT 1而已,其他具体实现基本一样

实现中,最后的CURD方法都会调到这个run方法,让我们来看一下这个run方法的实现。真正与mysql交互前,还需安装好mysql驱动。

//dbConfig.js
module.exports = {
    mysql: {
        host: 'localhost',
        user: 'root',
        password: 'your mysql psw',
        database: 'shop',
        port: 3306
    }
};
const mysql = require('mysql')
const dbConfig = require('./dbConfig')
//创建一个mysql连接池
const pool = mysql.createPool(dbConfig.mysql)
run() {
    let _sql = this.sql
    //debug模式打印出SQL语句
    if(this.debug) console.log(_sql)
    let table = this._table
    //注意在进行数据库查询的前一刻,应该把之前缓存的字符串清空掉。
    this.done()
    //基于Promise封装
    return new Promise((resolve, reject) => {
        if (!table) {
            reject('表名不可为空')
        } else {
            //进行数据库查询
            pool.getConnection((err, con) => {
                if (err) {
                    reject(err)
                } else {
                    con.query(_sql, (err, result) => {
                        if (err) {
                            reject(err)
                        } else {
                            resolve(result)
                            con.release()
                        }
                    })
                }
            })
        }
    })
}

done(){
    this.sql = ''
    this._table = ''
    this.filter = ''
}

最后再来总结一下流程:

  1. 暂存表名
  2. 拼装SQL
  3. return this保证链式调用
  4. Promise+async/await保证更友好的使用形式

了解了大概的套路之后,之后的编码就会更加顺利了。我们再来实现多几个方法。

where实现

在使用where的时候,不自觉的就往使用过的TP框架上面靠了。有一个小偏好就是,当传参只有两个时,默认的操作符是'='。 例如

DB.table('user').where('id',1).select()
//等同于
DB.table('user').where('id','=','1').select()
where(...arg) {
    let arr = [...arg]
    let column = arr[0] || null,
        op = arr[1] || null,
        val = arr[2] || null
    if (arr.length == 2) {
        column = arr[0]
        val = arr[1]
        op = '='
    }
    if (this.filter) {
        this.filter += ` AND ${column} ${op} '${val}'`
    } else {
        this.filter = `WHERE ${column} ${op} '${val}'`
    }
    return this
}

where方法的具体实现其实就是把传入的过滤字符串拼装filter变量里面,再最后做CURD操作时,将filter拼装上去即可。

insert实现

insert 方法我希望像如下这样使用:

let result = await orm.table('user').insert({
    name:'张三',
    age:18
})
console.log(result)

那么编写的时候,把对象遍历完key和value,依次拼装即可。

insert(obj) {
    let keys = '',
        values = ''
    let objArr = Object.keys(obj)
    for (let i = 0; i < objArr.length; i++) {
        let key = objArr[i]
        let val = obj[key]
        if (i != objArr.length - 1) {
            keys += `${key},`
            //注意 value最好用''括起来
            values += `'${val}',`
        } else {
            keys += `${key}`
            values += `'${val}'`
        }
    }
    this.sql = `INSERT INTO ${this._table} (${keys}) VALUES (${values})`
    return this.run()
    }

count方法

最后再贴一个count方法的实现吧

async count() {
    let result = await this.select()
    return new Promise((resolve, reject) => {
        resolve(result.length)
    })
}

其他的方法大同小异就不在赘述了,可以自行拓展一些更好的方法。拼装好SQL之后调用run即可。

最后

行文至此,感谢阅读,如果您喜欢的话,可以帮忙点个like哟~