borm

package module
v0.0.0-...-a775a36 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 7, 2020 License: MIT Imports: 14 Imported by: 0

README

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 备注
易用性 无需指定类型 :white_check_mark: :x: :x: borm在tag中无需低频的DDL
无需指定model :white_check_mark: :x: :x: gorm/xorm改操作需提供“模版”
无需指定主键 :white_check_mark: :x: :x: gorm/xorm易误操作,如删/改全表
学习成本低 :white_check_mark: :x: :x: 会SQL就会用borm
可复用原生连接 :white_check_mark: :x: :x: borm重构成本极小
全类型转换 :white_check_mark: maybe :x: 杜绝类型转换的抛错
复用查询命令 :white_check_mark: :x: :x: borm批量和单条使用同一个函数
可测试性 自mock :white_check_mark: :x: :x: 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报告
  • 事务相关支持
  • 联合查询
  • 匿名组合问题
  • 连接池
  • 读写分离

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func And

func And(conds ...interface{}) *ormCondEx

And .

func Between

func Between(field string, i interface{}, j interface{}) *ormCond

Between .

func BormMock

func BormMock(tbl, fun, caller, file, pkg string, data interface{}, ret int, err error)

BormMock .

func BormMockFinish

func BormMockFinish() error

BormMockFinish .

func Cond

func Cond(c string, args ...interface{}) *ormCond

Cond .

func Eq

func Eq(field string, i interface{}) *ormCond

Eq .

func Fields

func Fields(fields ...string) *fieldsItem

Fields .

func ForceIndex

func ForceIndex(idx string) *forceIndexItem

ForceIndex

func GroupBy

func GroupBy(fields ...string) *groupByItem

GroupBy .

func Gt

func Gt(field string, i interface{}) *ormCond

Gt .

func Gte

func Gte(field string, i interface{}) *ormCond

Gte .

func Having

func Having(conds ...interface{}) *havingItem

Having .

func In

func In(field string, args ...interface{}) *ormCond

In .

func Like

func Like(field string, pattern string) *ormCond

Like .

func Limit

func Limit(i ...interface{}) *limitItem

Limit .

func Lt

func Lt(field string, i interface{}) *ormCond

Lt .

func Lte

func Lte(field string, i interface{}) *ormCond

Lte .

func Neq

func Neq(field string, i interface{}) *ormCond

Neq .

func OnDuplicateKeyUpdate

func OnDuplicateKeyUpdate(keyVals V) *onDuplicateKeyUpdateItem

OnDuplicateKeyUpdate .

func Or

func Or(conds ...interface{}) *ormCondEx

Or .

func OrderBy

func OrderBy(orders ...string) *orderByItem

OrderBy .

func Where

func Where(conds ...interface{}) *whereItem

Where .

Types

type BormDBIFace

type BormDBIFace interface {
	QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
}

type BormItem

type BormItem interface {
	Type() int
	BuildSQL(*strings.Builder)
	BuildArgs(*[]interface{})
}

type BormTable

type BormTable struct {
	DB   BormDBIFace
	Name string
	Cfg  Config
	// contains filtered or unexported fields
}

func Table

func Table(db BormDBIFace, name string, ctx ...context.Context) *BormTable

Table .

func (*BormTable) BuildInsertSQL

func (t *BormTable) BuildInsertSQL(prefix string, objs interface{}, args ...BormItem) (sql string, stmtArgs []interface{}, err error)

func (*BormTable) BuildUpdateSQL

func (t *BormTable) BuildUpdateSQL(obj interface{}, args ...BormItem) (sql string, stmtArgs []interface{}, err error)

func (*BormTable) Debug

func (t *BormTable) Debug() *BormTable

Debug .

func (*BormTable) Delete

func (t *BormTable) Delete(args ...BormItem) (int, error)

func (*BormTable) Insert

func (t *BormTable) Insert(objs interface{}, args ...BormItem) (int, error)

func (*BormTable) InsertIgnore

func (t *BormTable) InsertIgnore(objs interface{}, args ...BormItem) (int, error)

func (*BormTable) ReplaceInto

func (t *BormTable) ReplaceInto(objs interface{}, args ...BormItem) (int, error)

func (*BormTable) Reuse

func (t *BormTable) Reuse() *BormTable

Reuse .

func (*BormTable) Select

func (t *BormTable) Select(res interface{}, args ...BormItem) (int, error)

func (*BormTable) ToTimestamp

func (t *BormTable) ToTimestamp() *BormTable

func (*BormTable) Update

func (t *BormTable) Update(obj interface{}, args ...BormItem) (int, error)

func (*BormTable) UseNameWhenTagEmpty

func (t *BormTable) UseNameWhenTagEmpty() *BormTable

type Config

type Config struct {
	Debug               bool
	Reuse               bool
	UseNameWhenTagEmpty bool
	ToTimestamp         bool
}

Config .

type DataBindingItem

type DataBindingItem struct {
	SQL  string
	Cols []interface{}
	Type reflect2.Type
	Elem interface{}
}

DataBindingItem .

type MockMatcher

type MockMatcher struct {
	Tbl    string
	Func   string
	Caller string
	File   string
	Pkg    string
	Data   interface{}
	Ret    int
	Err    error
}

MockMatcher .

type U

type U string

U - an alias string type for update to support `x=x+1`

type V

type V map[string]interface{}

V - an alias object value type

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL