Gorm 操作 MySQL

安裝

go get -u gorm.io/gorm

連接 MySQL

創建 model.go 文件,代碼如下:

package model

import (
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
 "gorm.io/gorm/logger"
 "gorm.io/gorm/schema"
 "time"
)

var db *gorm.DB

const dsn = "root:123456@tcp(127.0.0.1:3306)/db_gorm_example?charset=utf8mb4&parseTime=True&loc=Local"

func Setup() {
 var err error
 db, err = gorm.Open(
  mysql.New(mysql.Config{
   DSN:                       dsn,   // 連接地址
   DefaultStringSize:         191,   // string 類型字段的默認長度
   DisableDatetimePrecision:  true,  // 禁用 datetime 精度,MySQL 5.6 之前的數據庫不支持
   DontSupportRenameIndex:    true,  // 重命名索引時採用刪除並新建的方式,MySQL 5.7 之前的數據庫和 MariaDB 不支持重命名索引
   DontSupportRenameColumn:   true,  // 用 `change` 重命名列,MySQL 8 之前的數據庫和 MariaDB 不支持重命名列
   SkipInitializeWithVersion: false, // 根據當前 MySQL 版本自動配置
  }),
  &gorm.Config{
   Logger: logger.Default.LogMode(logger.Info), // 日誌輸出級別
   NamingStrategy: schema.NamingStrategy{
    TablePrefix:   "sys_", // 表名前綴,`User` 的表名應該是 `sys_users`
    SingularTable: true,   // 使用單數表名,啓用該選項,此時,`User` 的表名應該是 `sys_user`
   },
   DisableForeignKeyConstraintWhenMigrating: false, //在 AutoMigrate 或 CreateTable 時,GORM 會自動創建外鍵約束
  })
 if err != nil {
  panic(err)
 }
 setConnectionPool()
}

// 設置連接池
func setConnectionPool() {
 if db != nil {
  sqlDB, err := db.DB()
  if err != nil {
   panic(err)
  }
  sqlDB.SetMaxIdleConns(10)                  // 空閒連接池中連接的最大數量
  sqlDB.SetMaxOpenConns(10)                  // 打開數據庫連接的最大數量
  sqlDB.SetConnMaxLifetime(60 * time.Minute) // 連接可複用的最大時間
  err = sqlDB.Ping()
  if err != nil {
   panic(err)
  }
 }
}

sqlDB 對象是包含多個 idle(空閒)open(打開) 數據庫連接的連接池。idle 通過 SetMaxIdleConns 設置空閒連接池中連接的最大數量;open 通過 SetMaxOpenConns 設置打開數據庫連接的最大數量。當使用連接來執行數據庫任務時,該連接會被標記爲 open 。任務完成後,連接將變爲 idle。此外,還可以通過 SetConnMaxLifetime 設置連接池中連接可複用的最大時間,到期後會斷開連接,下次使用需要重新連接。

爲什麼要使用連接池?

連接複用,提升性能:數據庫連接本質就是一個 socket 連接。頻繁建立、斷開連接會降低系統的性能。所以可以把數據庫連接緩存起來複用,在規定可複用時間內(ConnMaxLifetime)可以直接重用這些連接。這就是連接池。

減少等待時間:在連接池中,創建連接後,將其放置在池中,並再次使用它,因此不必建立新的連接。如果使用了所有連接,則會建立一個新連接(不可以超過 MaxOpenConns )並將其添加到池中。減少了用戶必須等待建立與數據庫的連接的時間。

定義模型

首先定義一個班級模型,class.go

package model

import "gorm.io/gorm"

// Class 班級
type Class struct {
 gorm.Model
 // 班級名稱,類型爲varchar(20),不能爲空,唯一約束
 ClassName string `json:"class_name" gorm:"size:20;not null;unique;comment:班級名稱"`
}

// 插入一行記錄
func (c *Class) Create() error {
 return db.Create(c).Error
}

學生模型,student.go

