xorm 学习笔记

658 阅读11分钟

xorm 是一个简单但强大的 Go 语言 ORM 库。

快速开始

# 安装xorm库
go get xorm.io/xorm
# 根据操作的数据库下载相应驱动
# 下载操作mysql的驱动
go get github.com/go-sql-driver/mysql

创建引擎

所有操作均在 ORM引擎 上进行,所以需要先创建并配置好ORM引擎。XORM 支持 EngineEngine Group 两种引擎。

  • Engine 引擎 用于对单个数据库进行操作。
  • Engine Group 引擎用于对读写分离的数据库或者负载均衡的数据库进行操作。

Engine 引擎和 Engine Group 引擎的API基本相同,所有适用于 Engine 的 API 基本上都适用于 Engine Group,并且可以比较容易的从 Engine 迁移到 Engine Group

Engine

Engine 的参数与sql.Open参数相同,同样的也需要引入对应数据库的驱动包,相关驱动包可以在官网查看Databases/Drivers supported

// 没有引入会报错:sql: unknown driver "mysql" (forgotten import?)
import	_ "github.com/go-sql-driver/mysql"
...

engine, err := xorm.NewEngine("mysql", "root:123456@/test?charset=utf8mb4")
...

你也可以用 NewEngineWithParams, NewEngineWithDBNewEngineWithDialectAndDB 来创建引擎。 一般情况下如果只操作一个数据库,只需要创建一个 engine 即可。engine 是 GoRoutine 安全的。 创建完成 engine 之后,并没有立即连接数据库,此时可以通过 engine.Ping() 或者 engine.PingContext() 来进行数据库的连接测试是否可以连接到数据库。另外对于某些数据库有连接超时设置的,可以通过起一个定期Ping的Go程来保持连接鲜活。

Engine Group

Engine Group是xorm的主从读写分离解决方案,主要配置内容为主库和从库的配置,以及从库的负载策略。

创建Engine Group

xorm通过xorm.NewEngineGroup实现,但是有两种使用方式。

  • 第一种,通过dsn数组方式。所有Engine使用同一种数据库驱动。
conns := []string{
		"root:123456@/test?charset=utf8mb4",  // 第一个默认是master
		"root:123456@/test1?charset=utf8mb4", // 第二个开始都是slave
		"root:123456@/test2?charset=utf8mb4",
	}
eg, err := xorm.NewEngineGroup("mysql", conns)
  • 第二种,通过Engine创建。可以为Engine配置不同的数据库驱动。
master, err := xorm.NewEngine("mysql", "root:123456@/test?charset=utf8mb4")
if err != nil {
  panic(err)
}

slave1, err := xorm.NewEngine("mysql", "root:123456@/test1?charset=utf8mb4")
if err != nil {
  panic(err)
}

slave2, err := xorm.NewEngine("mysql", "root:123456@/test2?charset=utf8mb4")
if err != nil {
  panic(err)
}

slaves := []*xorm.Engine{slave1, slave2}
eg, err := xorm.NewEngineGroup(master, slaves)

创建完成 EngineGroup 之后,并没有立即连接数据库,此时可以通过 eg.Ping() 来进行数据库的连接测试是否可以连接到数据库,该方法会依次调用引擎组中每个Engine的Ping方法。另外对于某些数据库有连接超时设置的,可以通过起一个定期Ping的Go程来保持连接鲜活。EngineGroup 可以通过 eg.Close() 来手动关闭,但是一般情况下可以不用关闭,在程序退出时会自动关闭。

设置 Engine 组策略

通过 xorm.NewEngineGroup 创建 EngineGroup 时,第三个参数为 policies,我们可以通过该参数来指定 Slave 访问的负载策略。

eg, err := xorm.NewEngineGroup("mysql", conns, xorm.RandomPolicy())
// 或者
eg.SetPolicy(xorm.LeastConnPolicy())

xorm 中内置五种负载策略:

  • 随机访问负载策略 xorm.RandomPolicy
  • 权重随机访问负载策略 xorm.WeightRandomPolicy
  • 轮询访问负载策略 xorm.RoundRobinPolicy
  • 权重轮询访问负载策略 xorm.WeightRoundRobinPolicy
  • 最小连接数访问负载策略 xorm.LeastConnPolicy 当然,也可以通过实现GroupPolicy接口,自定义策略
type GroupPolicy interface {
	Slave(*EngineGroup) *Engine
}

日志

日志是一个接口,通过设置日志,可以显示SQL,警告以及错误等,默认的显示级别为 INFO。

  • engine.ShowSQL(true),则会在控制台打印出生成的SQL语句;
  • engine.Logger().SetLevel(log.LOG_DEBUG),则会在控制台打印调试及以上的信息;
  • engine.SetLogger(log.NewSimpleLogger(f)),修改日志输出目标,NewSimpleLogger(w io.Writer)接收一个io.Writer接口来将数据写入到对应的设施中。

连接池

engine内部支持连接池接口和对应的函数。

  • 如果需要设置连接池的空闲数大小,可以使用 engine.SetMaxIdleConns() 来实现。
  • 如果需要设置最大打开连接数,则可以使用 engine.SetMaxOpenConns() 来实现。
  • 如果需要设置连接的最大生存时间,则可以使用 engine.SetConnMaxLifetime() 来实现。

表操作

表定义

xorm使用结构体进行表结构定义。 表名的设置主要有两种方式:

  • Mapper映射规则,在没有TableName方法时启用。xorm内置了三种Mapper规则。
    • SnakeMapper (默认) 支持struct为驼峰式命名,表结构为下划线命名之间的转换,这个是默认的Maper;
    • SameMapper 支持结构体名称和对应的表名称以及结构体field名称与对应的表字段名称相同的命名;
    • GonicMapper 和SnakeMapper很类似,但是对于特定词支持更好,比如ID会翻译成id而不是i_d。 可以通过engine.SetMapper(names.GonicMapper{})方式设置,此外也可以自己实现Mapper以满足特殊的需求。 默认情况下,映射规则在表名和字段名中共用,如果需要区别开可以使用以下方式:
    engine.SetTableMapper(names.SameMapper{})
    engine.SetColumnMapper(names.SnakeMapper{})
    
    此外通过 names.NewPrefixMapper(names.SnakeMapper{}, "prefix") 可以创建一个在 Mapper 的基础上在命名中添加统一的前缀。
  • 结构体实现TableName() string方法,优先使用。

字段Tag规则:

  1. 字段名 字段的名称,可选,如不写,则自动根据field名字和转换规则命名。如果与关键字冲突需要使用单引号包裹,否则直接使用即可。
    Name string  `xorm:"varchar(25) notnull unique 'usr_name' comment('姓名')"`
    
  2. 关键字
关键字说明
pk是否是主键,如果有多个字段都使用了此标记,则为复合主键
当前支持30多种字段类型字段类型
autoincr是否是自增
[ not ]null 或 notnull是否可以为空
unique或unique(uniquename)括号中为联合唯一索引的名字,如果多个字段uniquename相同,则这些uniquename相同的字段组成联合唯一索引
index或index(indexname)括号中为联合索引的名字,如果多个字段indexname相同,则这些indexname相同的字段组成联合索引
extends应用于员结构体之上,表示此结构体的所有成员也映射到数据库中,extends可加载无限级
-这个Field将不进行字段映射
->这个Field将只写入到数据库而不从数据库读取
<-这个Field将只从数据库读取,而不写入到数据库
created这个Field将在Insert时自动赋值为当前时间
updated这个Field将在Insert或Update时自动赋值为当前时间
deleted这个Field将在Delete时设置为当前时间,并且当前记录不删除
version这个Field将会在insert时默认为1,每次更新自动加1
default0或default(0) 设置默认值,紧跟的内容如果是Varchar等需要加上单引号
json表示内容将先转成Json格式,然后存储到数据库中,数据库中的字段类型可以为Text或者二进制
comment设置字段的注释(当前仅支持mysql)

单主键当前支持int32,int,int64,uint32,uint,uint64,string这7种Go的数据类型,复合主键支持这7种Go的数据类型的组合。

建表

建表可以使用Sync方法, Sync2 已弃用。

Sync 不会对已有表结构的字段进行任何的更改、删除操作,而只会进行增加字段操作。

err := engine.Sync(new(User), new(Group))

老版本如果发现有字符删减,会打印Warn级别日志进行提示。新版本则不会,如果有需求,可以使用SyncWithOptions方法开启警告。

// 开启警告
err = engine.SyncWithOptions(xorm.SyncOptions{
		WarnIfDatabaseColumnMissed: true,
		IgnoreConstrains:           false,
		IgnoreIndices:              false,
		IgnoreDropIndices:          false,
	}, new(User), new(Group))

新版本Sync方法会一次性拉取所有数据库的表数据,与注入结构体对比。推荐将所有表结构一次性注入Sync方法,以提高同步效率。

// 不推荐
engine.Sync(new(User))
engine.Sync(new(Group))

// 推荐
engine.Sync(new(User), new(Group))

增删改查

插入数据

使用engine.Insert()方法,可以插入单条数据,也可以批量插入多条数据:

// 单挑插入,会更新user中的id字段
user := &User{Name: "zz", Salt: "salt", Age: 18, Level: 2, Passwd: "12345"}
affect, err := engine.Insert(user)

// 批量插入,不会修改id字段
users := make([]*User, 0, 6)
users = append(users,
  &User{Name: "zz", Salt: "salt", Age: 18, Level: 2, Passwd: "12345"},
  &User{Name: "lj", Salt: "salt", Age: 1, Level: 1, Passwd: "12345"},
  &User{Name: "sd", Salt: "salt", Age: 8, Level: 6, Passwd: "12345"},
  &User{Name: "fs", Salt: "salt", Age: 2, Level: 5, Passwd: "12345"},
  &User{Name: "qw", Salt: "salt", Age: 8, Level: 4, Passwd: "12345"},
  &User{Name: "ee", Salt: "salt", Age: 7, Level: 3, Passwd: "12345"},
)
affect, err = engine.Insert(&users)

Insert传入切片时,数据分成多条SQL进行插入,如果其中一条插入时出错,可能造成后续数据不能继续插入。

Insert传入切片指针时,数据会拼接为一条SQl执行能够插入,由于各数据库SQL长度限制,需要注意不要使用太长的切片内容。如果数据量太大,需要分片进行插入。官方建议每条长度不要超过150。

Insert支持多个不同类型的参数。可以进行多张表的插入。

查询

查询主要使用GetFind方法,Get查找一个结果,Find则为多个结果,结果会保存的出入参数中。

// 第一个返回值为是否查询到结果
ok, err := e.ID(2).Get(u)

err = e.Find(&us)

查询参数

  • Where("a = ? AND b = ?", 1, 2): where 条件
  • Alias("o").Where("o.name = ?", name): 表别名
  • .Where(...).And(...): AND
  • .Asc("id") .Desc("time") OrderBy(string):结果排序,可以组合使用
  • .ID(1):查询主键
  • Or(interface{}, …interface{}): OR条件
  • Select(string):Select内容
  • SQL(string, …interface{}):SQL内容
  • .In("cloumn", 1, 2, 3): IN
  • .Cols("age", "name"): 查询或更新的字段
  • .AllCols():查询或更新所有字段
  • MustCols(…string):某些字段必须更新,一般与Update配合使用。
  • .Omit("age", "gender"): 和cols相反,此函数指定排除某些指定的字段。注意:此方法和Cols方法不可同时使用。
  • .Distinct("age", "department"): 归类去重
  • Table(nameOrStructPtr interface{}):传入表名称或者结构体指针,如果传入的是结构体指针,则按照IMapper的规则提取出表名
  • Limit(int, …int):限制获取的数目,第一个参数为条数,第二个参数表示开始位置,如果不传则为0
  • Top(int):相当于Limit(int, 0)
  • Join(string,interface{},string)
  • GroupBy(string)
  • Having(string) 其他方法
  • Count: 统计数据数量
  • Exist: 判断某个记录是否存在,比Get,Exist性能更好。
  • Iterate:Iterate方法提供逐条执行查询到的记录的方法
  • Rows: 同样供逐条执行查询到的记录的方法,不过Rows更加灵活好用。
  • Sum:求和数据可以使用Sum, SumInt, SumsSumsInt 四个方法

删除

affected, err := engine.Where("name = ?", "lzy").Delete(&User{})

engine.Delete(new(User)) 不会进行删除操作,需要使用engine.Where("1=1").Delete(new(User))

使用软删除,需要在xorm标记中使用deleted标记

DeletedAt time.Time `xorm:"deleted"`

修改

更新通过engine.Update()实现,可以传入结构指针或map[string]interface{}。对于传入结构体指针的情况,xorm只会更新非空的字段。如果一定要更新空字段,需要使用Cols()方法显示指定更新的列。使用Cols()方法指定列后,即使字段为空也会更新

affect, err := engine.ID(1).Update(&model.User{Name: "jack"})
affect, err := engine.Table(&model.User{}).ID(2).Update(map[string]interface{}{"age": 16})
affect, err := engine.ID(3).Cols("level").Update(&model.User{Name: "smith", Level: 2})

执行原始SQL

查询

sql := "select * from userinfo"
results, err := engine.Query(sql)

返回值 results[]map[string][]byte Query 的参数也允许传入 *builder.Buidler 对象

此外还可以使用QueryInterface(返回类型[]map[string]interface{}) 和 QueryString(返回类型[]map[string]string

执行命令

sql = "update `userinfo` set username=? where id=?"
res, err := engine.Exec(sql, "xiaolun", 1)

事务

当使用事务处理时,需要创建 Session 对象。在进行事务处理时,可以混用 ORM 方法和 SQL 方法。

session := engine.NewSession()
defer session.Close()

// add Begin() before any action
if err := session.Begin(); err != nil {
    return err
}

user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()}
if _, err := session.Insert(&user1); err != nil {
    return err
}
user2 := Userinfo{Username: "yyy"}
if _, err = session.Where("id = ?", 2).Update(&user2); err != nil {
    return err
}

if _, err = session.Exec("delete from userinfo where username = ?", user2.Username); err != nil {
    return err
}

// add Commit() after all actions
return session.Commit()

reverse 工具

安装

go get xorm.io/reverse
go install xorm.io/reverse

使用默认模板

reverse -f example/custom.yml
kind: reverse
name: test
source:
  database: mysql
  conn_str: 'root:123456@/test?charset=utf8mb4'
targets:
- type: codes
  language: golang
  output_dir: ./models

生成的go文件 ./models/models.go

package models

import (
	"time"
)

type MyUser struct {
	ID      int64     `xorm:"not null pk autoincr BIGINT(20)"`
	Name    string    `xorm:"VARCHAR(255)"`
	Salt    string    `xorm:"VARCHAR(255)"`
	Age     int       `xorm:"INT(11)"`
	Level   int       `xorm:"INT(11)"`
	Passwd  string    `xorm:"VARCHAR(200)"`
	Created time.Time `xorm:"DATETIME"`
	Updated time.Time `xorm:"DATETIME"`
}

更复杂的配置

# complex.yaml
kind: reverse
name: mydb
source:
  database: mysql
  conn_str: 'root:123456@/test?charset=utf8mb4'
targets:
- type: codes
  # include_tables: # 需要导出的表
  #   - a
  #   - b
  # exclude_tables: # 需要排除的表
  #   - c
  table_mapper: snake # 表名映射规则
  column_mapper: snake # 字段映射规则
  table_prefix: "my_" # 表名前缀  默认会去除掉的部分
  multiple_files: true # 生产多份文件
  language: golang
  template: | # 代码模板 使用的是golang模板语法
    package models

    {{$ilen := len .Imports}}
    {{if gt $ilen 0}}
    import (
      {{range .Imports}}"{{.}}"{{end}}
    )
    {{end}}

    {{range .Tables}}
    type {{TableMapper .Name}} struct {
    {{$table := .}}
    {{range .ColumnsSeq}}{{$col := $table.GetColumn .}}	{{ColumnMapper $col.Name}}	{{Type $col}} `{{Tag $table $col}}`
    {{end}}
    }

    func (m *{{TableMapper .Name}}) TableName() string {
    	return "{{$table.Name}}"
    }
    {{end}}
  template_path: ./template/goxorm.tmpl # 模板文件路径  优先级低于template字段
  output_dir: ./models # 生产文件路径

生成的go文件 ./models/user.go

package models

import (
	"time"
)

type User struct {
	ID      int64     `xorm:"not null pk autoincr BIGINT(20)"`
	Name    string    `xorm:"VARCHAR(255)"`
	Salt    string    `xorm:"VARCHAR(255)"`
	Age     int       `xorm:"INT(11)"`
	Level   int       `xorm:"INT(11)"`
	Passwd  string    `xorm:"VARCHAR(200)"`
	Created time.Time `xorm:"DATETIME"`
	Updated time.Time `xorm:"DATETIME"`
}

func (m *User) TableName() string {
	return "user"
}

参考

Go 每日一库之 xorm
XORM - eXtra ORM for Go