Gorm 操作 MySQL

Posted 寻寻觅觅的Gopher

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gorm 操作 MySQL相关的知识,希望对你有一定的参考价值。

强烈推荐阅读官方文档https://gorm.io/zh_CN/docs/

安装

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 操作 MySQLGorm 操作 MySQL可以看到 gorm 在为我们创建表的时候加入了 idcreated_atupdated_atdeleted_at 四个字段,这其实是在定义模型的时候,我们在结构体中嵌入的 gorm.Model 起的作用,可以看看 gorm.Model 结构体的源码:Gorm 操作 MySQL也正是有了 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 操作 MySQL

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(12100)
 if err != nil {
  panic(err)
 }
 select {}
}

查看变更:

最后

关于 gorm 操作 MySQL 的代码可以从 

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

或阅读原文获取。

To Be Continued


以上是关于Gorm 操作 MySQL的主要内容,如果未能解决你的问题,请参考以下文章

Gorm框架学习--入门

Go语言使用gorm对MySQL进行性能测试

GORM基本使用

golang go-sql-driver gorm 数据库报错 bad connection

如何让gorm输出执行的sql

GORM的简单使用