package model

import "gorm.io/gorm"

// Student 學生
type Student struct {
 gorm.Model
 // 學生姓名,類型爲varchar(20),不能爲空
 Name    string `json:"name" gorm:"type:varchar(20);not null;comment:學生姓名"`
 ClassID uint   `json:"-" gorm:"not null;comment:學生所屬班級ID"`
 Class   Class  `json:"class"`
}

// 插入一行記錄
func (s *Student) Create() error {
 return db.Create(s).Error
}

// 批量插入
func BatchCreateStudent(students []Student) error {
 return db.Create(&students).Error
}

// 查詢所有學生信息
func QueryAllStudentInfo() ([]Student, error) {
 var students []Student
 err := db.Model(&Student{}).
  Preload("Class"). // 預加載班級信息
  Find(&students).Error
 if err != nil {
  return nil, err
 }
 return students, nil
}

// 查詢指定學生信息
func QueryStudentInfoByID(studentID uint) (*Student, error) {
 var student Student
 err := db.Model(&Student{}).Where("id = ?", studentID).
  Preload("Class"). // 預加載班級信息
  Take(&student).Error
 if err != nil {
  return nil, err
 }
 return &student, nil
}

// 軟刪除
func DelStudent(studentID uint) error {
 return db.Delete(&Student{}).Where("id = ?", studentID).Error
}

// 永久刪除
func UnscopedDelStudent(studentID uint) error {
 return db.Unscoped().Delete(&Student{}).Where("id = ?", studentID).Error
}

班級和學生是一對多的關係,如果想要 gorm 自動爲我們創建外鍵約束,可以使用 AutoMigrate 自動遷移功能。在model.go 中補充:

func Setup() {
 ...
 autoMigrate(&Class{}&Student{})
}

// 自動遷移
func autoMigrate(tables ...interface{}) {
 // 創建表時添加後綴
 db.Set("gorm:table_options",
  "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4").
  AutoMigrate(tables...)
 // AutoMigrate 會創建表、缺失的外鍵、約束、列和索引。
 // 如果大小、精度、是否爲空可以更改,則 AutoMigrate 會改變列的類型。
}

運行測試

最後在 main.go 中測試上面的代碼:

package main

import (
 "gorm-example/model"
)

func main() {
 // 初始化連接
 model.Setup()
 // 創建一行學生記錄(gorm會自動幫我們創建對應的班級記錄)
 student := model.Student{
  Name: "小米",
  Class: model.Class{
   ClassName: "一年級",
  },
 }
 student.Create()
 select {}
}

輸出日誌:查看數據庫:可以看到 gorm 在爲我們創建表的時候加入了 idcreated_atupdated_atdeleted_at 四個字段,這其實是在定義模型的時候,我們在結構體中嵌入的 gorm.Model 起的作用,可以看看 gorm.Model 結構體的源碼:也正是有了 DeletedAt 的存在,我們的數據庫表纔可以實現軟刪除。

測試使用 gorm 來查詢數據,更改我們的 main.go

package main

import (
 "fmt"
 "gorm-example/model"
)

func main() {
 // 初始化連接
 model.Setup()
 // 查詢所有學生數據
 students, err := model.QueryAllStudentInfo()
 if err != nil {
  panic(err)
 }
 for i, student := range students {
  fmt.Printf(
   "---------第%d條記錄---------\n"+
    "學生ID:%d\n"+
    "學生姓名:%s\n"+
    "學生班級:%s\n"+
    "---------------------------\n",
   i+1,
   student.ID,
   student.Name,
   student.Class.ClassName)
 }
 select {}
}

輸出結果:

Gorm 中使用事務

默認 gorm 是會在事務裏執行寫入操作的(創建、更新、刪除)。當然,如果不需要,我們也可以禁用它,官方說是會獲得大約 30%+ 性能提升。禁用默認事務只要在 model.go 中的 gorm.Config 加入SkipDefaultTransaction: true 即可。

func Setup() {
 var err error
 db, err = gorm.Open(
  mysql.New(mysql.Config{
   DSN:                       dsn,   // 連接地址
   DefaultStringSize:         191,   // string 類型字段的默認長度
   DisableDatetimePrecision:  true,  // 禁用 datetime 精度,MySQL 5.6 之前的數據庫不支持
   DontSupportRenameIndex:    true,  // 重命名索引時採用刪除並新建的方式,MySQL 5.7 之前的數據庫和 MariaDB 不支持重命名索引
   DontSupportRenameColumn:   true,  // 用 `change` 重命名列,MySQL 8 之前的數據庫和 MariaDB 不支持重命名列
   SkipInitializeWithVersion: false, // 根據當前 MySQL 版本自動配置
  }),
  &gorm.Config{
   Logger: logger.Default.LogMode(logger.Info), // 日誌輸出級別
   NamingStrategy: schema.NamingStrategy{
    TablePrefix:   "sys_", // 表名前綴,`User` 的表名應該是 `sys_users`
    SingularTable: true,   // 使用單數表名,啓用該選項,此時,`User` 的表名應該是 `sys_user`
   },
   DisableForeignKeyConstraintWhenMigrating: false, //在 AutoMigrate 或 CreateTable 時,GORM 會自動創建外鍵約束
   SkipDefaultTransaction:                   true,  //禁用默認事務
  })
 if err != nil {
  panic(err)
 }
 setConnectionPool()
 autoMigrate(&Class{}&Student{})
}

來一個需要使用事務的需求吧,定義一個模型 account.go

package model

import (
 "errors"
 "gorm.io/gorm"
 "gorm.io/gorm/clause"
)

// Account 賬戶
type Account struct {
 gorm.Model
 Amount uint `json:"amount" gorm:"not null;default:0;comment:金額"`
}

// 轉賬屬於一系列操作,只能全部成功,必須在事務中執行
// 這裏只做示例,忽略餘額不夠的情況
func Transfer(remitter, payee uint, money int) error {
 // 使用事務
 return db.Transaction(func(tx *gorm.DB) error {
  // 加排他鎖
  var accounts []Account
  err := tx.
   Clauses(clause.Locking{Strength: "UPDATE"}).
   Model(&Account{}).
   Where("id in (?)"[]uint{remitter, payee}).
   Find(&accounts).Error
  if err != nil {
   return err
  }
  if len(accounts) != 2 {
   return errors.New("賬號異常")
  }
  remitterOK := false
  payeeOK := false
  for _, account := range accounts {
   switch account.ID {
   case remitter:
    // 匯款人
    err = tx.Model(&Account{}).
     Where("id = ?", remitter).
     Update("amount", gorm.Expr("amount - ?", money)).Error
    if err != nil {
     return err
    }
    remitterOK = true
   case payee:
    // 收款人
    err = tx.Model(&Account{}).
     Where("id = ?", payee).
     Update("amount", gorm.Expr("amount + ?", money)).Error
    if err != nil {
     return err
    }
    payeeOK = true
   }
  }
  if remitterOK && payeeOK {
   // 當返回 nil 的時候纔會提交事務
   return nil
  } else {
   return errors.New("轉賬失敗")
  }
 })
}

同樣,讓自動遷移爲我們自動建表,在 model.goSetup() 函數中的 autoMigrate 補充 &Account{}

autoMigrate(&Class{}&Student{}&Account{})

手動插入一些數據如下:main.go 中進行轉賬操作,更改如下:

package main

import (
 "gorm-example/model"
)

func main() {
 // 初始化連接
 model.Setup()
 // (1)轉賬給(2) 100元
 err := model.Transfer(1, 2, 100)
 if err != nil {
  panic(err)
 }
 select {}
}

查看變更:

最後

關於 gorm 操作 MySQL 的代碼可以從 

https://github.com/togettoyou/go-one-server

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3V5lgqJJdMaxeN5zfmVJGQ