前言
最近寫業務用到xorm操作postgreSQL,抽空寫了一些平時會用到的常用的操作,好腦筋不如好筆記。
xorm
參考文檔
相關技術博客
上面那個作者的每日一庫系列跟Go的相關文章挺不錯:Go每日一庫、Go系列文章
操作總結
“初始化引擎操作”
下面使用的 model.DB.Walk 實際上是在項目啟動時就已經啟動的引擎,自己做本地測試的話可以在本地做初始化的工作:
// 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