22

Go 每日一库之 xorm

 4 years ago
source link: https://studygolang.com/articles/28531
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

简介

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

快速使用

先安装:

$ go get xorm.io/xorm

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

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

使用:

package main

import (
  "log"
  "time"

  _ "github.com/go-sql-driver/mysql"
  "xorm.io/xorm"
)

type User struct {
  Id      int64
  Name    string
  Salt    string
  Age     int
  Passwd  string    `xorm:"varchar(200)"`
  Created time.Time `xorm:"created"`
  Updated time.Time `xorm:"updated"`
}

func main() {
  engine, err := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  if err != nil {
    log.Fatal(err)
  }

  err = engine.Sync2(new(User))
  if err != nil {
    log.Fatal(err)
  }
}

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

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

mUV7Rbv.png!web

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

type User struct {
  Id      int64
  Name    string
  Salt    string
  Age     int
  Level   int
  Passwd  string    `xorm:"varchar(200)"`
  Created time.Time `xorm:"created"`
  Updated time.Time `xorm:"updated"`
}

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

eqMFr2M.png!web

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

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

[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 表中插入了一些数据:

ammMv2F.png!web

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

Get

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

type User struct {
  Id      int64
  Name    string
  Salt    string
  Age     int
  Passwd  string    `xorm:"varchar(200)"`
  Created time.Time `xorm:"created"`
  Updated time.Time `xorm:"updated"`
}

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  user1 := &User{}
  has, _ := engine.ID(1).Get(user1)
  if has {
    fmt.Printf("user1:%v\n", user1)
  }

  user2 := &User{}
  has, _ = engine.Where("name=?", "dj").Get(user2)
  if has {
    fmt.Printf("user2:%v\n", user2)
  }

  user3 := &User{Id: 5}
  has, _ = engine.Get(user3)
  if has {
    fmt.Printf("user3:%v\n", user3)
  }

  user4 := &User{Name: "pipi"}
  has, _ = engine.Get(user4)
  if has {
    fmt.Printf("user4:%v\n", user4)
  }
}

上面演示了 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" 的用户。

运行程序:

user1:&{1 dj salt 18 12345 2020-05-08 21:12:11 +0800 CST 2020-05-08 21:12:11 +0800 CST}
user2:&{1 dj salt 18 12345 2020-05-08 21:12:11 +0800 CST 2020-05-08 21:12:11 +0800 CST}
user3:&{5 mxg salt 54 12345 2020-05-08 21:13:31 +0800 CST 2020-05-08 21:13:31 +0800 CST}
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() ):

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  user1 := &User{}
  engine.ID(1).Cols("id", "name", "age").Get(user1)
  fmt.Printf("user1:%v\n", user1)

  user2 := &User{Name: "pipi"}
  engine.Omit("created", "updated").Get(user2)
  fmt.Printf("user2:%v\n", user2)
}

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

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  engine.ShowSQL(true)

  user := &User{}
  engine.ID(1).Omit("created", "updated").Get(user)
  fmt.Printf("user:%v\n", user)
}

运行程序:

[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
user:&{1 dj salt 18 12345 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC}

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

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

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

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  f, err := os.Create("sql.log")
  if err != nil {
    panic(err)
  }

  engine.SetLogger(log.NewSimpleLogger(f))
  engine.Logger().SetLevel(log.LOG_DEBUG)
  engine.ShowSQL(true)

  user := &User{}
  engine.ID(1).Omit("created", "updated").Get(user)
  fmt.Printf("user:%v\n", user)
}

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

Exist

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  user1 := &User{}
  has, _ := engine.ID(1).Exist(user1)
  if has {
    fmt.Println("user with id=1 exist")
  } else {
    fmt.Println("user with id=1 not exist")
  }

  user2 := &User{}
  has, _ = engine.Where("name=?", "dj2").Get(user2)
  if has {
    fmt.Println("user with name=dj2 exist")
  } else {
    fmt.Println("user with name=dj2 not exist")
  }
}

Find

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  slcUsers:= make([]User, 1)
  engine.Where("age > ? and age < ?", 12, 30).Find(&slcUsers)
  fmt.Println("users whose age between [12,30]:", slcUsers)

  mapUsers := make(map[int64]User)
  engine.Where("length(name) = ?", 3).Find(&mapUsers)
  fmt.Println("users whose has name of length 3:", mapUsers)
}

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

Iterate

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  engine.Where("age > ? and age < ?", 12, 30).Iterate(&User{}, func(i int, bean interface{}) error {
    fmt.Printf("user%d:%v\n", i, bean.(*User))
    return nil
  })
}

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

Count

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  num, _ := engine.Where("age >= ?", 50).Count(&User{})
  fmt.Printf("there are %d users whose age >= 50", num)
}

Rows

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  rows, _ := engine.Where("age > ? and age < ?", 12, 30).Rows(&User{})
  defer rows.Close()

  u := &User{}
  for rows.Next() {
    rows.Scan(u)

    fmt.Println(u)
  }
}

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

Sum

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

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

例如:

type Sum struct {
  Id    int64
  Money int32
  Rate  float32
}

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  engine.Sync2(∑{})

  var slice []*Sum
  for i := 0; i < 100; i++ {
    slice = append(slice, ∑{
      Money: rand.Int31n(10000),
      Rate:  rand.Float32(),
    })
  }
  engine.Insert(&slice)

  totalMoney, _ := engine.SumInt(∑{}, "money")
  fmt.Println("total money:", totalMoney)

  totalRate, _ := engine.Sum(∑{}, "rate")
  fmt.Println("total rate:", totalRate)

  totals, _ := engine.Sums(∑{}, "money", "rate")
  fmt.Printf("total money:%f & total rate:%f", totals[0], totals[1])
}

插入

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  user := &User{Name: "lzy", Age: 50}

  affected, _ := engine.Insert(user)
  fmt.Printf("%d records inserted, user.id:%d\n", affected, user.Id)

  users := make([]*User, 2)
  users[0] = &User{Name: "xhq", Age: 41}
  users[1] = &User{Name: "lhy", Age: 12}

  affected, _ = engine.Insert(&users)
  fmt.Printf("%d records inserted, id1:%d, id2:%d", affected, users[0].Id, users[1].Id)
}

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

更新

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")
  engine.ID(1).Update(&User{Name: "ldj"})
  engine.ID(1).Cols("name", "age").Update(&User{Name: "dj"})

  engine.Table(&User{}).ID(1).Update(map[string]interface{}{"age": 18})
}

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

删除

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

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  affected, _ := engine.Where("name = ?", "lzy").Delete(&User{})
  fmt.Printf("%d records deleted", affected)
}

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

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

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

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

type Player struct {
  Id int64
  Name string
  Age int
  CreatedAt time.Time `xorm:"created"`
  UpdatedAt time.Time `xorm:"updated"`
  DeletedAt time.Time `xorm:"deleted"`
}

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  engine.Sync2(&Player{})
  engine.Insert(&Player{Name:"dj", Age:18})

  p := &Player{}
  engine.Where("name = ?", "dj").Get(p)
  fmt.Println("after insert:", p)
  time.Sleep(5 * time.Second)

  engine.Table(&Player{}).ID(p.Id).Update(map[string]interface{}{"age":30})

  engine.Where("name = ?", "dj").Get(p)
  fmt.Println("after update:", p)
  time.Sleep(5 * time.Second)

  engine.ID(p.Id).Delete(&Player{})

  engine.Where("name = ?", "dj").Unscoped().Get(p)
  fmt.Println("after delete:", p)
}

输出:

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}
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}
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 语句:

func main() {
  engine, _ := xorm.NewEngine("mysql", "root:12345@/test?charset=utf8")

  querySql := "select * from user limit 1"
  reuslts, _ := engine.Query(querySql)
  for _, record := range reuslts {
    for key, val := range record {
      fmt.Println(key, string(val))
    }
  }

  updateSql := "update `user` set name=? where id=?"
  res, _ := engine.Exec(updateSql, "ldj", 1)
  fmt.Println(res.RowsAffected())
}

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

运行程序:

salt salt
age 18
passwd 12345
created 2020-05-08 21:12:11
updated 2020-05-08 22:44:58
id 1
name ldj
1 <nil>

总结

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

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

参考

  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/darjun/go-daily-lib

我的博客: https://darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

bMFFVjA.jpg!web

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK