# 数据库orm ## ## MySql操作: 在**config.js**文件里,有如下mysql链接池配置: ```typescript /** * mysql配置文件 * @type {IPoolConfig & MysqlConfig} */ exports.mysql = { host: '127.0.0.1', user: 'root', password: '123456', database: 'blog', port: 3306, } ``` 该链接池的详细配置见**IPoolConfig & MysqlConfig**接口注释。 然后通过`MysqlConnecter`类来创建一个数据库链接: ```typescript import {MysqlConnecter} from "dfv/src/db/MysqlConnecter"; import * as cfg from "../config/config"; export const db = { //mysql链接 connecter: new MysqlConnecter(cfg.mysql), } ``` 我们可以通过db.connecter.query(查询)或db.connecter.update(更新)来执行原生的sql语句,**这样会被sql注入攻击,不推荐。** 接下来主要介绍如何像LINQ一样,写出强类型且带智能感知的sql语句。 首先我们需要数据表对应的model。 ### 自动生成表model: 我们可以在http.ts的开始部分看到一行代码: ```typescript mysqlModel.generate(); ``` 以上代码通过`db.connecter`链接,读取出其中所有的表,自动生成对应的model代码至`/runtime/models/`目录。 进入`/runtime/models/`目录可以看到每一个表都生成了一个对应的同名class文件。 生成的表model大致是这个样子: ```typescript import {sql} from "dfv/src/public/sql"; //表名,对应于数据库中的表名 export class TestTable { /** * id字段,对应数据库里的int,double,float类型 */ @sql.primaryKey//装饰为主键 @sql.autoIncrement//自动增长 id: number = 0; /** *name属性对应数据库里的char类型 */ name: string = ""; age: number = 0; } ``` 属性与mysql中字段对应类型参照这里:https://github.com/mysqljs/mysql#type-casting model生成规则代码模板,可以在**config/template/mysqlModel.ts**里修改。 对应的`/models/db.ts`文件: ```typescript import {MysqlConnecter} from "dfv/src/db/MysqlConnecter"; import * as cfg from "../config/config"; export const db = { //mysql链接 connecter: new MysqlConnecter(cfg.mysql), //TestTable表,返回一个SqlBuilder对象 TestTable: ()=>new SqlBuilder(TestTable, db.connecter), } ``` 然后我们就可以通过SqlBuilder类的成员函数来执行连贯sql操作: ### 插入: ```typescript let d = new TestTable(); d.name = "123" //向TestTable插入一条数据,insert会自动生成覆盖TestTable的autoIncrement属性 let ret = await db.TestTable().insert(d); //插入一个数组 ret = await db.TestTable().insert([d, {id: 0, name: "bbb", age: 1}]); /** * 默认情况下insert会覆盖掉autoIncrement装饰过的属性,想要手动指定一个id的话给Insert第二个参数传false */ ret = await db.TestTable().insert({id: 100, name: "bbb", age: 1}, false); ``` 所有需要数据库链接操作返回的都是一个Promise对象,insert返回的是 `Promise`类型。number表示插入的行数。操作失败则抛出异常。 ### 查询: ```typescript //生成了这样的sql语句:select id,name,age from TestTable where name='名字' let list = await db.TestTable().where(f => f.name.eq("名字")).toArray() ``` 上例调用的where()成员函数,接收了一个匿名函数,该匿名函数的第一个参数是TestTable类的实例(上面的**f**变量),所以当我们打f.时会自动感知出TestTable的所有字段成员. TestTable类的所有属性,都被添加了一些扩展函数(像上面的**eq("名字")**),这些扩展函数和sql中的语法一一对应。例如eq对应=,le对应<,gt对应>,leEq对应<=,grEq对应>=,in和like之类的不变。 最后我们调用toArray()函数获取结果列表:`Promise>`类型。再通过await 获取到的是`Array`类型。 也可以调用toOne(),返回的是单条记录:`Promise` 再看一些复杂的查询例子: ```typescript //带上逻辑运算符,生成结果:where id>1 and id<=10 or name='' and id!=5 await db.TestTable().where(f => f.id.gt(1) & f.id.leEq(10) | f.name.eq("") & f.id.notEq(5)).toArray() //where里加上括号嵌套也是可以的: where id>1 and (id<10 or name='') and id!=5 order by id asc,name desc limit 1,2 await db.TestTable() .where(f => f.id.gt(1) & (f.id.le(10) | f.name.eq("")) & f.id.notEq(5)) .order(f => f.id.asc() & f.name.desc()) .limit(1, 2) .toArray() //分段组合sql语句 let build = db.TestTable(); build.where(f => f.id.gt(1) & (f.id.le(10) | f.name.eq(""))) build.and(f => f.id.eq(1)) build.or(f => f.name.in(["标题1", "标题2"]) | f.name.like("%123%")) //最终结果:where id>1 and (id<10 or name='') and id=1 or name in ('标题1', '标题2') or name like '%123%' let res = await build.toArray(); ``` 注意上例的三个函数:where(),or(),and(),其中where函数是重置sql语句,and()和or()是添加sql语句。 像下面这样只使用and和or也是可以的: ```typescript //where id>0 and id<10 or id=100 db.TestTable().and(f => f.id.gt(0)).and(f => f.id.le(10)).or(f => f.id.eq(100)).toOne(); ``` ### 删除: 和上面的查询类似,只是在where条件基础上把toArray()替换为delete(): ```typescript //返回值为影响行数。 let count = await db.TestTable().and(f => f.id.gt(0)).delete() ``` ### 更新: 在以上where条件基础上使用update(): ```typescript //set name='abc',age=age+1 await db.TestTable().where(f => f.id.gt(0)).update(f => f.name.set("abc") & f.age.inc(1)) /** * 可以使用set()函数连贯更新操作 * set name=age+age-2 ,age=age-1 where id=0 */ await db.TestTable().where(f => f.id.gt(0)) .set(f => f.name.eq(f.age.add(f.age.sub(2)))) .set(f => f.age.dec(2)) .update() ``` 同样的update()返回的也是影响行数:`Promise` 。除了上述的inc,dec自增减函数外,还可以使用四则运算:加add(),减sub(),乘mul(),除div()。 ### 联表操作: 在数据库中创建视图,会自动生成对应的model。 ### 查询指定字段: 使用select()函数: ```typescript /** * 向select()传递表达式,来指定返回字段 * select id,age from TestTable */ await db.TestTable().select(f => f.id & f.age).toArray(); /** * unselect,排除某些字段: * select name,age from TestTable */ await db.TestTable().unselect(f => f.id).toArray(); /** * 使用别名 * select max(id) as maxId,count(name) as countName */ let rr = await db.TestTable().selectAs(f => ({ maxId: f.id.max(), countName: f.name.count(), })).toOne(); if (rr) console.log(rr.countName + " " + rr.maxId); /** * 向select()传递对象,来指定返回字段 * select id,name from TestTable */ await db.TestTable().selectObj({id: 0, name: ""}).toArray(); ``` 以上,select(),where(),order(),limit()的前后调用顺序对sql生成结果无影响。 ### 事务操作: 通过`db.connecter`mysql链接的transaction成员来使用事务 ```typescript //所有事务操作写在transaction回调函数里,中途失败则roll back并抛异常。 await db.connecter.transaction(async (t) => { //事务插入操作 await db.TestTable().transaction(t).insert({id: 0, name: "bbb", age: 1}); //可以手动抛异常来中断事务(roll back) //throw Error("roll back"); await db.TestTable().transaction(t).update(f => f.name.set("1")) await db.TestTable().transaction(t).update(f => f.name.set("2")) }) console.log("事务结束") } ``` ### 执行原始sql语句: 有时候我们既想写原始sql语句,但又不想包含字段名硬编码,可以使用:`sql.src` ```typescript //toArray会自动带上selet ... from ... //select .. from TestTable where name='aaa' and age<10 db.TestTable().where(f => sql.src `${f.name}='aaa' and ${f.age}<10`).toArray() ``` ### 内存缓存: 本框架提供了简单的表数据内存缓存功能。 但是并不支持进程间的数据同步,所以当多进程部署时,还是应该使用[redis](https://redis.io/)。 首先用sql.cacheId装饰器对待缓存的表指定查询条件: ```typescript class TestTable { //id字段 @sql.primaryKey//装饰为主键 @sql.autoIncrement//自动增长 id: number = 0; name: string = ""; /** * 将该字段标注为缓存查询条件 */ @sql.cacheId age = 0; } ``` **查询缓存:** ```typescript //从缓存中获取所有age为1的数组结果集。当缓存中不存在时,自动从数据库获取并缓存。 let list = await db.TestTable.cacheGetInt(1); ``` 如上所示使用cacheGetInt()对应整形字段,也可以使用cacheGetString()对应字符串字段。 返回值为:`Promise>` 。 **删除缓存:** 当我们修改了TestTable表中的数据之后一定要删除对应的缓存,不然会造成缓存与数据库内容不同步。 ```typescript //删除所有age为1的缓存 db.TestTable().cacheRemove(1) //移除该表所有缓存 db.TestTable().cacheRemoveAll(); ``` 也可以使用自定义sql语句做为缓存查询条件: ```typescript //自定义缓存查询条件,回调函数有个val参数,代表cacheGetInt的传入值 //手写sql语句是不带防sql注入的,使用sql.filter()函数过滤输入参数val @sql.cacheWhere(val=>`age>${sql.filter(val)} order by age`) class TestTable { //id字段 @sql.primaryKey//装饰为主键 @sql.autoIncrement//自动增长 id: number = 0; name: string = ""; age = 0; } ``` 查询缓存: ```typescript //从缓存中获取所有age大于10的结果集 let list = await db.TestTable().cacheGetInt(10); ``` ------ ## MongoDB操作: mongoDB的增删改查写法和以上mysql操作基本上一致(我们可以不用手写抽象语法树了)。这样为我们无缝切换数据库提供了一点便利。 唯一的问题是mongoDB不支持联表与事务操作,并且mongoDB也无法通过现有的表直接生成model代码。 不过mongoDB支持复杂的对象数组嵌套查询。 并且可以通过model代码在数据库里自动生成对应的表与索引。 因为mongoDB的动态类型特性,可以随时新建字段,改变字段类型等,直接修改Model里的属性即可。然而太过于动态的设计会造成维护困难。还是尽量要保证数据的一致性。 ### 表model: ```typescript /** * 数组成员类型 */ class TestTableSub { a = "" } //表名,对应与数据库中的表名 class TestTable { /** * id字段 */ _id = new ObjectID() @sql.index()//普通索引 age = 0; @sql.indexText()//全文索引 name: string = ""; //嵌套对象 s = {a: 1, b: 2}; //数组 arr = array(TestTableSub) arr2 = arrayString() } ``` 链接: ```typescript import * as cfg from "../config/config"; import {MongoConnect} from "dfv/src/db/MongoConnect"; export const db = { //mysql链接 connecter: new MongoConnect(cfg.mongo), //TestTable表,返回一个MongoBuilder对象 TestTable: ()=>new MongoBuilder(TestTable, db.connecter), } ``` 然后和mysql一样使用`db.TestTable()`就可以了,MongoBuilder会自动建表,建索引。 **查询并更新**: ```typescript await db.TestTable() .where(f => f.age.gt(0) | f.name.eq("") & (f.age.eq(2) | f.s.a.eq(2))) .updatePromise(f => f.arr.push({a: "aaa"}) & f.name.set("123") & f.age.inc(1) & f.arr2.pull(f => f.in(["0", "1"]))) ``` 以上代码生成了如下查询条件(就是将逻辑表达式转换为了抽象语法树): ```json { "$or": [ { "age": { "$gt": 0 } }, { "$and": [ { "name": "" }, { "$or": [ { "age": 2 }, { "s.a": 2 } ] } ] } ] } ``` 以及如下更新操作: ```json { "$push": { "arr": { "a": "aaa" } }, "$set": { "name": "123" }, "$inc": { "age": 1 }, "$pull": { "arr2": { "$in": [ "0", "1" ] } } } ``` **原始mongoDB操作:** 本框架里并没有把mongoDB的所有所有操作都包含进来,我们可以调用MongoBuilder里的collection()函数进行原始操作: ```typescript await (await db.TestTable().collection()).findOneAndDelete({ "aaa": 123, "bbb": {$gt: 0} }) ``` collection支持的所有操作参照: [https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html](https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html)。 ### 下一章:[4. tsx模板](4.tsx.md)