![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SZiJzMiVjYlRjM4U2MkhDZ2EDO1YmZhFzM1ITZzEWNw8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
簡介
Go 标準庫提供的資料庫接口
database/sql
比較底層,使用它來操作資料庫非常繁瑣,而且容易出錯。因而社群開源了不少第三方庫,如上一篇文章中的
sqlc
工具,還有各式各樣的 ORM (Object Relational Mapping,對象關系映射庫),如
gorm
和
xorm
。本文介紹
xorm
。
xorm
是一個簡單但強大的 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:[email protected]/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
檢視表結構:
如果表
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
指令檢視表結構:
發現表中多了一個
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
表中插入了一些資料:
後面的代碼為了簡單起見,忽略了錯誤處理,實際使用中不要漏掉!
Get
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:[email protected]/test?charset=utf8")
user1 := &User{}
has, _ := engine.ID(1).Get(user1)
if has {
fmt.Printf("user1:%vn", user1)
}
user2 := &User{}
has, _ = engine.Where("name=?", "dj").Get(user2)
if has {
fmt.Printf("user2:%vn", user2)
}
user3 := &User{Id: 5}
has, _ = engine.Get(user3)
if has {
fmt.Printf("user3:%vn", user3)
}
user4 := &User{Name: "pipi"}
has, _ = engine.Get(user4)
if has {
fmt.Printf("user4:%vn", user4)
}
}
上面示範了 3 種使用
Get()
的方式:
- 使用主鍵:
查詢主鍵(即engine.ID(1)
)為 1 的使用者;id
- 使用條件語句:
查詢engine.Where("name=?", "dj")
的使用者;name = "dj"
- 使用對象中的非空字段:
設定了user3
字段為 5,Id
查詢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:[email protected]/test?charset=utf8")
user1 := &User{}
engine.ID(1).Cols("id", "name", "age").Get(user1)
fmt.Printf("user1:%vn", user1)
user2 := &User{Name: "pipi"}
engine.Omit("created", "updated").Get(user2)
fmt.Printf("user2:%vn", user2)
}
上面第一個查詢使用
Cols()
方法指定隻傳回
id
、
name
、
age
這 3 列,第二個查詢使用
Omit()
方法忽略列
created
和
updated
。
另外,為了便于排查可能出現的問題,
xorm
提供了
ShowSQL()
方法設定将執行的 SQL 同時在控制台中輸出:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/test?charset=utf8")
engine.ShowSQL(true)
user := &User{}
engine.ID(1).Omit("created", "updated").Get(user)
fmt.Printf("user:%vn", 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:[email protected]/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:%vn", user)
}
這樣
xorm
就會将調試日志輸出到
sql.log
檔案中。注意
log.NewSimpleLogger(f)
是
xorm
的子包
xorm.io/xorm/log
提供的簡單日志封裝,而非标準庫
log
。
Exist
Exist
Exist()
方法查詢符合條件的記錄是否存在,它的傳回與
Get()
方法一緻,都是
(bool, error)
。不同之處在于
Get()
會将查詢得到的字段指派給傳入的對象。相比之下
Exist()
方法效率要高一些。如果不需要擷取資料,隻要判斷是否存在建議使用
Exist()
方法。
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/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
Find
Get()
方法隻能傳回單條記錄,其生成的 SQL 語句總是有
LIMIT 1
。
Find()
方法傳回所有符合條件的記錄。
Find()
需要傳入對象切片的指針或 map 的指針:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/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
Iterate
與
Find()
一樣,
Iterate()
也是找到滿足條件的所有記錄,隻不過傳入了一個回調去處理每條記錄:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/test?charset=utf8")
engine.Where("age > ? and age < ?", 12, 30).Iterate(&User{}, func(i int, bean interface{}) error {
fmt.Printf("user%d:%vn", i, bean.(*User))
return nil
})
}
如果回調傳回一個非
nil
的錯誤,後面的記錄就不會再處理了。
Count
Count
Count()
方法統計滿足條件的記錄數量:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/test?charset=utf8")
num, _ := engine.Where("age >= ?", 50).Count(&User{})
fmt.Printf("there are %d users whose age >= 50", num)
}
Rows
Rows
Rows()
方法與
Iterate()
類似,不過傳回一個
Rows
對象由我們自己疊代,更加靈活:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/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
Sum
xorm
提供了兩組求和的方法:
-
:求某個字段的和,Sum/SumInt
傳回Sum
,float64
傳回SumInt
;int64
-
:分别求某些字段的和,Sums/SumsInt
傳回Sums
,[]float64
傳回SumsInt
。[]int64
例如:
type Sum struct {
Id int64
Money int32
Rate float32
}
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/test?charset=utf8")
engine.Sync2(&Sum{})
var slice []*Sum
for i := 0; i < 100; i++ {
slice = append(slice, &Sum{
Money: rand.Int31n(10000),
Rate: rand.Float32(),
})
}
engine.Insert(&slice)
totalMoney, _ := engine.SumInt(&Sum{}, "money")
fmt.Println("total money:", totalMoney)
totalRate, _ := engine.Sum(&Sum{}, "rate")
fmt.Println("total rate:", totalRate)
totals, _ := engine.Sums(&Sum{}, "money", "rate")
fmt.Printf("total money:%f & total rate:%f", totals[0], totals[1])
}
插入
使用
engine.Insert()
方法,可以插入單條資料,也可以批量插入多條資料:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/test?charset=utf8")
user := &User{Name: "lzy", Age: 50}
affected, _ := engine.Insert(user)
fmt.Printf("%d records inserted, user.id:%dn", 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
字段不會被自動指派,是以上面最後一行輸出
id1
和
id2
均為 0。另外,一次
Insert()
調用可以傳入多個參數,可以對應不同的表。
更新
更新通過
engine.Update()
實作,可以傳入結構指針或
map[string]interface{}
。對于傳入結構體指針的情況,
xorm
隻會更新非空的字段 。如果一定要更新空字段,需要使用
Cols()
方法顯示指定更新的列。使用
Cols()
方法指定列後,即使字段為空也會更新:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/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()
方法會更新
name
和
age
兩個字段,
age
被更新為 0。
删除
直接調用
engine.Delete()
删除符合條件的記錄,傳回删除的條目數量:
func main() {
engine, _ := xorm.NewEngine("mysql", "root:[email protected]/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:[email protected]/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:[email protected]/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
參考
- xorm GitHub:https://github.com/go-xorm/xorm
- xorm 手冊:http://gobook.io/read/gitea.com/xorm/manual-zh-CN/
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
我的部落格:https://darjun.github.io
歡迎關注我的微信公衆号【GoUpUp】,共同學習,一起進步~