Skip to content
 
 

Latest commit

 

History

155 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

borm

license Go Report Card Build Status codecov FOSSA Status

🏎️ 更好的ORM库 (Better ORM library that is simple, fast and self-mockable for Go)

目标

  • 易用:SQL-Like(一把梭:One-Line-CRUD)
  • KISS:保持小而美(不做大而全)
  • 通用:支持struct,pb,map和基本类型
  • 可测:支持自mock(因为参数作返回值,大部分mock框架不支持)
    • 非测试向的library不是好library
  • As-Is:尽可能不作隐藏设定,防止误用
  • 解决核心痛点:
    • 手撸SQL难免有错,组装数据太花时间
    • time.Time无法直接读写的问题
    • SQL函数结果无法直接Scan
    • db操作无法方便的Mock
    • QueryRow的sql.ErrNoRows问题
    • 直接替换系统自带Scanner,完整接管数据读取的类型转换
  • 核心原则:
    • 别像使用其他orm那样把一个表映射到一个model
    • (在borm里可以用Fields过滤器做到)
    • 尽量保持简单把一个操作映射一个model吧!
  • 其他优点:
    • 更自然的where条件(仅在需要加括号时添加,对比gorm)
    • In操作接受各种类型slice,并且单元素时转成Equal操作
    • 从其他orm库迁移无需修改历史代码,无侵入性修改

特性矩阵

下面是和一些主流orm库的对比(请不吝开issue勘误)

borm (me) gorm xorm 备注
易用性 无需指定类型 borm在tag中无需低频的DDL
无需指定model gorm/xorm改操作需提供“模版”
无需指定主键 gorm/xorm易误操作,如删/改全表
学习成本低 会SQL就会用borm
可复用原生连接 borm重构成本极小
全类型转换 maybe 杜绝类型转换的抛错
复用查询命令 borm批量和单条使用同一个函数
可测试性 自mock borm非常便于单元测试
性能 较原生耗时 <=1x 2~3x 2~3x xorm使用prepare模式会再慢2~3x
反射 reflect2 reflect reflect borm零使用ValueOf

快速入门

  1. 引入包

    import b "github.com/orca-zhang/borm"
  2. 定义Table对象

    t := b.Table(d.DB, "t_usr")
    
    t1 := b.Table(d.DB, "t_usr", ctx)
  • d.DB是支持Exec/Query/QueryRow的数据库连接对象
  • t_usr可以是表名,或者是嵌套查询语句
  • ctx是需要传递的Context对象,默认不传为context.Background()
  1. (可选)定义model对象

    // Info 默认未设置borm tag的字段不会取
    type Info struct {
       ID   int64  `borm:"id"`
       Name string `borm:"name"`
       Tag  string `borm:"tag"`
    }
    
    // 调用t.UseNameWhenTagEmpty(),可以用未设置borm tag的字段名本身作为待获取的db字段
  2. 执行操作

  • CRUD接口返回值为 (影响的条数,错误)

  • 类型Vmap[string]interface{}的缩写形式,参考gin.H

  • 插入

    // o可以是对象/slice/ptr slice
    n, err = t.Insert(&o)
    n, err = t.InsertIgnore(&o)
    n, err = t.ReplaceInto(&o)
    
    // 只插入部分字段(其他使用缺省)
    n, err = t.Insert(&o, b.Fields("name", "tag"))
    
    // 解决主键冲突
    n, err = t.Insert(&o, b.Fields("name", "tag"),
       b.OnDuplicateKeyUpdate(b.V{
          "name": "new_name",
          "age":  b.U("age+1"), // 使用b.U来处理非变量更新
       }))
  • 查询

    // o可以是对象/slice/ptr slice
    n, err := t.Select(&o, 
       b.Where("name = ?", name), 
       b.GroupBy("id"), 
       b.Having(b.Gt("id", 0)), 
       b.OrderBy("id", "name"), 
       b.Limit(1))
    
    // 使用基本类型+Fields获取条目数(n的值为1,因为结果只有1条)
    var cnt int64
    n, err = t.Select(&cnt, b.Fields("count(1)"), b.Where("name = ?", name))
    
    // 还可以支持数组
    var ids []int64
    n, err = t.Select(&ids, b.Fields("id"), b.Where("name = ?", name))
    
    // 可以强制索引
    n, err = t.Select(&ids, b.Fields("id"), b.ForceIndex("idx_xxx"), b.Where("name = ?", name))
  • 更新

    // o可以是对象/slice/ptr slice
    n, err = t.Update(&o, b.Where(b.Eq("id", id)))
    
    // 使用map更新
    n, err = t.Update(b.V{
          "name": "new_name",
          "tag":  "tag1,tag2,tag3",
          "age":  b.U("age+1"), // 使用b.U来处理非变量更新
       }, b.Where(b.Eq("id", id)), b.Limit(1))
    
    // 使用map更新部分字段
    n, err = t.Update(b.V{
          "name": "new_name",
          "tag":  "tag1,tag2,tag3",
       }, b.Fields("name"), b.Where(b.Eq("id", id)), b.Limit(1))
    
    n, err = t.Update(&o, b.Fields("name"), b.Where(b.Eq("id", id)), b.Limit(1))
  • 删除

    // 根据条件删除
    n, err = t.Delete(b.Where("name = ?", name))
    
    // 根据条件删除部分条数
    n, err = t.Delete(b.Where(b.Eq("id", id)), b.Limit(1))
  • 可变条件

    conds := []interface{}{b.Cond("1=1")} // 防止空where条件
    if name != "" {
       conds = append(conds, b.Eq("name", name))
    }
    if id > 0 {
       conds = append(conds, b.Eq("id", id))
    }
    // 执行查询操作
    n, err := t.Select(&o, b.Where(conds...))
  • 联表查询(临时)

    type Info struct {
       ID   int64  `borm:"t_usr.id"` // 字段定义加表名
       Name string `borm:"t_usr.name"`
       Tag  string `borm:"t_tag.tag"`
    }
    
    t := b.Table(d.DB, "t_usr join t_tag on t_usr.id=t_tag.id") // 表名用join语句
    
    var o Info
    n, err := t.Select(&o, b.Where(b.Eq("t_usr.id", id))) // 条件加上表名
  • 获取插入的自增id

    // 首先需要数据库有一个自增ID的字段
    type Info struct {
       BormLastId int64 // 添加一个名为BormLastId的整型字段
       Name       string `borm:"name"`
       Age        string `borm:"age"`
    }
    
    o := Info{
       Name: "OrcaZ",
       Age:  30,
    }
    n, err = t.Insert(&o)
    
    id := o.BormLastId // 获取到插入的id
  • 正在使用其他orm框架(新的接口先切过来吧)

    // [gorm] db是一个*gorm.DB
    t := b.Table(db.DB(), "tbl")
    
    // [xorm] db是一个*xorm.EngineGroup
    t := b.Table(db.Master().DB().DB, "tbl")
    // or
    t := b.Table(db.Slave().DB().DB, "tbl")

其他细节

Table的选项

选项 说明
Debug 打印sql语句
Reuse 根据调用位置复用sql和存储方式
UseNameWhenTagEmpty 用未设置borm tag的字段名本身作为待获取的db字段
ToTimestamp 调用Insert时,使用时间戳,而非格式化字符串

选项使用示例:

n, err = t.Debug().Insert(&o)

n, err = t.ToTimestamp().Insert(&o)

Where

示例 说明
Where("id=? and name=?", id, name) 常规格式化版本
Where(Eq("id", id), Eq("name", name)...) 默认为and连接
Where(And(Eq("x", x), Eq("y", y), Or(Eq("x", x), Eq("y", y)...)...) And & Or

预置Where条件

名称 示例 说明
逻辑与 And(...) 任意个参数,只接受下方的关系运算子
逻辑或 Or(...) 任意个参数,只接受下方的关系运算子
普通条件 Cond("id=?", id) 参数1为格式化字符串,后面跟占位参数
相等 Eq("id", id) 两个参数,id=?
不相等 Neq("id", id) 两个参数,id<>?
大于 Gt("id", id) 两个参数,id>?
大于等于 Gte("id", id) 两个参数,id>=?
小于 Lt("id", id) 两个参数,id<?
小于等于 Lte("id", id) 两个参数,id<=?
在...之间 Between("id", start, end) 三个参数,在start和end之间
近似 Like("name", "x%") 两个参数,name like "x%"
多值选择 In("id", ids) 两个参数,ids是基础类型的slice,slice只有1个元素会转化成Eq

GroupBy

示例 说明
GroupBy("id", "name"...) -

Having

示例 说明
Having("id=? and name=?", id, name) 常规格式化版本
Having(Eq("id", id), Eq("name", name)...) 默认为and连接
Having(And(Eq("x", x), Eq("y", y), Or(Eq("x", x), Eq("y", y)...)...) And & Or

OrderBy

示例 说明
OrderBy("id desc", "name asc"...) -

Limit

示例 说明
Limit(1) 分页大小为1
Limit(0, 100) 偏移位置为0,分页大小为100

OnDuplicateKeyUpdate

示例 说明
OnDuplicateKeyUpdate(V{"name": "new"}) 解决主键冲突的更新

ForceIndex

示例 说明
ForceIndex("idx_biz_id") 解决索引选择性差的问题

如何mock

mock步骤:

  • 调用BormMock指定需要mock的操作
  • 使用BormMockFinish检查是否命中mock

说明:

  • 前五个参数分别为tbl, fun, caller, file, pkg

    • 设置为空默认为匹配

    • 支持通配符'?'和'*',分别代表匹配一个字符和多个字符

    • 不区分大小写

      参数 名称 说明
      tbl 表名 数据库的表名
      fun 方法名 Select/Insert/Update/Delete
      caller 调用方方法名 需要带包名
      file 文件名 使用处所在文件路径
      pkg 包名 使用处所在的包名
  • 后三个参数分别为返回的数据返回的影响条数错误

  • 只能在测试文件中使用

使用示例:

待测函数:

   package x

   func test(db *sql.DB) (X, int, error) {
      var o X
      tbl := b.Table(db, "tbl")
      n, err := tbl.Select(&o, b.Where("`id` >= ?", 1), b.Limit(100))
      return o, n, err
   }

x.test方法中查询tbl的数据,我们需要mock数据库的操作

   // 必须在_test.go里面设置mock
   // 注意调用方方法名需要带包名
   b.BormMock("tbl", "Select", "*.test", "", "", &o, 1, nil)

   // 调用被测试函数
   o1, n1, err := test(db)

   So(err, ShouldBeNil)
   So(n1, ShouldEqual, 1)
   So(o1, ShouldResemble, o)

   // 检查是否全部命中
   err = b.BormMockFinish()
   So(err, ShouldBeNil)

待完成

  • Select存储到map
  • Insert从map读
  • Insert/Update支持非指针类型
  • Benchmark报告
  • 事务相关支持
  • 联合查询
  • 匿名组合问题
  • 连接池
  • 读写分离

About

【🔥今日热门】🏎️ 更好的ORM库 (Better ORM library that is simple, fast and self-mockable for Go)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages