# zorm **Repository Path**: tszy/zorm ## Basic Information - **Project Name**: zorm - **Description**: readygo的子项目,依赖原生的sql语句,golang 轻量级orm. - **Primary Language**: Go - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 120 - **Created**: 2020-08-21 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # zorm #### 介绍 golang轻量级ORM,[readygo](https://gitee.com/chunanyong/readygo)子项目 [API文档](https://pkg.go.dev/gitee.com/chunanyong/zorm?tab=doc) 源码地址:https://gitee.com/chunanyong/zorm ``` go get gitee.com/chunanyong/zorm ``` * 基于原生sql语句编写,是[springrain](https://gitee.com/chunanyong/springrain)的精简和优化. * [自带代码生成器](https://gitee.com/chunanyong/readygo/tree/master/codegenerator) * 代码精简,总计2000行左右,注释详细,方便定制修改. * 支持事务传播,这是zorm诞生的主要原因 * 支持mysql,postgresql,oracle,mssql,sqlite * 支持数据库读写分离 * 更新性能zorm,gorm,xorm相当. 读取性能zorm比gorm,xorm快一倍 生产使用参考 [UserStructService.go](https://gitee.com/chunanyong/readygo/tree/master/permission/permservice) ## 测试用例 https://gitee.com/chunanyong/readygo/blob/master/test/testzorm/BaseDao_test.go ```go // zorm 使用原生的sql语句,没有对sql语法做限制.语句使用Finder作为载体 // 占位符统一使用?,zorm会根据数据库类型,自动替换占位符,例如postgresql数据库把?替换成$1,$2... // 为了保持数据库兼容性,分页语句必须有order by // zorm使用 ctx context.Context 参数实现事务传播,ctx从web层传递进来即可,例如gin的c.Request.Context() // zorm的事务操作需要显示使用zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})开启 ``` #### 数据库脚本和实体类 https://gitee.com/chunanyong/readygo/blob/master/test/testzorm/demoStruct.go 生成实体类或手动编写,建议使用代码生成器 https://gitee.com/chunanyong/readygo/tree/master/codegenerator ```go package testzorm import ( "time" "gitee.com/chunanyong/zorm" ) //建表语句 /* DROP TABLE IF EXISTS `t_demo`; CREATE TABLE `t_demo` ( `id` varchar(50) NOT NULL COMMENT '主键', `userName` varchar(30) NOT NULL COMMENT '姓名', `password` varchar(50) NOT NULL COMMENT '密码', `createTime` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `active` int(0) NOT NULL DEFAULT 1 COMMENT '是否有效(0否,1是)', PRIMARY KEY (`id`) ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '例子' ; */ //demoStructTableName 表名常量,方便直接调用 const demoStructTableName = "t_demo" // demoStruct 例子 type demoStruct struct { //引入默认的struct,隔离IEntityStruct的方法改动 zorm.EntityStruct //Id 主键 Id string `column:"id"` //UserName 姓名 UserName string `column:"userName"` //Password 密码 Password string `column:"password"` //CreateTime CreateTime time.Time `column:"createTime"` //Active 是否有效(0否,1是) Active int `column:"active"` //------------------数据库字段结束,自定义字段写在下面---------------// } //GetTableName 获取表名称 func (entity *demoStruct) GetTableName() string { return demoStructTableName } //GetPKColumnName 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称. func (entity *demoStruct) GetPKColumnName() string { return "id" } //newDemoStruct 创建一个默认对象 func newDemoStruct() demoStruct { demo := demoStruct{ //如果Id=="",保存时zorm会调用zorm.FuncGenerateStringID(),默认UUID字符串,也可以自己定义实现方式,例如 zorm.FuncGenerateStringID=funcmyId Id: zorm.FuncGenerateStringID(), UserName: "defaultUserName", Password: "defaultPassword", Active: 1, CreateTime: time.Now(), } return demo } ``` ## 测试用例即文档 ```go // testzorm 使用原生的sql语句,没有对sql语法做限制.语句使用Finder作为载体 // 占位符统一使用?,zorm会根据数据库类型,自动替换占位符,例如postgresql数据库把?替换成$1,$2... // 为了保持数据库兼容性,分页语句必须有order by // zorm使用 ctx context.Context 参数实现事务传播,ctx从web层传递进来即可,例如gin的c.Request.Context() // zorm的事务操作需要显示使用zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})开启 package testzorm import ( "context" "fmt" "testing" "time" "gitee.com/chunanyong/zorm" //00.引入数据库驱动 _ "github.com/go-sql-driver/mysql" ) //dbDao 代表一个数据库,如果有多个数据库,就对应声明多个DBDao var dbDao *zorm.DBDao // ctx默认应该有 web层传入,例如gin的c.Request.Context().这里只是模拟 var ctx = context.Background() //01.初始化DBDao func init() { //自定义zorm日志输出 //zorm.LogCalldepth = 4 //日志调用的层级 //zorm.FuncLogError = myFuncLogError //记录异常日志的函数 //zorm.FuncLogPanic = myFuncLogPanic //记录panic日志,默认使用ZormErrorLog实现 //zorm.FuncPrintSQL = myFuncPrintSQL //打印sql的函数 //dbDaoConfig 数据库的配置 dbDaoConfig := zorm.DataSourceConfig{ //DSN 数据库的连接字符串 DSN: "root:root@tcp(127.0.0.1:3306)/readygo?charset=utf8&parseTime=true", //DriverName 数据库驱动名称,和DBType对应,一个数据库可以有多个驱动(DriverName) DriverName: "mysql", //DBType 数据库类型(mysql,postgresql,oracle,mssql,sqlite),zorm判断方言的依据,一个数据库可以有多个驱动(DriverName) DBType: "mysql", //MaxOpenConns 数据库最大连接数 默认50 MaxOpenConns: 50, //MaxIdleConns 数据库最大空闲连接数 默认50 MaxIdleConns: 50, //ConnMaxLifetimeSecond 连接存活秒时间. 默认600(10分钟)后连接被销毁重建.避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) ConnMaxLifetimeSecond: 600, //PrintSQL 打印SQL.会使用FuncPrintSQL记录SQL PrintSQL: true, } // 根据dbDaoConfig创建dbDao, 一个数据库只执行一次,第一个执行的数据库为 defaultDao,后续zorm.xxx方法,默认使用的就是defaultDao dbDao, _ = zorm.NewDBDao(&dbDaoConfig) } //TestInsert 02.测试保存Struct对象 func TestInsert(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { //创建一个demo对象 demo := newDemoStruct() //保存对象,参数是对象指针.如果主键是自增,会赋值到对象的主键属性 _, err := zorm.Insert(ctx, &demo) //如果返回的err不是nil,事务就会回滚 return nil, err }) //标记测试失败 if err != nil { t.Errorf("错误:%v", err) } } //TestInsertEntityMap 03.测试保存EntityMap对象,用于不方便使用struct的场景,使用Map作为载体 func TestInsertEntityMap(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { //创建一个EntityMap,需要传入表名 entityMap := zorm.NewEntityMap(demoStructTableName) //设置主键名称 entityMap.PkColumnName = "id" //如果是自增序列,设置序列的值 //entityMap.PkSequence = "mySequence" //Set 设置数据库的字段值 //如果主键是自增或者序列,不要entityMap.Set主键的值 entityMap.Set("id", zorm.FuncGenerateStringID()) entityMap.Set("userName", "entityMap-userName") entityMap.Set("password", "entityMap-password") entityMap.Set("createTime", time.Now()) entityMap.Set("active", 1) //执行 _, err := zorm.InsertEntityMap(ctx, entityMap) //如果返回的err不是nil,事务就会回滚 return nil, err }) //标记测试失败 if err != nil { t.Errorf("错误:%v", err) } } //TestQuery 04.测试查询一个struct对象 func TestQuery(t *testing.T) { //声明一个对象的指针,用于承载返回的数据 demo := &demoStruct{} //构造查询用的finder finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo //finder = zorm.NewSelectFinder(demoStructTableName, "id,userName") // select id,userName from t_demo //finder = zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo //finder.Append 第一个参数是语句,后面的参数是对应的值,值的顺序要正确.语句统一使用?,zorm会处理数据库的差异 finder.Append("WHERE id=? and active in(?)", "41b2aa4f-379a-4319-8af9-08472b6e514e", []int{0, 1}) //执行查询 err := zorm.Query(ctx, finder, demo) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } //打印结果 fmt.Println(demo) } //TestQueryMap 05.测试查询map接收结果,用于不太适合struct的场景,比较灵活 func TestQueryMap(t *testing.T) { //构造查询用的finder finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo //finder.Append 第一个参数是语句,后面的参数是对应的值,值的顺序要正确.语句统一使用?,zorm会处理数据库的差异 finder.Append("WHERE id=? and active in(?)", "41b2aa4f-379a-4319-8af9-08472b6e514e", []int{0, 1}) //执行查询 resultMap, err := zorm.QueryMap(ctx, finder) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } //打印结果 fmt.Println(resultMap) } //TestQuerySlice 06.测试查询对象列表 func TestQuerySlice(t *testing.T) { //创建用于接收结果的slice list := make([]demoStruct, 0) //构造查询用的finder finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo //为了保证数据库迁移,分页语句必须要有order by finder.Append("order by id asc") //创建分页对象,查询完成后,page对象可以直接给前端分页组件使用 page := zorm.NewPage() page.PageNo = 1 //查询第1页,默认是1 page.PageSize = 20 //每页20条,默认是20 //执行查询 err := zorm.QuerySlice(ctx, finder, &list, page) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } //打印结果 fmt.Println("总条数:", page.TotalCount, " 列表:", list) } //TestQueryMapSlice 07.测试查询map列表,用于不方便使用struct的场景,一条记录是一个map对象 func TestQueryMapSlice(t *testing.T) { //构造查询用的finder finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo //为了保证数据库迁移,分页语句必须要有order by finder.Append("order by id asc") //创建分页对象,查询完成后,page对象可以直接给前端分页组件使用 page := zorm.NewPage() //执行查询 listMap, err := zorm.QueryMapSlice(ctx, finder, page) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } //打印结果 fmt.Println("总条数:", page.TotalCount, " 列表:", listMap) } //TestUpdateNotZeroValue 08.更新struct对象,只更新不为零值的字段.主键必须有值 func TestUpdateNotZeroValue(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { //声明一个对象的指针,用于更新数据 demo := &demoStruct{} demo.Id = "41b2aa4f-379a-4319-8af9-08472b6e514e" demo.UserName = "UpdateNotZeroValue" //更新 "sql":"UPDATE t_demo SET userName=? WHERE id=?","args":["UpdateNotZeroValue","41b2aa4f-379a-4319-8af9-08472b6e514e"] _, err := zorm.UpdateNotZeroValue(ctx, demo) //如果返回的err不是nil,事务就会回滚 return nil, err }) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } } //TestUpdate 09.更新struct对象,更新所有字段.主键必须有值 func TestUpdate(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { //声明一个对象的指针,用于更新数据 demo := &demoStruct{} demo.Id = "41b2aa4f-379a-4319-8af9-08472b6e514e" demo.UserName = "TestUpdate" _, err := zorm.Update(ctx, demo) //如果返回的err不是nil,事务就会回滚 return nil, err }) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } } //TestUpdateFinder 10.通过finder更新,zorm最灵活的方式,可以编写任何更新语句,甚至手动编写insert语句 func TestUpdateFinder(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { finder := zorm.NewUpdateFinder(demoStructTableName) // UPDATE t_demo SET //finder = zorm.NewDeleteFinder(demoStructTableName) // DELETE FROM t_demo //finder = zorm.NewFinder().Append("UPDATE").Append(demoStructTableName).Append("SET") // UPDATE t_demo SET finder.Append("userName=?,active=?", "TestUpdateFinder", 1).Append("WHERE id=?", "41b2aa4f-379a-4319-8af9-08472b6e514e") //更新 "sql":"UPDATE t_demo SET userName=?,active=? WHERE id=?","args":["TestUpdateFinder",1,"41b2aa4f-379a-4319-8af9-08472b6e514e"] _, err := zorm.UpdateFinder(ctx, finder) //如果返回的err不是nil,事务就会回滚 return nil, err }) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } } //TestUpdateEntityMap 11.更新一个EntityMap,主键必须有值 func TestUpdateEntityMap(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { //创建一个EntityMap,需要传入表名 entityMap := zorm.NewEntityMap(demoStructTableName) //设置主键名称 entityMap.PkColumnName = "id" //Set 设置数据库的字段值,主键必须有值 entityMap.Set("id", "41b2aa4f-379a-4319-8af9-08472b6e514e") entityMap.Set("userName", "TestUpdateEntityMap") //更新 "sql":"UPDATE t_demo SET userName=? WHERE id=?","args":["TestUpdateEntityMap","41b2aa4f-379a-4319-8af9-08472b6e514e"] _, err := zorm.UpdateEntityMap(ctx, entityMap) //如果返回的err不是nil,事务就会回滚 return nil, err }) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } } //TestDelete 12.删除一个struct对象,主键必须有值 func TestDelete(t *testing.T) { //需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚 _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { demo := &demoStruct{} demo.Id = "ae9987ac-0467-4fe2-a260-516c89292684" //删除 "sql":"DELETE FROM t_demo WHERE id=?","args":["ae9987ac-0467-4fe2-a260-516c89292684"] _, err := zorm.Delete(ctx, demo) //如果返回的err不是nil,事务就会回滚 return nil, err }) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } } //TestOther 13.其他的一些说明.非常感谢您能看到这一行 func TestOther(t *testing.T) { //场景1.多个数据库.通过对应数据库的dbDao,调用BindContextDBConnection函数,把这个数据库的连接绑定到返回的ctx上,然后把ctx传递到zorm的函数即可. newCtx, err := dbDao.BindContextDBConnection(ctx) if err != nil { //标记测试失败 t.Errorf("错误:%v", err) } finder := zorm.NewSelectFinder(demoStructTableName).Append("order by id ") //把新产生的newCtx传递到zorm的函数 list, _ := zorm.QueryMapSlice(newCtx, finder, nil) fmt.Println(list) //场景2.单个数据库的读写分离.设置读写分离的策略函数. zorm.FuncReadWriteStrategy = myReadWriteStrategy //场景3.如果是多个数据库,每个数据库还读写分离,按照 场景1 处理 } //单个数据库的读写分离的策略 rwType=0 read,rwType=1 write func myReadWriteStrategy(rwType int) *zorm.DBDao { //根据自己的业务场景,返回需要的读写dao,每次需要数据库的连接的时候,会调用这个函数 return dbDao } ``` ## 性能压测 测试代码:https://github.com/alphayan/goormbenchmark 指标说明 总时间,平均每次纳秒数,平均每次分配的内存,平均每次分配内存次数 更新性能zorm,gorm,xorm相当.读取性能zorm比gorm,xorm快一倍 ``` 2000 times - Insert zorm: 9.05s 4524909 ns/op 2146 B/op 33 allocs/op gorm: 9.60s 4800617 ns/op 5407 B/op 119 allocs/op xorm: 12.63s 6315205 ns/op 2365 B/op 56 allocs/op 2000 times - BulkInsert 100 row xorm: 23.89s 11945333 ns/op 253812 B/op 4250 allocs/op gorm: Don't support bulk insert - https://github.com/jinzhu/gorm/issues/255 zorm: Don't support bulk insert 2000 times - Update xorm: 0.39s 195846 ns/op 2529 B/op 87 allocs/op zorm: 0.51s 253577 ns/op 2232 B/op 32 allocs/op gorm: 0.73s 366905 ns/op 9157 B/op 226 allocs/op 2000 times - Read zorm: 0.28s 141890 ns/op 1616 B/op 43 allocs/op gorm: 0.45s 223720 ns/op 5931 B/op 138 allocs/op xorm: 0.55s 276055 ns/op 8648 B/op 227 allocs/op 2000 times - MultiRead limit 1000 zorm: 13.93s 6967146 ns/op 694286 B/op 23054 allocs/op gorm: 26.40s 13201878 ns/op 2392826 B/op 57031 allocs/op xorm: 30.77s 15382967 ns/op 1637098 B/op 72088 allocs/op ```