简介

Go 标准库提供的数据库接口database/sql比较底层,使用它来操作数据库非常繁琐,而且容易出错。因而社区开源了不少第三方库,如上一篇文章中的sqlc工具,还有各式各样的 ORM (Object Relational Mapping,对象关系映射库),如gormxorm。本文介绍xormxorm是一个简单但强大的 Go 语言 ORM 库,使用它可以大大简化我们的数据库操作。

快速使用

先安装:

  1. $ go get xorm.io/xorm

由于需要操作具体的数据库(本文中我们使用 MySQL),需要安装对应的驱动:

  1. $ go get github.com/go-sql-driver/mysql

使用:

  1. package main
  2. import (
  3. "log"
  4. "time"
  5. _ "github.com/go-sql-driver/mysql"
  6. "xorm.io/xorm"
  7. )
  8. type User struct {
  9. Id int64
  10. Name string
  11. Salt string
  12. Age int
  13. Passwd string `xorm:"varchar(200)"`
  14. Created time.Time `xorm:"created"`
  15. Updated time.Time `xorm:"updated"`
  16. }
  17. func main() {
  18. engine, err := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  19. if err != nil {
  20. log.Fatal(err)
  21. }
  22. err = engine.Sync2(new(User))
  23. if err != nil {
  24. log.Fatal(err)
  25. }
  26. }

使用xorm来操作数据库,首先需要使用xorm.NewEngine()创建一个引擎。该方法的参数与sql.Open()参数相同。

上面代码中,我们演示了xorm的一个非常实用的功能,将数据库中的表与对应 Go 代码中的结构体做同步。初始状态下,数据库test中没有表user,调用Sync2()方法会根据User的结构自动创建一个user表。执行后,通过describe user查看表结构:

每日一库之38:xorm - 图1

如果表user已经存在,Sync()方法会对比User结构与表结构的不同,对表做相应的修改。我们给User结构添加一个Level字段:

  1. type User struct {
  2. Id int64
  3. Name string
  4. Salt string
  5. Age int
  6. Level int
  7. Passwd string `xorm:"varchar(200)"`
  8. Created time.Time `xorm:"created"`
  9. Updated time.Time `xorm:"updated"`
  10. }

再次执行这个程序后,用describe user命令查看表结构:

每日一库之38:xorm - 图2

发现表中多了一个level字段。

此修改只限于添加字段。删除表中已有的字段会带来比较大的风险。如果我们User结构的Salt字段删除,然后执行程序。出现下面错误:

  1. [xorm] [warn] 2020/05/07 22:44:38.528784 Table user has column salt but struct has not related field

数据库操作

查询&统计

xorm提供了几个查询和统计方法,Get/Exist/Find/Iterate/Count/Rows/Sum。下面逐一介绍。

为了代码演示方便,我在user表中插入了一些数据:

每日一库之38:xorm - 图3

后面的代码为了简单起见,忽略了错误处理,实际使用中不要漏掉!

Get

Get()方法用于查询单条数据,并使用返回的字段为传入的对象赋值:

  1. type User struct {
  2. Id int64
  3. Name string
  4. Salt string
  5. Age int
  6. Passwd string `xorm:"varchar(200)"`
  7. Created time.Time `xorm:"created"`
  8. Updated time.Time `xorm:"updated"`
  9. }
  10. func main() {
  11. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  12. user1 := &User{}
  13. has, _ := engine.ID(1).Get(user1)
  14. if has {
  15. fmt.Printf("user1:%v\n", user1)
  16. }
  17. user2 := &User{}
  18. has, _ = engine.Where("name=?", "dj").Get(user2)
  19. if has {
  20. fmt.Printf("user2:%v\n", user2)
  21. }
  22. user3 := &User{Id: 5}
  23. has, _ = engine.Get(user3)
  24. if has {
  25. fmt.Printf("user3:%v\n", user3)
  26. }
  27. user4 := &User{Name: "pipi"}
  28. has, _ = engine.Get(user4)
  29. if has {
  30. fmt.Printf("user4:%v\n", user4)
  31. }
  32. }

