最近找到了工作,因为公司比较小,也没什么事情做,于是开始摸鱼,之前学过一段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!!!