Golang 使用gorm添加數據庫排他鎖,for update

適用於先讀後更新的數據競爭場景,且應該將加鎖操作放到事務中,防止鎖被自動釋放,原因參考mysql doc

func UpdateUser(db *gorm.DB, id int64) error {
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()
  if err := tx.Error; err != nil {
    return err
  }
  user := User{}
  // 鎖住指定 id 的 User 記錄
  if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&user, id).Error; err != nil {
    tx.Rollback()
    return err
  }
  // 更新操作...
  // commit事務,釋放鎖
  if err := tx.Commit().Error; err != nil {
    return err
  }
  return nil
}

sync.Mutex解法(效率較低):

var lock sync.Mutex
func UpdateUser(db *gorm.DB, id int64) error {
  lock.Lock()
  // 數據庫操作...
  lock.Unlock()
  return nil
}

參考

doc

補充:Golang數據庫編程之GORM模型定義與數據庫遷移

在開發應用程序時,一般而言,我們是先設計好數據表,再使用開發語言建立對應的數據模型,不過,我們今天要講的是一個逆向操作的過程,即如何通定義GORM框架的數據模型,然後再通過執行GROM框架編寫的應用程序,用定義好數據模型在數據庫中創建對應的數據表。

因此需要先講講怎麼定義GORM的數據模型。

模型定義

一般來說,我們說GROM的模型定義,是指定義代表一個數據表的結構體(struct),然後我們可以使用GROM框架可以將結構體映射為相對應的關系數據庫的數據表,或者查詢數據表中的數據來填充結構體,如下所示,我們定義瞭一個名為Post的結構體。

type Post struct {
PostId int
Uid int
Title string
Content string
Type int
CreatedAt time.Time
UpdatedAt time.Time
}

創建好一個結構體隻是第一步,不過先不著急要怎麼去創建數據表,我們要先瞭解一下結構體與數據表之間的映射規則,主要有以下幾點:

Struct tags

我們知道,Go語言的結構體支持使用tags為結構體的每個字段擴展額外的信息,如使用標準庫encoding/json包進行JSON編碼時,便可以使用tags進行編碼額外信息的擴展。

GROM框架有自己的一個tags約定,如下所示:

Column 指定列名

Type 指定列數據類型

Size 指定列大小, 默認值255

PRIMARY_KEY 將列指定為主鍵

UNIQUE 將列指定為唯一

DEFAULT 指定列默認值

PRECISION 指定列精度

NOT NULL 將列指定為非 NULL

AUTO_INCREMENT 指定列是否為自增類型

INDEX 創建具有或不帶名稱的索引, 如果多個索引同名則創建復合索引

UNIQUE_INDEX 和 INDEX 類似,隻不過創建的是唯一索引

EMBEDDED 將結構設置為嵌入

EMBEDDED_PREFIX 設置嵌入結構的前綴

– 忽略此字段

GROM還支持一些關聯數據表的tags約定,有機會我講講GROM數據表關聯的時候,會說到的。

上面列出的GORM支持的tags,方便我們定制結構體字段到數據表字段之間的映射規則,下面的代碼,我們給Post結構體定制一些tags擴展,如下:

type Post struct {
PostId int `gorm:"primary_key;auto_increment"`
Uid int `gorm:"type:int;not null"`
Title string `gorm:"type:varchar(255);not null"`
Content string `gorm:"type:text;not null"`
Type uint8 `gorm:"type:tinyint;default 1;not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}

從上面的例子我們可以看出GORM為數據模型的字段定義tags的格式,每個字段可以用多個類型的tags信息,不同的tag之間用分號分隔。

慣例

除瞭上面講的tags定義瞭字段之間的映射規則外,Go將結構體映射為關系型數據表時,還有自己的一套慣例,或稱為約定,主要有以下幾點:

主鍵

GROM的約定中,一般將數據模型中的ID字段映射為數據表的主鍵,如下面定義的TestModel,ID為主鍵,TestModel的ID的數據類型為string,如果ID的數據類型為int,則GROM還會為該設置AUTO_INCREMENT,使用ID成為自增主鍵。

type TestModel struct{
ID int
Name string
}

當然,我們也可以自定義主鍵字段的名稱,如上面的Post結構體,我們設置瞭PostId字段為主鍵,如果我們定義瞭其他字段為主鍵,那麼,就算結構體中仍有ID字段,GROM框架也不會把ID字段當作主鍵瞭。

type Post struct {
ID int
PostId int `gorm:"primary_key;auto_increment"`
Uid int `gorm:"type:int;not null"`
Title string `gorm:"type:varchar(255);not null"`
Content string `gorm:"type:text;not null"`
Type uint8 `gorm:"type:tinyint;default 1;not null"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
}

所以,我們在Post結構體中加一個ID字段,PostId字段仍是主鍵,下面是在數據中使用desc posts語句打印出來的結果:

數據表映射規則

當我們使用結構體創建數據表時,數據表的名稱默認為結構體的小寫復數形式,如結構體Post對應的數據表名稱為posts,當然我們也可以自己指定結構體對應的數據表名稱,而不是用默認的。

為結構體加上TableName()方法,通過這個方法可以返回自定義的數據表名,如下:

//指定Post結構體對應的數據表為my_posts
func (p Post) TableName() string{
return "my_posts"
}

數據表前綴

除瞭指定數據表名外,我們也可以重寫gorm.DefaultTableNameHandler這個變量,這樣可以為所有數據表指定統一的數據表前綴,如下:

gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
return "tb_" + defaultTableName;
}

這樣的話,通過結構體Post創建的數據表名稱則為tb_posts。

字段映射規則

結構體到數據表的名稱映射規則為結構體名稱的復數,而結構體的字段到數據表字段的默認映射規則是用下劃線分隔每個大寫字母開頭的單詞,如下:

type Prize struct {
ID int
PrizeName string
}

上面的結構體Prize中的PrizeName字段對應的數據表為prize_name,但我們把PrizeName改為Prizename時,則對應的數據表字段名稱為prizename,這是為因為隻分隔大寫字段開頭的單詞。

當然,我們也可以為結構體的某個字段定義tags擴展信息,這樣結構體字段到數據表字段的映規則就在tags中定義。

時間點追蹤

前面我們說過,如果結構體中有名稱為ID字段,則GORM框架會把該字段作為數據表的主鍵,除此之外,如果結構體中有CreatedAt,UpdatedAt,DeletedAt這幾個字段的話,則GROM框架也會作一些特殊處理,規則如下:

CreatedAt:新增數據表記錄的時候,會自動寫入這個字段。 UpdatedAt:更新數據表記錄的時候,會自動更新這個字段。 DeletedAt:當執行軟刪除的時候,會自動更新這個字段,表示刪除時間

gorm.Model

由於如果結構體中有ID,CreatedAt,UpdatedAt,DeletedAt這幾個比較通用的字段,GORM框架會自動處理這幾個字段,所以如果我們結構體需要這幾個字段時,我們可以直接在自定義結構體中嵌入gorm.Model結構體,gorm.Model的結構體如下:

type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
}

所以,如果我們在結構體Prize中嵌入gorm.Model,如下:

type Prize struct{
gorm.Model
Name string
}

這樣的話,則結構體Prize包含有五個字段瞭。

數據庫遷移

我們這裡所說的數據庫遷移,即通過使用GROM提供的一系列方法,根據數據模型定義好的規則,進行創建、刪除數據表等操作,也就是數據庫的DDL操作。

GORM提供對數據庫進行DDL操作的方法,主要以下幾類:

數據表操作

//根據模型自動創建數據表
func (s *DB) AutoMigrate(values ...interface{}) *DB 
//根據模型創建數據表
func (s *DB) CreateTable(models ...interface{}) *DB
//刪除數據表,相當於drop table語句
func (s *DB) DropTable(values ...interface{}) *DB 
//相當於drop table if exsist 語句
func (s *DB) DropTableIfExists(values ...interface{}) *DB 
//根據模型判斷數據表是否存在
func (s *DB) HasTable(value interface{}) bool

列操作

//刪除數據表字段
func (s *DB) DropColumn(column string) *DB
//修改數據表字段的數據類型
func (s *DB) ModifyColumn(column string, typ string) *DB

索引操作

//添加外鍵
func (s *DB) AddForeignKey(field string, dest string, onDelete string, onUpdate string) *DB
//給數據表字段添加索引
func (s *DB) AddIndex(indexName string, columns ...string) *DB
//給數據表字段添加唯一索引
func (s *DB) AddUniqueIndex(indexName string, columns ...string) *DB

數據遷移簡單代碼示例

註意,下面示例程序中db變量代表gorm.DB對象,其初始化過程本篇不講瞭。

type User struct {
Id int //對應數據表的自增id
Username string
Password string
Email string
Phone string
}
func main(){
db.AutoMigrate(&Post{},&User{})//創建posts和users數據表
db.CreateTable(&Post{})//創建posts數據表
db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&Post{})//創建posts表時指存在引擎
db.DropTable(&Post{},"users")//刪除posts和users表數據表
db.DropTableIfExists(&Post{},"users")//刪除前會判斷posts和users表是否存在
//先判斷users表是否存在,再刪除users表
if db.HasTable("users") {
db.DropTable("users")
}
//刪除數據表字段
db.Model(&Post{}).DropColumn("id")
//修改字段數據類型
db.Model(&Post{}).ModifyColumn("id","varchar(255)")
//建立posts與users表之間的外鍵關聯
db.Model(&Post{}).AddForeignKey("uid", "users(id)", "RESTRICT", "RESTRICT")
//給posts表的title字段添加索引
db.Model(&Post{}).AddIndex("index_title","title")
//給users表的phone字段添加唯一索引
db.Model(&User{}).AddUniqueIndex("index_phone","phone")
}

小結

可能你會問,直接在數據庫中進行數據表創建、刪除等操作不就行瞭嗎?為什麼要在應用程序裡去做這些操作呢?因為有些時候,我們不一定能登錄到數據庫系統當中,又或者,我們需要開發一個可以管理數據庫的應用程序,這時候,GROM框架提供的這些數據庫遷移的能便派上用場瞭。

推薦閱讀: