mybatis plus这么好用,node为什么没有

最近找到了工作,因为公司比较小,也没什么事情做,于是开始摸鱼,之前学过一段java,但是用着磕磕巴巴,于是还是捡起了node,还是js语法比较熟悉,但是!!!!为什么就没有类似于spring的生态呢,想找一个类似于spring security的框架,只找到了登录鉴权的,感觉好鸡肋,mysql只看了一下mysql那个库,竟然还要手写sql!!!  
于是抱着闲着也是闲着,不如封装一下的心思,开始了封装mysql的日子,前前后后做了两天的时间(比较菜啦,各位大大嘴下留情,刚开始工作请不要骂我QAQ)  
先展示一下用法
``` javascript
import { createDb } from "../../utiles/db/db"

const db = createDb({
  localhost: "localhost",
  root: "root",
  password: "admin123",
  database: "tb_shudong"
})
```
`createDb`是我封装的一个初始化的函数,后边会贴详细代码
`const queryWrapper = db.createWrapper(User);`
这个是emmm,类似于mp的那个baseMapper的东西,就是可以使用crud方法,自定义sql,参数是要传入一个实体类,主要用于ts静态检查

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a3917187afb44d46b1fd8ecea040e2b0~tplv-k3u1fbpfcp-watermark.image?)
这是暂时封装了几个crud的方法,还支持自定义sql语句

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/21d09ecc147b4830aab527fbc3ce26ad~tplv-k3u1fbpfcp-watermark.image?)  
可以提供提示,今天还在考虑要不要做成一个类,实体类继承就可以使用方法,但是静态提示这一块不太会搞就没有去做,我感觉用实体类继承一下会更好  

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5b1ba89c19e848f39fdb1ef0f0de95b1~tplv-k3u1fbpfcp-watermark.image?)
这是一个实体类,导入的是一些装饰器,也就是java的注解
- tableName(name?:string):指定表名,比如实体类叫做User,如果不做任何配置的话,默认表名就是tb_user(这个后边讲),如果传入tb_demo,那么表名就是tb_demo
- tableField(name?:string|Field,options?:Field):这个就是指定列名,如果属性名跟列名不一样,可以用这个注解来指定某个属性是某一个列名,可以传入一个配置项,一般是在创建表的时候使用,为什么第一个参数是string|Field类型,因为我想实现就是第一个参数可以是字符串,第二个是配置项,但如果想只写配置项,那么字符串要传入一个同属性名的字符串,感觉不太好,如果放在第二个的话,第一个是配置项,那么要映射列名,还要写配置项,也没有很好的解决办法就暂时用了这个
- tableId(name:string|Id,options:id):理由同上,指定主键,当使用xxxById这种类型直接传入id,会自动寻找主键,前提是指定了此注解,否则找不到,也可以映射列名(是这个意思吧,一些专业名词搞不懂,意思你们肯定懂的!!)
- createTable(name:string):创建表,name指定表名,不写默认tb_+类名

现在说一下配置项,直接看一下代码即可
```
enum Data {
  INT, //带符号的整数值
  TINYINT, //带符号的小整数值
  SMALLINT, //带符号的中等数值
  BIGINT, //大整数值
  BIT, //用于存储位值0 1
  FLOAT, //用于存储小浮点数
  DOUBLE, //用于存储双精度浮点数
  CHAR, //固定长度的字符
  VARCHAR, //可变长度的字符
  TEXT, //大量的文本数据
  DATE, //日期值
  TIME, //时间值
  DATETIME, //日期和时间值
  TIMESTAMP //时间戳
}
type DataType = keyof typeof Data

interface Field {
  type: DataType,
  notNull?: boolean,
  typeLen: number, //类型长度,比如char(11)
  defaults?: string,
  comment?: string, //注释
}

interface Id extends Field {
  type: DataType,
  primary: boolean, //主键
  auto: boolean, //自增
}
```
注解的代码也贴一下吧,比较菜,逻辑想的比较简单
```
//自定义表名
export function tableName(name: string) {
  return (target: any) => {
    const targetName = target.name;
    const str = JSON.parse(`{"${targetName}":"${name}"}`);
    store.classNameArr.push(str);
    store.classFieldObj[name] = store.classFieldObj[targetName];
    store.classFieldObj[targetName] = null
  }
}
//自定义字段名,传入字符串代表要映射的数据库的字段
//比如 数据库字段为id,类属性是userId,可以通过此注解指定userId映射到数据库的id字段
export function tableField(fieldName?: string | Field, options?: Field) {
  return (target: any, name: string) => {
    if (typeof fieldName === "object") {
      options = fieldName;
      fieldName = undefined;
    }
    let str;
    if (options) {
      const { type, typeLen, notNull, comment, defaults } = options;
      str = JSON.parse(`{"${name}":{"alias":"${fieldName}","type":"${type}","typeLen":"${typeLen}","notNull":"${notNull}","comment":"${comment}","default":"${defaults}"}}`);
    } else {
      str = JSON.parse(`{"${name}":{"alias":"${fieldName}"}}`);
    }
    const keys = Object.keys(store.classFieldObj);
    //如果自定义表名就用自定义表名,否则就用类名
    if (!keys.includes(target.constructor.name)) {
      store.classFieldObj[target.constructor.name] = [];
    }
    store.classFieldObj[target.constructor.name]!.push(str);

  }
}
// 指定主键,可选,主键映射的字段名,第一个参数可以是映射列名或者配置项,但是第二个参数只能是配置项
export function tableId(fieldName?: string | Id, options?: Id) {
  return (target: any, name: string) => {
    if (typeof fieldName === "object") {
      options = fieldName;
      fieldName = undefined
    }
    //主键不能为null
    let str;
    if (options) {
      const { type, typeLen, primary, auto, comment } = options!;
      str = JSON.parse(`{"${name}":{"alias":"${fieldName}","type":"${type}","typeLen":"${typeLen}","primary":"${primary}","auto":"${auto}","comment":"${comment}"}}`);
    } else {
      str = JSON.parse(`{"${target.constructor.name}":{"alias":"${fieldName ? fieldName : name}"}}`);
    }
    const keys = Object.keys(store.classFieldObj);
    //如果自定义表名就用自定义表名,否则就用类名
    if (!keys.includes(target.constructor.name)) {
      store.classFieldObj[target.constructor.name] = [];
    }
    store.classFieldObj[target.constructor.name]!.push(str);
    store.classIdArr.push(str);
  }
}
//创建数据表,默认以类名进行表命名,可传入字符串指定名称
export function createTable(fieldName?: string) {
  return (target: any) => {
    let strs = "";
    
    if (fieldName) {
      const targetName = target.name;
      const str = JSON.parse(`{"${targetName}":"${fieldName}"}`);
      store.classNameArr.push(str);
      store.classFieldObj[fieldName] = store.classFieldObj[targetName];
      store.classFieldObj[targetName] = null
    }

    store.classFieldObj[fieldName ? fieldName : target.name]?.forEach((item, index) => {
      const key = Object.keys(item)[0];
      const { alias, type, typeLen, notNull, comment, defaults, primary, auto } = item[key]
      strs += `${alias != "undefined" ? alias : key} ${type}(${typeLen}) ${primary ? "PRIMARY KEY" : ""} ${auto ? "AUTO_INCREMENT" : ""} ${notNull ? "not null" : ""} ${defaults != "undefined" && defaults ? "default " + defaults : ""} ${comment != "undefined" && comment ? "comment " + `'${comment}'` : ""},`
    })
    const str = `create table if not exists ${fieldName ? fieldName : target.name} (${strs.substring(0, strs.length - 1)})`;
    store.createTableArr.push({
      table: str,
      isBool: false
    });
  }
}
```
> 有一个疑问,建表这样会有sql注入的 风险吗,如果有的话改为?传值了就

其中store存储了一些数据
```
const classNameArr:Array<{[propname:string]:string}> = []
type A = {
  [propName:string]:Array<{[propname:string]:{[propName:string]:any,alias:string}}>|null //alias:别名
}
const classFieldObj:A = {};
const classIdArr:Array<{[propName:string]:{[propName:string]:any,alias:string}}> = [];
const createTableArr:{table:string,isBool:boolean}[] = [];

export const store = {
  classNameArr, //存储的自定义表名
  classFieldObj, //存储的自定义字段名
  classIdArr, //存储的主键
  createTableArr, //存储的建表内容
}
```
基本功能就这些了

然后说一下创建过程  
首先通过`createDb`函数传入配置项,因为我暂时就用到了几个,就没写很多,主要是也不是很懂mysql,想找个大佬指点一下
```
interface Options {
  localhost: string;
  root: string;
  password: string;
  database: string;
  tb_prefix?: string; //表前缀
}

export function createDb(options: Options) {
  let { localhost, root, password, database, tb_prefix } = options;
  if (!tb_prefix) {
    tb_prefix = "tb_"; //默认表前缀
  }
  const db = new QueryMapper(localhost, root, password, database, tb_prefix);
  return db;
}
```
表前缀默认tb_,如果不喜欢可以传入配置项替换
`QueryMapper`类就是初始化连接池,然后暴露一个方法去使用crud方法
```
class QueryMapper {
  db = createPool({}); //mysql实例
  tb_prefix!: string;
  constructor(host: string, user: string, password: string, database: string, tb_prefix: string) {
    this.db = createPool({
      host,
      user,
      password,
      database //数据库名称
    });
    this.tb_prefix = tb_prefix;
  };
  createWrapper<T>(entityClass: new () => T) { //这边就是用于ts提醒
    const en = new entityClass();
    type A = keyof typeof en;
    type Entity = {
      [k in A]?: any
    }
    const func = new CreateWrapper<Entity>(this.db, this.tb_prefix, entityClass);
    return func;
  }
}
```
不知道为什么entityClassany不行,必须要这种写法,这个东西困扰了我好久才解决  
`CreateWrapper`类就是封装的sql方法,无非就是字符串拼接,没什么好说的
```
class CreateWrapper<T extends { [propname: string]: any }> {
  db!: Pool;
  table!: string; //表名/自定义表名
  tb_prefix!: string; //表前缀
  tableId!: string;
  constructor(obj: Pool, tb_prefix: string, entity: new () => T) {
    this.db = obj;
    this.table = `${tb_prefix}${entity.name.toLocaleLowerCase()}`;
    this.tb_prefix = tb_prefix;
    store.classIdArr.forEach(item => {
      if (item[entity.name]) {
        this.tableId = item[entity.name].alias
      }
    })

    //遍历查看是否自定义表名,如果有,则更换table
    let a = true;
    store.classNameArr.forEach(item => {
      const key = Object.keys(item)[0];
      if (a) {
        if (key === entity.name) { //如果自定义了表名则使用自定义的
          this.table = item[key];
          a = false;
        }
      }
    })
    //循环创建表
    store.createTableArr.forEach(item => {
      if(!item.isBool){
        this.sql(item.table).then(res=>{
          console.log(res);
        }).catch((err)=>{
          console.log(err);
        })
        item.isBool = true
      }
    })
  }
  //通用sql方法
  sql(query: string, params?: Array<string | number>): Promise<any> {
    console.log(query)
    return new Promise((res, rej) => {
      this.db.query(query, params, (error, results, fields) => {
        if (error) {
          rej(error)
        } else {
          res(results);
        }
      })
    })
  };
  //根据传入的Array<any>类,区分出键跟值
  getKeyAndValue(arr: T) {
    const keyArr: Array<string> = [];
    const valueArr: Array<any> = [];

    keyArr.push(...Object.keys(arr))
    keyArr.forEach(item => {
      valueArr.push(arr[item]);
    })
    //将类的属性名转为mysql字段名,如果修改了
    let tableArr: Array<string> = this.table.split(this.tb_prefix);
    let table = "";
    if (tableArr.length == 1) {
      table = tableArr[0];
    } else {
      table = tableArr.slice(1, tableArr.length).join("");
      table = table[0].toLocaleUpperCase() + table.slice(1);
    }

    //如果有映射列名就转换为数据库字段
    const fieldArr = store.classFieldObj[table];
    fieldArr!.forEach(item => {
      keyArr.forEach((item2, index, arr) => {
        if (item[item2]) {
          arr[index] = item[item2].alias
        }
      })
    })
    //设置值 key=value,key=value...用于update
    let str = "";
    for (let i = 0; i < keyArr.length; i++) {
      str += `${keyArr[i]}=?,`;
      if (i === keyArr.length - 1) {
        str = str.substring(0, str.length - 1);
      }
    }
    //条件判断 key=value and key=value ... 用于where后
    let where = "";
    for (let i = 0; i < keyArr.length; i++) {
      where += `${keyArr[i]}=? and `;
      if (i == keyArr.length - 1) {
        where = where.substring(0, where.length - 5);
      }
    }

    const arrs: Array<string> = [];
    const rep = /[A-Z]/g
    keyArr.forEach((item) => {
      const arr = item.match(rep);
      arr?.forEach(item2 => {
        const ind = item.indexOf(item2); //找到大写的那个位置
        const arr = item.split("")
        arr.splice(ind, 1, item2.toLocaleLowerCase());
        arr.splice(ind, 0, "_");
        item = arr.join("");
      })
      arrs.push(item);
    })

    return {
      keyArr: arrs,
      valueArr,
      updateStr: str,
      whereStr: where,
    }
  }
  //获取表的所有内容
  async getAll(): Promise<Array<T>> { //需要使用??的形式就是,from tb_user
    const query = "select * from ??"; //标识符比如表名这种select * from tb_user,如果使用?则会"tb_user"会报错
    const res = await this.sql(query, [this.table]);
    return res;
  };
  //根据id获取表数据,idObj:{表的id字段:值}
  async getById(id: string | number): Promise<T> {
    const query = "select * from ?? where " + this.tableId + "=?"
    const res = await this.sql(query, [this.table, id]);
    return res;
  }
  //根据条件筛选数据,arr是一个数组,每一项都是{条件字段:值}形式
  async selectList(arr: T): Promise<Array<T>> {
    const { whereStr: where, valueArr } = this.getKeyAndValue(arr);
    const query = "select * from ?? where " + where;
    const res = await this.sql(query, [this.table, ...valueArr]);
    return res as Array<any>;
  }
  //修改某个字段全部数据
  async update(arr: T, whereArr: T) {
    const { valueArr, updateStr } = this.getKeyAndValue(arr);
    let query = "";
    if (whereArr) {
      const { valueArr: valueArr2, whereStr } = this.getKeyAndValue(whereArr);
      query = `update ?? set ${updateStr} where ${whereStr}`;
      const res = await this.sql(query, [this.table, ...valueArr, ...valueArr2]);
      return res;
    }

    query = `update ?? set ${updateStr}`;
    const res = await this.sql(query, [this.table, ...valueArr]);
    return res;
  }
  //根据id修改某个字段的数据
  async updateById(arr: T, id: string | number) {
    const { valueArr, updateStr } = this.getKeyAndValue(arr);
    const query = `update ?? set ${updateStr} where ${this.tableId}=?`;
    const res = await this.sql(query, [this.table, ...valueArr, id]);
    return res;
  }
  //向数据库中插入数据
  async insertInfo(arr: T) {
    const { keyArr, valueArr } = this.getKeyAndValue(arr);
    const keyLen = Object.keys(arr[0]).length;
    let str = "";
    for (let i = 0; i < valueArr.length; i += keyLen) { //有问题
      str += `(${"?".repeat(keyLen).split("").join(", ")}),`
    }
    const query = `insert into ?? (${keyArr.join(", ")}) values ${str.substring(0, str.length - 1)}`;
    const res = await this.sql(query, [this.table, ...valueArr]);
    return res;
  }
  //根据id删除数据,
  async deleteById(id: string | number) {
    const query = `delete from ?? where ${this.tableId}=?`;
    const res = await this.sql(query, [this.table, id]);
    return res;
  }
  //根据条件批量删除数据
  async deleteList(arr: T) {
    const { valueArr, whereStr } = this.getKeyAndValue(arr);
    const query = `delete from ?? where ${whereStr}`;
    const res = await this.sql(query, [this.table, ...valueArr]);
    return res;
  }
  //分页查询
  async page(page: number, pageSize: number): Promise<Array<T>> {
    const query = `select * from ?? limit ?,?`;
    const res = await this.sql(query, [this.table, page, pageSize]);
    return res;
  }
  //自定义sql
  async customSql(query: string, params: Array<string | number>) {
    const res = await this.sql(query, params);
    return res;
  }
}
```
基本用法就是这个样子,然后现在准备开始做项目了,后续会慢慢更新,等项目完成以后考虑继续完善这一个东西,第一次做还是蛮开心的,第一次写文章请大家多多包涵,也希望能有大佬指点一下,毕竟mysql确实不太懂,不怕大家笑话,sql语句还是百度写的hhh,祝大家天天开心,升职加薪下班早,代码一次跑通永无bug!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值