xorm相关操作小结
前言
最近写业务用到xorm操作postgreSQL,抽空写了一些平时会用到的常用的操作,好脑筋不如好笔记。
xorm
参考文档
相关技术博客
上面那个作者的每日一库系列跟Go的相关文章挺不错:Go每日一库、Go系列文章
操作总结
“初始化引擎操作”
下面使用的 model.DB.Walk 实际上是在项目启动时就已经启动的引擎,自己做本地测试的话可以在本地做初始化的工作:
// 记得import这2个库
import (
_ "github.com/lib/pq"
"github.com/xormplus/xorm"
)
// init string
const OnlineConn = "postgres://bytepower_rw:xxxxxx@zzzxxx.com.cn:5432/xxx_db_namexxx?sslmode=disable;"
// init db model.DB.Walk其实就是初始化后的db engine~
model.DB.Walk, err = xorm.NewEngine("postgres", OnlineConn)
if err != nil {
panic("初始化数据库错误:" + err.Error())
}
// 设置链接与链接池相关的参数
// 用于设置最大打开的连接数,默认值为0表示不限制.设置最大的连接数,可以避免并发太高导致连接mysql出现too many connections的错误。
model.DB.Walk.SetMaxOpenConns(cfg.MaxOpenConns)
// 用于设置闲置的连接数.设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。
model.DB.Walk.SetMaxIdleConns(cfg.MaxIdleConns)
// 设置链接的最长链接时间 注意:go 16版本以后才能设置这一项!!!
model.DB.Walk.SetConnMaxLifetime(10 * time.Minute)
defer model.DB.Walk.Close()
定义结构体
type StudentTest struct {
// TODO 注意xorm的注释暂时只支持MySQL引擎,postgreSQL即使设置了也不生效
// 主键 字段名设置为 sid
Id string `json:"id" xorm:"varchar(255) pk 'sid' comment('学生ID')"`
// TODO 注意如果设置为 "非空且唯一" 的话会自动创建一个索引!但是 Sync2方法会报错,尽量不要设置非空且唯一字段!
Name string `json:"name" xorm:"varchar(25) notnull 'name' comment('学生姓名')"`
Age int `json:"age" xorm:"notnull 'age' comment('学生年龄')"`
// 索引字段 TODO 使用orm内置的方法创建索引会失败!需要手动执行SQL语句
Score float64 `json:"score" xorm:"notnull 'score' comment('学生成绩')"`
ClassId string `json:"class_id" xorm:"notnull 'class_id' comment('学生所在班级')"`
// 创建时间与修改时间
CreatedTime time.Time `json:"created_time" xorm:"created notnull"`
UpdatedTime time.Time `json:"updated_time" xorm:"updated notnull"`
}
func (s *StudentTest) TableName() string {
return "student_test"
}
// 新建一个班级表,用于连表查询的测试
type SClassTestModel struct {
Id string `json:"id" xorm:"varchar(255) pk 'cid' comment('班级id')"`
Name string `json:"name" xorm:"varchar(255) notnull 'name' comment('班级名称')"`
}
func (sc *SClassTestModel) TableName() string {
return "sclass_test"
}
创建表
// 创建班级表
func TestCreateClassTable(t *testing.T) {
err := test.Session.Sync2(aaa_module.SClassTestModel{})
require.Equal(t, nil, err)
}
// 创建学生表
func TestCreateTable(t *testing.T) {
// 创建学生表
// TODO 使用 Sync2 方法可以在修改model结构体后直接修改表字段,建议使用这个方法
err := test.Session.Sync2(aaa_module.StudentTest{})
require.Equal(t, nil, err)
}
“批量插入数据”
// 批量插入数据
func InsertStudentSlice(studentSlice []*StudentTest) (int64, error) {
affected, err := model.DB.Walk.Table("student_test").Insert(studentSlice)
if err != nil {
return affected, errors.New("insert student slice raise error!" + err.Error())
}
return affected, nil
}
// 具体实现
// 批量插入多条数据
func TestInsertStudentSlice(t *testing.T) {
stu1 := aaa_module.StudentTest{Id: "1", Name: "whw1", Age: 12, Score: 99, ClassId: "223"}
stu2 := aaa_module.StudentTest{Id: "2", Name: "whw2", Age: 13, Score: 98, ClassId: "222"}
stu3 := aaa_module.StudentTest{Id: "3", Name: "whw3", Age: 14, Score: 97, ClassId: "221"}
stu4 := aaa_module.StudentTest{Id: "4", Name: "whw4", Age: 15, Score: 96, ClassId: "222"}
stu5 := aaa_module.StudentTest{Id: "5", Name: "whw5", Age: 16, Score: 95, ClassId: "221"}
stuSlice := []*aaa_module.StudentTest{&stu1, &stu2, &stu3, &stu4, &stu5}
// 创建
if affected, err := aaa_module.InsertStudentSlice(stuSlice); err != nil {
fmt.Println("affected: ", affected)
fmt.Println("err: ", err)
} else {
fmt.Println("写入成功!affected: ", affected)
}
}
“插入单条数据”
// 插入单条数据
func InsertStudentObj(stuObj *StudentTest) (int64, error) {
if ok, err := model.DB.Walk.Table("student_test").Insert(stuObj); err != nil {
return ok, err
} else {
return ok, nil
}
}
// 具体实现
func TestInsertStuObj(t *testing.T) {
stuObj := aaa_module.StudentTest{Id: "6", Name: "naruto", Age: 22, Score: 99, ClassId: "231"}
if ok, err := aaa_module.InsertStudentObj(&stuObj); err != nil {
fmt.Println("insert stuObj raise error! ", err.Error())
} else {
fmt.Println("insert stuObj successfully! ok: ", ok)
}
}
查询符合条件的记录的数量
func GetCount(session *xorm.Session, query interface{}, args ...interface{}) (count int64, err error) {
return session.Where(query, args...).Count(&WithdrawRecordModel{})
}
// 具体使用
cashId := "123"
userId := "2"
query := "cash_id = ? and user_id = ?"
cashCount, err := GetCount(session, query, cashId, userId)
判读记录是否存在
// 判断记录是否存在
func ExistStu(stu *StudentTest) (bool, error) {
has, err := model.DB.Walk.Table("student_test").Exist(stu)
return has, err
}
// 具体实现
// 判断记录是否存在 —— 效率更高
func TestExistStuObj(t *testing.T) {
stuObj := aaa_module.StudentTest{Name: "naruto"}
has, err := aaa_module.ExistStu(&stuObj)
if err != nil {
fmt.Println("err: ", err.Error())
} else if !has {
fmt.Println("不存在这条记录!")
} else {
fmt.Println("存在这条记录!")
}
}
用主键ID最效率的查询
// 通过id查找学生对象 高效的查询方式!
func GetStudentById(sid string) (*StudentTest, bool, error) {
stuObj := StudentTest{}
// 使用这种查询效率高
// TODO 注意这里需要将地址传进去
has, err := model.DB.Walk.Table("student_test").Id(sid).Get(&stuObj)
// TODO 也可以指定返回的字段
// has, err := model.DB.Walk.Table("student_test").Id(sid).Cols("sid", "name", "score")
return &stuObj, has, err
}
// 具体实现
// 根据主键id查询单条数据 TODO 效率高的方式
func TestGetStudentById(t *testing.T) {
sid := "2"
stuObj, has, err := aaa_module.GetStudentById(sid)
if err != nil {
fmt.Println("get student by i·d raise error! ", err.Error())
} else {
if !has {
fmt.Println("can't get stuObj by that sid!")
} else {
fmt.Println(fmt.Sprintf("get the stuObj: sid: %s, Name: %s, Age: %d, Score: %.3f, ClassId %s ", stuObj.Id, stuObj.Name, stuObj.Age, stuObj.Score, stuObj.ClassId))
}
}
}
根据条件查询单条数据
// 根据条件查询单条数据, 接收一个StudentTest结构体指针作为条件
func GetStuObj(stu *StudentTest) (*StudentTest, bool, error) {
has, err := model.DB.Walk.Table("student_test").Get(stu)
return stu, has, err
}
// 具体实现
// 根据不同的条件查询单条数据
// TODO: 这种方式很强大,不需要构建繁杂的查询,有的话直接将结果存入 "条件结构体即可"
func TestGetStudent(t *testing.T) {
stu := &aaa_module.StudentTest{Name: "naruto"}
stu, has, err := aaa_module.GetStuObj(stu)
// 判断有没有出错
if err != nil {
fmt.Println("get student raises error! ", err.Error())
// 判断有没有查到数据
} else if !has {
fmt.Println("can't get stuObj!")
// 拿到结果的话直接将结果放在定义好的结构体中
} else {
fmt.Println("stuObj: ", stu.Id, stu.Name, stu.Score)
}
}
返回所有符合条件的记录
// 返回所有符合条件的记录 ———— 条件是固定死的
func FindSlice() ([]StudentTest, error) {
stuObjSlice := make([]StudentTest, 1)
err := model.DB.Walk.Table("student_test").Where("age > ? and score < ?", 10, 100).Find(&stuObjSlice)
return stuObjSlice, err
}
// 具体实现
// 返回所有符合条件的记录 ———— 条件是固定死的
func TestFind(t *testing.T) {
stuObjSlice, err := aaa_module.FindSlice()
if err != nil {
fmt.Println("err>>> ", err)
} else {
fmt.Println("stuObjSlice: ", stuObjSlice)
}
}
* 自定义条件查询所有记录
// 返回所有符合条件的记录 —— 自定义条件查询
// 接收一个 map[string]interface{} key是查询的字段,value是查询的值
func FindSliceBySelfCondition(queryMap map[string]interface{}) ([]*StudentTest, error) {
var retSlice []*StudentTest
// ****** 其实本质上就是一个map[string]interface{} ******
err := model.DB.Walk.Table("student_test").Where(queryMap).Find(&retSlice)
return retSlice, err
}
// 具体实现
// 返回所有符合条件的记录 —— 自定义条件查询
func TestFindSliceBySelfCondition(t *testing.T){
// ****** 构建查询条件查询的条件 ******
queryMap := map[string]interface{}{
"name": "naruto",
"age": 22,
}
retSlice, err := aaa_module.FindSliceBySelfCondition(queryMap)
if err != nil{
fmt.Println("err: ", err)
}else{
fmt.Println("retSlice: ", retSlice)
// 遍历结果
for _, obj := range retSlice{
fmt.Println("obj: ", obj)
}
}
}
连表查询+执行原生SQL
func JoinQuery() error {
// 连表查询语句
sqlStr := "select t1.sid as stu_id, t1.name as stu_name,t1.age as stu_age, t1.score as stu_score, t2.name as class_name, t2.cid as class_id " +
"from student_test as t1 " +
"left join s_class_test_model as t2 " +
"on t1.class_id = t2.cid"
// 1、QueryInterface TODO: 左表中有但右表中没有的会显示nil ———— 推荐这种方式
rets, err := model.DB.Walk.Table("student_test").QueryInterface(sqlStr)
fmt.Println("QueryInterface: ret: ", rets)
for _, mp := range rets {
fmt.Println("mp: ", mp["class_id"], mp["class_name"], mp["stu_name"])
}
// 2、QueryString TODO 左表中有但右表中没有的不会显示
results, err := model.DB.Walk.Table("student_test").QueryString(sqlStr)
fmt.Println("QueryString ret: ", results)
for _, mp := range results {
fmt.Println("mp: ", mp["class_id"], mp["class_name"], mp["stu_name"])
}
return err
}
// 具体实现
// 连表查询 TODO 执行原生SQL的方式!
func TestJoinQuery(t *testing.T){
err := aaa_module.JoinQuery()
if err != nil{
fmt.Println("err: ", err)
}else{
fmt.Println("join Query successfully")
}
}
更新记录
// 更新记录
/*
更新通过engine.Update()实现,可以传入结构指针或map[string]interface{}。对于传入结构体指针的情况,xorm只会更新非空的字段。
如果一定要更新空字段,需要使用Cols()方法显示指定更新的列。使用Cols()方法指定列后,即使字段为空也会更新
*/
func UpdateData() (int64, error){
affected, err := model.DB.Walk.Table("student_test").Where("name=?", "naruto").Update(&StudentTest{Score: 123})
// 或者
// affected, err := model.DB.Walk.Table("student_test").ID("2").Update(&StudentTest{Score: 666})
return affected, err
}
// 具体实现
func TestUpdate(t *testing.T){
affected, err := aaa_module.UpdateData()
if err != nil{
fmt.Println("err: ", err)
}else{
fmt.Println("修改成功!affected: ", affected)
}
}
删除记录
// 删除记录
func DeleteData() (int64, error){
affected, err := model.DB.Walk.Table("student_test").Where("name=?", "sasuke").Delete(&StudentTest{})
return affected, err
}
事务的使用
1、官方中文文档:https://gobook.io/read/gitea.com/xorm/manual-zh-CN/chapter-10/index.html
2、项目中有一个 engine.Transtraction方法——其实就是对上面的方法做了一些封装!
金币领取接口用到了事务的操作:
// 事务中执行
resultResponse, err := model.DB.Vanguard.Transaction(func(session *xorm.Session) (interface{}, error) {
retResponse, err := service.handlePostGoldenReward(session)
if err != nil {
return nil, err
}
return retResponse, nil
})
可以看一下 Transaction 方法的源码:
package xorm
// Transaction Execute sql wrapped in a transaction(abbr as tx), tx will automatic commit if no errors occurred
func (engine *Engine) Transaction(f func(*Session) (interface{}, error)) (interface{}, error) {
session := engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return nil, err
}
result, err := f(session)
if err != nil {
return nil, err
}
if err := session.Commit(); err != nil {
return nil, err
}
return result, nil
}
代码流程:
(1) "首先需要知道Transaction的调用者是xorm的引擎 *Engine",在我们的业务代码中对应的引擎是:model.DB.Vanguard
(2) "Transaction函数的参数是一个函数"
(3) "整个Transaction函数的返回值是 interface{}, error"
(4) "里面作为参数的函数f,他的入参是 *Session,返回值也是 interface{}, error"
(5) 在Transaction函数中已经初始化了一个session了,所以我们在业务函数中没有必要再创建一个session对象
(6) 业务方法 service.handlePostGoldenReward的定义如下:
func (g *GoldenEggService) handlePostGoldenReward(session *xorm.Session) (map[string]interface{}, error)
"特别注意:这个业务方法一定要把session作为参数,里面涉及到的所有orm的操作也都要将session作为参数处理!并且在中途一定不要更 改session对象!中途修改session对象的话可能会导致事务处理失败!"
(7) "业务方法返回的是 map[string]interface{},但是整个事务返回的是一个interface{},如果后面需要对结果再进行一下处理的话得做类型断言:"
handleRet := retResponse.(map[string]interface{})
(8) "像业务中那样,如果事务得到的结果就是接口返回的结果就不用做类型断言了,直接用封装好的方法返回结果即可:"
handler.SendResponse(c, errnum.OK, resultResponse)
return
xorm在项目中使用乐观锁❗️
主要是在做appserver 010 的大转盘优化需求的时候,如果同时请求获取宝箱信息、更新宝箱状态这两个接口会有并发修改数据的问题~
❗️下面这些是从记录我做过的appserver相关的业务中的那个文档截取的:
乐观锁的问题
乐观锁是数据库级别的一个锁,为了避免并发情况下多个请求同时对同一条数据进行修改而加的锁。
设计接口的时候一定要考虑客户端并发请求的情况。。。
我们这里设计之初也应该考虑并发请求的问题,服务端这里以后做涉及到敏感数据的操作,比如用户金币、奖励的领取状态也需要提前考虑到并发的情况,在设计开始就做好预处理
项目中使用乐观锁
1、在model中定义一个Version字段
// 在定义model时创建一个Version字段~~❗️注意数据库中也必须有一个对应的version字段!
type UserWithdrawCouponModel struct {
UserId string `json:"user_id" xorm:"pk user_id"`
CouponValue int `json:"coupon_value" xorm:"coupon_value"`
CouponTotal int `json:"coupon_total" xorm:"coupon_total"`
// 乐观锁使用的字段
Version int `json:"version" xorm:"version"` // 乐观锁
Updated time.Time `json:"updated" xorm:"updated"`
Created time.Time `json:"created" xorm:"created"`
}
2、在数据库中必须定义一个version字段
官方文档:https://gobook.io/read/gitea.com/xorm/manual-zh-CN/chapter-06/1.lock.html