Xorm 是使用 Go 语言编写的 ORM 库,支持 Mysql 、Postgres、TiDB 等数据库驱动,在 gitea、grafana 等开源项目中均有应用。
xorm 旧版本有在 github 上维护过,但是新版本都转移到 gitea 上维护,安装最新版的 xorm 库的命令为:
go get -v xorm.io/xorm
在代码中导入的语句为
import (
"xorm.io/xorm"
// 根据需要匿名导入数据库驱动
_ "github.com/go-sql-driver/mysql"
)
"
如果开启了 go module,在代码文件中导入 xorm 后,可以执行 go mod tidy 自动下载 xorm 库并同步到 go.mod
文件中
ORM(Object Relational Mapping)的含义就是把代码中的对象映射为数据库(通常时关系型数据库)中的一个表,在 xorm 中基础的代码及数据库的映射关系如下:
// 一个 struct 结构体对应数据库中的一张表
// 结构体中的成员对应数据表中的一个字段
// 结构体成员的标签对应数据表字段的相关属性,如类型、是否允许为空、注释等,如果没有指定标签,则用 xorm 库中规定 Go数据类型及字段类型对应表
type User struct {
Id int64
Name string `xorm:"varchar(25) notnull unique 'usr_name' comment('姓名')"`
}
// xorm中的一个 Engine 数据类型,对应数据库中的一个库,此例中就是 xorm-demo 库
engine, _ := xorm.NewEngine("mysql", "root:passwd@/xorm-demo?charset=utf8")
// 设置日志显示sql查询语句
engine.ShowSQL(true)
// 根据struct定义在数据库中创建相应的表结构
if err = engine.Sync2(new(User)); err != nil {
log.Fatalf("Fail to sync database: %v\n", err)
}
本文以经典的银行交易为例子,演示 xorm 库在操作数据库时的用途。
数据模型为:
type Account struct {
Id int64
Name string `xorm:"notnull unique"`
Balance decimal.Decimal
CardNumber string `xorm:"index"`
Version int `xorm:"version"`
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
}
type Card struct {
Number string `xorm:"pk varchar(25) notnull unique 'card_No' comment('银行卡号')"`
ValidFrom time.Time
ValidThru time.Time
}
结构体的成员可以通过xorm的标签来声明一些字段属性,有一些可以“望文生义”,比如 notnull 表示字段不能为空,unique 表示字段的值必须唯一,有一些需要查阅文档,比如 created 代表该字段的值为记录创建的时间,version 表示该字段作为乐观锁,详细情况可以查看文档 Xorm中文文档-Column属性定义
xorm的默认名称映射规则是把驼峰命名的结构体成员转换为下划线命名的数据库字段,如 CreatedAt
会被映射为 created_at
xorm 中定义了 Mapper 接口,凡是实现了 Mapper 接口方法的结构体都可以作为映射规则,默认的映射规则对应的结构体是 SnakeMapper
映射规则是全局生效的,即所有的表名或所有的字段名都遵从一个映射规则:
// 设置映射规则后,所有表的迁移、查询、插入等操作都会运用该映射规则
colMapper := names.NewPrefixMapper(names.SnakeMapper{}, "card_")
engine.SetColumnMapper(colMapper)
if err = engine.Sync2(new(Card)); err != nil {
log.Fatalf("Fail to sync database: %v\n", err)
}
// 同步后的 card 表
mysql> desc card;
+-----------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+-------------+------+-----+---------+-------+
| card_id | bigint(20) | YES | | NULL | |
| card_No | varchar(25) | NO | PRI | NULL | |
| card_valid_from | datetime | YES | | NULL | |
| card_valid_thru | datetime | YES | | NULL | |
+-----------------+-------------+------+-----+---------+-------+
映射规则还可以通过 Mapper 之外的方式改,如结构体中的标签设定的字段名就比 Mapper 方式的优先级高,示例中的 card_No
就是由 card 结构体中Number 的标签指定的字段名,如果按 Mapper 的规则应是 card_number
创建一个账户:
func createAccount(engine *xorm.Engine, name string, amount float64) (err error) {
cardNumber := fmt.Sprintf("%08v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100000000))
if _, err = engine.Insert(&Account{Name: name, Balance: decimal.NewFromFloat(amount), CardNumber: cardNumber}); err != nil {
return err
}
return nil
}
从 Account 表中删除一个指定ID的账户:
func deleteAccount(engine *xorm.Engine, id int64) error {
_, err := engine.ID(id).Delete(&Account{})
return err
}
存款操作触发对账户余额的更新,默认是更新结构体内所有不为 nil 的成员变量,通过 Cols 函数可以指定要更新的字段:
func makeDeposit(engine *xorm.Engine, name string, amount float64) error {
a, err := getAccountByName(engine, name)
if err != nil {
return err
}
a.Balance = a.Balance.Add(decimal.NewFromFloat(amount))
if _, err = engine.ID(a.Id).Cols("balance").Update(a); err != nil {
return err
}
return nil
}
Find方法可以获取表中的所有记录:
func listAccounts(engine *xorm.Engine) (as []Account, err error) {
err = engine.Find(&as)
return as, err
}
当使用事务处理时,需要创建 Session 对象,通过 session.Begin() 和 session.Close() 标识一个事务的开始和结束,在每个更改数据库的操作出现异常时,都要调用 session.Rollback() 回滚事务,保障事务的原子性,所有操作执行后,调用 session.Commit() 提交事务
func makeTransfer(engine *xorm.Engine, srcAccountName, destAccountName string, amount float64) error {
sess := engine.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
a1, err := getAccountByName(engine, srcAccountName)
if err != nil {
return err
}
a2, err := getAccountByName(engine, destAccountName)
if err != nil {
return err
}
if a1.Balance.Cmp(decimal.NewFromFloat(amount)) == -1 {
return errors.New("Source account does not have enough money")
}
a1.Balance = a1.Balance.Add(decimal.NewFromFloat(amount))
a2.Balance = a2.Balance.Sub(decimal.NewFromFloat(amount))
if _, err = sess.ID(a1.Id).Cols("balance").Update(a1); err != nil {
sess.Rollback()
return err
}
if _, err = sess.ID(a2.Id).Cols("balance").Update(a2); err != nil {
sess.Rollback()
return err
}
return sess.Commit()
}
当进行复杂的sql查询时,可以使用 xorm 的 sql builder 构造 sql 查询语句,然后通过 Query、QueryString 或者 QueryInterface 的方法发起查询,这三个方法效果相同,只是返回的数据类型不同,分别是 []map[string][]byte,[]map[string]string 和 []map[string]interface{},可以根据自己的需要进行选择
import "xorm.io/builder"
engine.QueryString(builder.Select("*").From("account").Where(builder.Eq{"name": name}))
通过 Join 函数和 extends 标签进行多表联合查询,定义的 AccountCard 结构体可以用于接收返回的数据
type AccountCard struct {
Account `xorm:"extends"`
Card `xorm:"extends"`
}
func listAccountWithCards(engine *xorm.Engine) (acs []AccountCard, err error) {
acs = make([]AccountCard, 0)
err = engine.Table("account").Join("INNER", "card", "account.card_number = card.card_No").Find(&acs)
return acs, err
}
事件钩子可以在增删改查的前后触发,比如在本例中,可以在查询银行卡号的数据库操作后,对银行卡号进行脱敏,屏蔽其中一些位数
xorm支持两类事件钩子,一种是通过结构体的方法函数,一种是在增删改查的链式函数前插入After或者Before方法,作为临时使用的钩子函数
// maskCardNumber是参数为空接口的函数,实现对银行卡号的屏蔽
func listAccounts(engine *xorm.Engine) (as []Account, err error) {
err = engine.After(maskCardNumber).Find(&as)
return as, err
}
package main
import (
"errors"
"fmt"
"log"
"math/rand"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/shopspring/decimal"
"xorm.io/builder"
"xorm.io/xorm"
)
// Account belongs to user
type Account struct {
Id int64
Name string `xorm:"notnull unique"`
Balance decimal.Decimal
CardNumber string `xorm:"index"`
Version int `xorm:"version"`
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
}
// Card created when account is setting
type Card struct {
Number string `xorm:"pk varchar(25) notnull unique 'card_No' comment('银行卡号')"`
ValidFrom time.Time
ValidThru time.Time
}
type AccountCard struct {
Account `xorm:"extends"`
Card `xorm:"extends"`
}
var (
engine *xorm.Engine
item int
id int64
accountName string
amount float64
srcAccountName string
destAccountName string
)
func main() {
fmt.Println("engine", engine)
engine, err := xorm.NewEngine("mysql", "root:Hwb.12621@/xorm_demo?charset=utf8")
fmt.Println("engine", engine)
if err != nil {
log.Fatalf("Fail to create engine: %v\n", err)
}
//设置日志显示
engine.ShowSQL(true)
if err = engine.Sync2(new(Account), new(Card)); err != nil {
log.Fatalf("Fail to sync database: %v\n", err)
}
menu := []string{
"--------------------",
"Chose an item:",
"1.Create an account",
"2.List all accounts",
"3.List all accounts with cards",
"4.Transfer money",
"5.Make deposit",
"6.Make withdraw",
"7.Delete an account",
}
fmt.Println(strings.Join(menu, "\r\n"))
fmt.Printf("Chose:")
fmt.Scanln(&item)
switch item {
case 1:
fmt.Printf("Enter account name and initial balance:")
fmt.Scanln(&accountName, &amount)
if err = createFullAccount(engine, accountName, amount); err != nil {
log.Fatalf("Fail to create full account: %v\n", err)
}
case 2:
as, _ := listAccounts(engine)
for _, a := range as {
fmt.Printf("%v %v %v\n", a.Name, a.Balance, a.CardNumber)
}
case 3:
acs, _ := listAccountWithCards(engine)
fmt.Println(acs)
for _, ac := range acs {
fmt.Printf("%v %v %v %v %v %v\n", ac.Id, ac.Name, ac.Balance, ac.CardNumber, ac.ValidFrom, ac.ValidThru)
}
case 4:
fmt.Printf("Enter source and destination account name,and amount to tansfer:")
fmt.Scanln(&srcAccountName, &destAccountName, &amount)
if err = makeTransfer(engine, srcAccountName, destAccountName, amount); err != nil {
log.Fatalf("Failed to transfer money: %v\n", err)
}
case 5:
fmt.Printf("Enter account Name and amount deposited:")
fmt.Scanln(&destAccountName, &amount)
if err = makeDeposit(engine, destAccountName, amount); err != nil {
log.Fatalf("Deposit error: %v\n", err)
}
case 6:
fmt.Printf("Enter account Name and amount withdrawed:")
fmt.Scanln(&destAccountName, &amount)
if err = makeWithdraw(engine, destAccountName, amount); err != nil {
log.Fatalf("Withdraw error: %v\n", err)
}
case 7:
fmt.Printf("Enter account ID:")
fmt.Scanln(&id)
if err = deleteAccount(engine, id); err != nil {
log.Fatalf("Delete account error: %v\n", err)
}
default:
log.Fatalln("Invalid option")
}
}
func createFullAccount(engine *xorm.Engine, name string, amount float64) (err error) {
sess := engine.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
cardNumber := fmt.Sprintf("%08v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100000000))
if _, err = sess.Insert(&Account{Name: name, Balance: decimal.NewFromFloat(amount), CardNumber: cardNumber}); err != nil {
sess.Rollback()
return err
}
setValidRange := func(bean interface{}) {
card := bean.(*Card)
card.ValidFrom = time.Now()
card.ValidThru = card.ValidFrom.AddDate(3, 0, 0)
}
if _, err = sess.Before(setValidRange).Insert(&Card{Number: cardNumber}); err != nil {
sess.Rollback()
return err
}
return sess.Commit()
}
func listAccounts(engine *xorm.Engine) (as []Account, err error) {
err = engine.After(maskCardNumber).Find(&as)
return as, err
}
func getAccountByName(engine *xorm.Engine, name string) (*Account, error) {
account := &Account{}
results, err := engine.QueryString(builder.Select("*").From("account").Where(builder.Eq{"name": name}))
result := results[0]
id, _ := strconv.Atoi(result["id"])
account.Id = int64(id)
account.Name = result["name"]
account.Balance, _ = decimal.NewFromString(result["balance"])
account.CardNumber = result["card_number"]
account.Version, _ = strconv.Atoi(result["version"])
account.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", result["created_at"])
account.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", result["updated_at"])
fmt.Println(account)
return account, err
}
func maskCardNumber(bean interface{}) {
maskedTemp := make([]rune, 0)
switch bean.(type) {
case *Account:
account, _ := bean.(*Account)
for index, char := range account.CardNumber {
if index == 0 || index == 1 {
maskedTemp = append(maskedTemp, char)
} else {
maskedTemp = append(maskedTemp, '*')
}
}
account.CardNumber = string(maskedTemp)
case *AccountCard:
ac, _ := bean.(*AccountCard)
for index, char := range ac.CardNumber {
if index == 0 || index == 1 {
maskedTemp = append(maskedTemp, char)
} else {
maskedTemp = append(maskedTemp, '*')
}
}
ac.CardNumber = string(maskedTemp)
default:
log.Fatalln("Unknonw type")
}
}
func makeTransfer(engine *xorm.Engine, srcAccountName, destAccountName string, amount float64) error {
sess := engine.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
a1, err := getAccountByName(engine, srcAccountName)
if err != nil {
return err
}
a2, err := getAccountByName(engine, destAccountName)
if err != nil {
return err
}
if a1.Balance.Cmp(decimal.NewFromFloat(amount)) == -1 {
return errors.New("Source account does not have enough money")
}
a1.Balance = a1.Balance.Add(decimal.NewFromFloat(amount))
a2.Balance = a2.Balance.Sub(decimal.NewFromFloat(amount))
if _, err = sess.ID(a1.Id).Cols("balance").Update(a1); err != nil {
sess.Rollback()
return err
}
if _, err = sess.ID(a2.Id).Cols("balance").Update(a2); err != nil {
sess.Rollback()
return err
}
return sess.Commit()
}
func makeDeposit(engine *xorm.Engine, name string, amount float64) error {
a, err := getAccountByName(engine, name)
if err != nil {
return err
}
a.Balance = a.Balance.Add(decimal.NewFromFloat(amount))
if _, err = engine.ID(a.Id).Cols("balance").Update(a); err != nil {
return err
}
return nil
}
func makeWithdraw(engine *xorm.Engine, name string, amount float64) error {
a, err := getAccountByName(engine, name)
if err != nil {
return err
}
if a.Balance.Cmp(decimal.NewFromFloat(amount)) == -1 {
return errors.New("Not enough balance")
}
a.Balance = a.Balance.Sub(decimal.NewFromFloat(amount))
if _, err = engine.ID(a.Id).Cols("balance").Update(a); err != nil {
return err
}
return nil
}
func deleteAccount(engine *xorm.Engine, id int64) error {
_, err := engine.ID(id).Delete(&Account{})
return err
}
func listAccountWithCards(engine *xorm.Engine) (acs []AccountCard, err error) {
acs = make([]AccountCard, 0)
err = engine.Table("account").Join("INNER", "card", "account.card_number = card.card_No").After(maskCardNumber).Find(&acs)
return acs, err
}