上面演示了 3 种使用Get()的方式:

  • 使用主键:engine.ID(1)查询主键(即id)为 1 的用户;
  • 使用条件语句:engine.Where("name=?", "dj")查询name = "dj"的用户;
  • 使用对象中的非空字段:user3设置了Id字段为 5,engine.Get(user3)查询id = 5的用户;user4设置了字段Name"pipi"engine.Get(user4)查询name = "pipi"的用户。

运行程序:

  1. user1:&{1 dj salt 18 12345 2020-05-08 21:12:11 +0800 CST 2020-05-08 21:12:11 +0800 CST}
  2. user2:&{1 dj salt 18 12345 2020-05-08 21:12:11 +0800 CST 2020-05-08 21:12:11 +0800 CST}
  3. user3:&{5 mxg salt 54 12345 2020-05-08 21:13:31 +0800 CST 2020-05-08 21:13:31 +0800 CST}
  4. user4:&{3 pipi salt 2 12345 2020-05-08 21:13:31 +0800 CST 2020-05-08 21:13:31 +0800 CST}

查询条件的使用不区分调用顺序,但是必须在Get()方法之前调用。实际上后面介绍的查询&统计方法也是如此,可以在调用实际的方法前添加一些过滤条件。除此之外xorm支持只返回指定的列(xorm.Cols())或忽略特定的列(xorm.Omit()):

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. user1 := &User{}
  4. engine.ID(1).Cols("id", "name", "age").Get(user1)
  5. fmt.Printf("user1:%v\n", user1)
  6. user2 := &User{Name: "pipi"}
  7. engine.Omit("created", "updated").Get(user2)
  8. fmt.Printf("user2:%v\n", user2)
  9. }

上面第一个查询使用Cols()方法指定只返回idnameage这 3 列,第二个查询使用Omit()方法忽略列createdupdated

另外,为了便于排查可能出现的问题,xorm提供了ShowSQL()方法设置将执行的 SQL 同时在控制台中输出:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. engine.ShowSQL(true)
  4. user := &User{}
  5. engine.ID(1).Omit("created", "updated").Get(user)
  6. fmt.Printf("user:%v\n", user)
  7. }

运行程序:

  1. [xorm] [info] 2020/05/08 21:38:29.349976 [SQL] SELECT `id`, `name`, `salt`, `age`, `passwd` FROM `user` WHERE `id`=? LIMIT 1 [1] - 4.0033ms
  2. user:&{1 dj salt 18 12345 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC}

由输出可以看出,执行的 SQL 语句为:

  1. SELECT `id`, `name`, `salt`, `age`, `passwd` FROM `user` WHERE `id`=? LIMIT 1

该语句耗时 4.003 ms。在开发中这个方法非常好用!

有时候,调试信息都输出到控制台并不利于我们查询,xorm可以设置日志选项,将日志输出到文件中:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. f, err := os.Create("sql.log")
  4. if err != nil {
  5. panic(err)
  6. }
  7. engine.SetLogger(log.NewSimpleLogger(f))
  8. engine.Logger().SetLevel(log.LOG_DEBUG)
  9. engine.ShowSQL(true)
  10. user := &User{}
  11. engine.ID(1).Omit("created", "updated").Get(user)
  12. fmt.Printf("user:%v\n", user)
  13. }

这样xorm就会将调试日志输出到sql.log文件中。注意log.NewSimpleLogger(f)xorm的子包xorm.io/xorm/log提供的简单日志封装,而非标准库log

Exist

Exist()方法查询符合条件的记录是否存在,它的返回与Get()方法一致,都是(bool, error)。不同之处在于Get()会将查询得到的字段赋值给传入的对象。相比之下Exist()方法效率要高一些。如果不需要获取数据,只要判断是否存在建议使用Exist()方法。

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. user1 := &User{}
  4. has, _ := engine.ID(1).Exist(user1)
  5. if has {
  6. fmt.Println("user with id=1 exist")
  7. } else {
  8. fmt.Println("user with id=1 not exist")
  9. }
  10. user2 := &User{}
  11. has, _ = engine.Where("name=?", "dj2").Get(user2)
  12. if has {
  13. fmt.Println("user with name=dj2 exist")
  14. } else {
  15. fmt.Println("user with name=dj2 not exist")
  16. }
  17. }

Find

Get()方法只能返回单条记录,其生成的 SQL 语句总是有LIMIT 1Find()方法返回所有符合条件的记录。Find()需要传入对象切片的指针或 map 的指针:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. slcUsers:= make([]User, 1)
  4. engine.Where("age > ? and age < ?", 12, 30).Find(&slcUsers)
  5. fmt.Println("users whose age between [12,30]:", slcUsers)
  6. mapUsers := make(map[int64]User)
  7. engine.Where("length(name) = ?", 3).Find(&mapUsers)
  8. fmt.Println("users whose has name of length 3:", mapUsers)
  9. }

map的键为主键,所以如果表为复合主键就不能使用这种方式了。

Iterate

Find()一样,Iterate()也是找到满足条件的所有记录,只不过传入了一个回调去处理每条记录:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. engine.Where("age > ? and age < ?", 12, 30).Iterate(&User{}, func(i int, bean interface{}) error {
  4. fmt.Printf("user%d:%v\n", i, bean.(*User))
  5. return nil
  6. })
  7. }

如果回调返回一个非nil的错误,后面的记录就不会再处理了。

Count

Count()方法统计满足条件的记录数量:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. num, _ := engine.Where("age >= ?", 50).Count(&User{})
  4. fmt.Printf("there are %d users whose age >= 50", num)
  5. }

Rows

Rows()方法与Iterate()类似,不过返回一个Rows对象由我们自己迭代,更加灵活:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. rows, _ := engine.Where("age > ? and age < ?", 12, 30).Rows(&User{})
  4. defer rows.Close()
  5. u := &User{}
  6. for rows.Next() {
  7. rows.Scan(u)
  8. fmt.Println(u)
  9. }
  10. }

Rows()的使用与database/sql有些类似,但是rows.Scan()方法可以传入一个对象,比database/sql更方便。

Sum

xorm提供了两组求和的方法:

  • Sum/SumInt:求某个字段的和,Sum返回float64SumInt返回int64
  • Sums/SumsInt:分别求某些字段的和,Sums返回[]float64SumsInt返回[]int64

例如:

  1. type Sum struct {
  2. Id int64
  3. Money int32
  4. Rate float32
  5. }
  6. func main() {
  7. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  8. engine.Sync2(&Sum{})
  9. var slice []*Sum
  10. for i := 0; i < 100; i++ {
  11. slice = append(slice, &Sum{
  12. Money: rand.Int31n(10000),
  13. Rate: rand.Float32(),
  14. })
  15. }
  16. engine.Insert(&slice)
  17. totalMoney, _ := engine.SumInt(&Sum{}, "money")
  18. fmt.Println("total money:", totalMoney)
  19. totalRate, _ := engine.Sum(&Sum{}, "rate")
  20. fmt.Println("total rate:", totalRate)
  21. totals, _ := engine.Sums(&Sum{}, "money", "rate")
  22. fmt.Printf("total money:%f & total rate:%f", totals[0], totals[1])
  23. }

插入

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

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. user := &User{Name: "lzy", Age: 50}
  4. affected, _ := engine.Insert(user)
  5. fmt.Printf("%d records inserted, user.id:%d\n", affected, user.Id)
  6. users := make([]*User, 2)
  7. users[0] = &User{Name: "xhq", Age: 41}
  8. users[1] = &User{Name: "lhy", Age: 12}
  9. affected, _ = engine.Insert(&users)
  10. fmt.Printf("%d records inserted, id1:%d, id2:%d", affected, users[0].Id, users[1].Id)
  11. }

插入单条记录传入一个对象指针,批量插入传入一个切片。需要注意的是,批量插入时,每个对象的Id字段不会被自动赋值,所以上面最后一行输出id1id2均为 0。另外,一次Insert()调用可以传入多个参数,可以对应不同的表。

更新

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

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. engine.ID(1).Update(&User{Name: "ldj"})
  4. engine.ID(1).Cols("name", "age").Update(&User{Name: "dj"})
  5. engine.Table(&User{}).ID(1).Update(map[string]interface{}{"age": 18})
  6. }

由于使用map[string]interface{}类型的参数,xorm无法推断表名,必须使用Table()方法指定。第一个Update()方法只会更新name字段,其他空字段不更新。第二个Update()方法会更新nameage两个字段,age被更新为 0。

删除

直接调用engine.Delete()删除符合条件的记录,返回删除的条目数量:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. affected, _ := engine.Where("name = ?", "lzy").Delete(&User{})
  4. fmt.Printf("%d records deleted", affected)
  5. }

创建时间、更新时间、软删除

如果我们为time.Time/int/int64这些类型的字段设置xorm:"created"标签,插入数据时,该字段会自动更新为当前时间;

如果我们为tiem.Time/int/int64这些类型的字段设置xorm:"updated"标签,插入和更新数据时,该字段会自动更新为当前时间;

如果我们为time.Time类型的字段设置了xorm:"deleted"标签,删除数据时,只是设置删除时间,并不真正删除记录。

  1. type Player struct {
  2. Id int64
  3. Name string
  4. Age int
  5. CreatedAt time.Time `xorm:"created"`
  6. UpdatedAt time.Time `xorm:"updated"`
  7. DeletedAt time.Time `xorm:"deleted"`
  8. }
  9. func main() {
  10. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  11. engine.Sync2(&Player{})
  12. engine.Insert(&Player{Name:"dj", Age:18})
  13. p := &Player{}
  14. engine.Where("name = ?", "dj").Get(p)
  15. fmt.Println("after insert:", p)
  16. time.Sleep(5 * time.Second)
  17. engine.Table(&Player{}).ID(p.Id).Update(map[string]interface{}{"age":30})
  18. engine.Where("name = ?", "dj").Get(p)
  19. fmt.Println("after update:", p)
  20. time.Sleep(5 * time.Second)
  21. engine.ID(p.Id).Delete(&Player{})
  22. engine.Where("name = ?", "dj").Unscoped().Get(p)
  23. fmt.Println("after delete:", p)
  24. }

输出:

  1. after insert: &{1 dj 18 2020-05-08 23:09:19 +0800 CST 2020-05-08 23:09:19 +0800 CST 0001-01-01 00:00:00 +0000 UTC}
  2. after update: &{1 dj 30 2020-05-08 23:09:19 +0800 CST 2020-05-08 23:09:24 +0800 CST 0001-01-01 00:00:00 +0000 UTC}
  3. after delete: &{1 dj 30 2020-05-08 23:09:19 +0800 CST 2020-05-08 23:09:24 +0800 CST 2020-05-08 23:09:29 +0800 CST}

创建时间一旦创建成功就不会再改变了,更新时间每次更新都会变化。已删除的记录必须使用Unscoped()方法查询,如果要真正 删除某条记录,也可以使用Unscoped()

执行原始的 SQL

除了上面提供的方法外,xorm还可以执行原始的 SQL 语句:

  1. func main() {
  2. engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  3. querySql := "select * from user limit 1"
  4. reuslts, _ := engine.Query(querySql)
  5. for _, record := range reuslts {
  6. for key, val := range record {
  7. fmt.Println(key, string(val))
  8. }
  9. }
  10. updateSql := "update `user` set name=? where id=?"
  11. res, _ := engine.Exec(updateSql, "ldj", 1)
  12. fmt.Println(res.RowsAffected())
  13. }

Query()方法返回[]map[string][]byte,切片中的每个元素都代表一条记录,map的键对应列名,[]byte为值。还有QueryInterface()方法返回[]map[string]interface{}QueryString()方法返回[]map[string]interface{}

运行程序:

  1. salt salt
  2. age 18
  3. passwd 12345
  4. created 2020-05-08 21:12:11
  5. updated 2020-05-08 22:44:58
  6. id 1
  7. name ldj
  8. 1 <nil>

总结

本文对xorm做了一个简单的介绍,xorm的特性远不止于此。xorm可以定义结构体字段与表列名映射规则、创建索引、执行事务、导入导出 SQL 脚本等。感兴趣可自行探索。好在xorm有比较详尽的中文文档。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. xorm GitHub:https://github.com/go-xorm/xorm
  2. xorm 手册:http://gobook.io/read/gitea.com/xorm/manual-zh-CN/
  3. Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib