[go-每日一库]golang-gorm模板-连接-自动建表-错误处理(二)

原文链接:
1.(二)GORM模板定义 https://www.cnblogs.com/infodriven/p/16348171.html
2.(三)GORM连接数据库 https://www.cnblogs.com/infodriven/p/16348215.html
3.(十三)GORM 自动建表(Migration特性) https://www.cnblogs.com/infodriven/p/16351624.html
4.(十四)GORM 错误处理 https://www.cnblogs.com/infodriven/p/16351632.html

1.gorm的模板定义

1.1 介绍

ORM框架操作数据库都需要预先定义模型,模型可以理解成数据模型,作为操作数据库的媒介。
例如:

  • 从数据库读取的数据会先保存到预先定义的模型对象,然后我们就可以从模型对象得到我们想要的数据。
  • 插入数据到数据库也是先新建一个模型对象,然后把想要保存的数据先保存到模型对象,然后把模型对象保存到数据库。
    在golang中gorm模型定义是通过struct实现的,这样我们就可以通过gorm库实现struct类型和mysql表数据的映射。

提示:gorm负责将对模型的读写操作翻译成sql语句,然后gorm再把数据库执行sql语句后返回的结果转化为我们定义的模型对象。

1.2 gorm模型定义

gorm模型定义主要就是在struct类型定义的基础上增加字段标签说明实现,下面看个完整的例子。
假如有个商品表,表结构如下:

CREATE TABLE `food` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID,商品Id',
  `name` varchar(30) NOT NULL COMMENT '商品名',
  `price` decimal(10,2) unsigned  NOT NULL COMMENT '商品价格',
  `type_id` int(10) unsigned NOT NULL COMMENT '商品类型Id',
  `createtime` int(10) NOT NULL DEFAULT 0 COMMENT '创建时间',
   PRIMARY KEY (`id`)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8

模型定义如下

//字段注释说明了gorm库把struct字段转换为表字段名长什么样子。
type Food struct {
	Id         int  //表字段名为:id
	Name       string //表字段名为:name
	Price      float64 //表字段名为:price
	TypeId     int  //表字段名为:type_id
	//字段定义后面使用两个反引号``包裹起来的字符串部分叫做标签定义,这个是golang的基础语法,不同的库会定义不同的标签,有不同的含义
	CreateTime int64 `gorm:"column:createtime"`  //表字段名为:createtime
}

默认gorm对struct字段名使用Snake Case命名风格转换成mysql表字段名(需要转换成小写字母)。
根据gorm的默认约定,上面例子只需要使用gorm:"column:createtime"标签定义为CreateTime字段指定表字段名,其他使用默认值即可。

*提示:Snake Case命名风格,就是各个单词之间用下划线(_)分隔,例如: CreateTime的Snake Case风格命名为create_time
*

1.3 gorm模型标签

通过上面的例子,大家看到可以通过类似gorm:"column:createtime"这样的标签定义语法,定义struct字段的列名(表字段名)。
gorm标签语法:gorm:"标签定义"

标签定义部分,多个标签定义可以使用分号(;)分隔

例如定义列名:
gorm:"column:列名"
gorm常用标签如下:

1.4 定义表名

可以通过定义struct类型的TableName函数实现定义模型的表名
接上面的例子:

//设置表名,可以通过给Food struct类型定义 TableName函数,返回一个字符串作为表名
func (v Food) TableName() string {
    return "food"
}
建议: 默认情况下都给模型定义表名,有时候定义模型只是单纯的用于接收手写sql查询的结果,这个时候是不需要定义表名;手动通过gorm函数Table()指定表名,也不需要给模型定义TableName函数。

1.5 gorm.Model

GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt。

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

更多模型定义,请参考:https://gorm.io/docs/models.html

以将它嵌入到我们的结构体中,就以包含这几个字段,类似继承的效果。
例子:

type User struct {
  gorm.Model // 嵌入gorm.Model的字段
  Name string
}

1.6 自动更新时间

GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。如果定义了这种字段,GORM 在创建、更新时会自动填充当前时间。
要使用不同名称的字段,您可以配置 autoCreateTime、autoUpdateTime 标签
如果想要保存 UNIX(毫/纳)秒时间戳,而不是 time,只需简单地将 time.Time 修改为 int 即可。
例子:

type User struct {
  CreatedAt time.Time // 默认创建时间字段, 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 默认更新时间字段, 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 自定义字段, 使用时间戳填纳秒数充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` //自定义字段, 使用时间戳毫秒数填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      //自定义字段, 使用时间戳秒数填充创建时间
}

2.gorm连接数据库

gorm支持多种数据库,这里主要介绍mysql,连接mysql主要有两个步骤:

  • 配置DSN (Data Source Name)
  • 使用gorm.Open连接数据库

2.1 配置DSN (Data Source Name)

gorm库使用dsn作为连接数据库的参数,dsn翻译过来就叫数据源名称,用来描述数据库连接信息。一般都包含数据库连接地址,账号,密码之类的信息。
DSN格式:

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

mysql连接dsn例子:

//mysql dsn格式
//涉及参数:
//username   数据库账号
//password   数据库密码
//host       数据库连接地址,可以是Ip或者域名
//port       数据库端口
//Dbname     数据库名
username:password@tcp(host:port)/Dbname?charset=utf8&parseTime=True&loc=Local

//填上参数后的例子
//username = root
//password = 123456
//host     = localhost
//port     = 3306
//Dbname   = tizi365
//后面K/V键值对参数含义为:
//  charset=utf8 客户端字符集为utf8
//  parseTime=true 支持把数据库datetime和date类型转换为golang的time.Time类型
//  loc=Local 使用系统本地时区
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local

//gorm 设置mysql连接超时参数
//开发的时候经常需要设置数据库连接超时参数,gorm是通过dsn的timeout参数配置
//例如,设置10秒后连接超时,timeout=10s
//下面是完成的例子
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local&timeout=10s

//设置读写超时时间
// readTimeout - 读超时时间,0代表不限制
// writeTimeout - 写超时时间,0代表不限制
root:123456@tcp(localhost:3306)/tizi365?charset=utf8&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=60s

2.2 使用gorm.Open连接数据库(v2)

有了上面配置的dsn参数,就可以使用gorm连接数据库,下面是连接数据库的例子

package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main()  {
    //配置MySQL连接参数
    username := "root"  //账号
    password := "123456" //密码
    host := "127.0.0.1" //数据库地址,可以是Ip或者域名
    port := 3306 //数据库端口
    Dbname := "tizi365" //数据库名
    timeout := "10s" //连接超时,10秒

    //拼接下dsn参数, dsn格式可以参考上面的语法,这里使用Sprintf动态拼接dsn参数,因为一般数据库连接参数,我们都是保存在配置文件里面,需要从配置文件加载参数,然后拼接dsn。
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
    //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }
    //延时关闭数据库连接
    defer db.Close()
}

2.3 gorm调试模式

为了方便调试,了解gorm操作到底执行了怎么样的sql语句,开发的时候需要打开调试日志,这样gorm会打印出执行的每一条sql语句。

使用Debug函数执行查询即可
例子:

result := db.Debug().Where("username = ?", "tizi365").First(&u)

2.4 gorm连接池

在高并发实践中,为了提高数据库连接的使用率,避免重复建立数据库连接带来的性能消耗,会经常使用数据库连接池技术来维护数据库连接。
gorm自带了数据库连接池使用非常简单只要设置下数据库连接池参数即可。

数据库连接池使用例子:
定义tools包,负责数据库初始化工作

//定义一个工具包,用来管理gorm数据库连接池的初始化工作。
package tools

//定义全局的db对象,我们执行数据库操作主要通过他实现。
var _db *gorm.DB

//包初始化函数,golang特性,每个包初始化的时候会自动执行init函数,这里用来初始化gorm。
func init() {
    ...忽略dsn配置,请参考上面例子...

    // 声明err变量,下面不能使用:=赋值运算符,否则_db变量会当成局部变量,导致外部无法访问_db变量
    var err error
    //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
    _db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }

    sqlDB, _ := db.DB()

    //设置数据库连接池参数
    sqlDB.SetMaxOpenConns(100)   //设置数据库连接池最大连接数
    sqlDB.SetMaxIdleConns(20)   //连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于20,超过的连接会被连接池关闭。
}

//获取gorm db对象,其他包需要执行数据库查询的时候,只要通过tools.getDB()获取db对象即可。
//不用担心协程并发使用同样的db对象会共用同一个连接,db对象在调用他的方法的时候会从数据库连接池中获取新的连接
func GetDB() *gorm.DB {
    return _db
}

使用例子:

package main
//导入tools包
import tools

func main() {
    //获取DB
    db := tools.GetDB()

    //执行数据库查询操作
    u := User{}
    //自动生成sql: SELECT * FROM `users`  WHERE (username = 'tizi365') LIMIT 1
    db.Where("username = ?", "tizi365").First(&u)
}

注意:使用连接池技术后,千万不要使用完db后调用db.Close关闭数据库连接,这样会导致整个数据库连接池关闭,导致连接池没有可用的连接。

3.gorm自动建表

GORM支持Migration特性,支持根据Go Struct结构自动生成对应的表结构。
注意:GORM 的AutoMigrate函数,仅支持建表,不支持修改字段和删除字段,避免意外导致丢失数据。

3.1 自动建表

通过AutoMigrate函数可以快速建表,如果表已经存在不会重复创建。

// 根据User结构体,自动创建表结构.
db.AutoMigrate(&User{})

// 一次创建User、Product、Order三个结构体对应的表结构
db.AutoMigrate(&User{}, &Product{}, &Order{})

// 可以通过Set设置附加参数,下面设置表的存储引擎为InnoDB
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

3.2 Schema方法

3.2.1 检测表是否存在

// 检测User结构体对应的表是否存在
db.Migrator().HasTable(&User{})

// 检测表名users是否存在
db.Migrator().HasTable("users")

3.2.2 建表

// 根据User结构体建表
db.Migrator().CreateTable(&User{})

3.2.3 删除表

// 删除User结构体对应的表
db.Migrator().DropTable(&User{})

// 删除表名为users的表
db.Migrator().DropTable("users")

3.2.4 删除字段

// 删除User结构体对应表中的description字段
db.Migrator().DropColumn(&User{}, "Name")

3.2.5 添加索引

type User struct {
  gorm.Model
  Name string `gorm:"size:255;index:idx_name,unique"`
}

// 为 Name 字段创建索引
db.Migrator().CreateIndex(&User{}, "Name")
db.Migrator().CreateIndex(&User{}, "idx_name")

// 为 Name 字段删除索引
db.Migrator().DropIndex(&User{}, "Name")
db.Migrator().DropIndex(&User{}, "idx_name")

// 检查索引是否存在
db.Migrator().HasIndex(&User{}, "Name")
db.Migrator().HasIndex(&User{}, "idx_name")

type User struct {
  gorm.Model
  Name  string `gorm:"size:255;index:idx_name,unique"`
  Name2 string `gorm:"size:255;index:idx_name_2,unique"`
}
// 修改索引名
db.Migrator().RenameIndex(&User{}, "Name", "Name2")
db.Migrator().RenameIndex(&User{}, "idx_name", "idx_name_2")

3.2.6 组合索引

两个字段使用同一个索引名,Migration将创建复合索引,例如:

type User struct {
    Name   string `gorm:"index:idx_member"`
    Number string `gorm:"index:idx_member"`
}

以下是Migrator接口的声明方法
通过以下方法可以实现对库-表-字段的多级的CRUD。

// Migrator migrator interface
type Migrator interface {
	// AutoMigrate
	AutoMigrate(dst ...interface{}) error

	// Database
	CurrentDatabase() string
	FullDataTypeOf(*schema.Field) clause.Expr

	// Tables
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	HasTable(dst interface{}) bool
	RenameTable(oldName, newName interface{}) error
	GetTables() (tableList []string, err error)

	// Columns
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
	AlterColumn(dst interface{}, field string) error
	MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
	HasColumn(dst interface{}, field string) bool
	RenameColumn(dst interface{}, oldName, field string) error
	ColumnTypes(dst interface{}) ([]ColumnType, error)

	// Views
	CreateView(name string, option ViewOption) error
	DropView(name string) error

	// Constraints
	CreateConstraint(dst interface{}, name string) error
	DropConstraint(dst interface{}, name string) error
	HasConstraint(dst interface{}, name string) bool

	// Indexes
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
	HasIndex(dst interface{}, name string) bool
	RenameIndex(dst interface{}, oldName, newName string) error
}

4.错误处理

4.1 错误处理

如果在执行SQL查询的时候,出现错误,GORM 会将错误信息保存到 *gorm.DB 的Error字段,我们只要检测Error字段就可以知道是否存在错误。

if err := db.Where("name = ?", "tizi365").First(&user).Error; err != nil {
  // 错误处理
}

或者

if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {
  // 错误处理
}

4.2 error

ErrRecordNotFound 为例:
当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误。如果发生了多个错误,你可以通过 errors.Is 判断错误是否为 ErrRecordNotFound,例如:

// 检查错误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

更多错误类型,参见gorm源码:

var (
	// ErrRecordNotFound record not found error
	ErrRecordNotFound = logger.ErrRecordNotFound
	// ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback`
	ErrInvalidTransaction = errors.New("invalid transaction")
	// ErrNotImplemented not implemented
	ErrNotImplemented = errors.New("not implemented")
	// ErrMissingWhereClause missing where clause
	ErrMissingWhereClause = errors.New("WHERE conditions required")
	// ErrUnsupportedRelation unsupported relations
	ErrUnsupportedRelation = errors.New("unsupported relations")
	// ErrPrimaryKeyRequired primary keys required
	ErrPrimaryKeyRequired = errors.New("primary key required")
	// ErrModelValueRequired model value required
	ErrModelValueRequired = errors.New("model value required")
	// ErrInvalidData unsupported data
	ErrInvalidData = errors.New("unsupported data")
	// ErrUnsupportedDriver unsupported driver
	ErrUnsupportedDriver = errors.New("unsupported driver")
	// ErrRegistered registered
	ErrRegistered = errors.New("registered")
	// ErrInvalidField invalid field
	ErrInvalidField = errors.New("invalid field")
	// ErrEmptySlice empty slice found
	ErrEmptySlice = errors.New("empty slice found")
	// ErrDryRunModeUnsupported dry run mode unsupported
	ErrDryRunModeUnsupported = errors.New("dry run mode unsupported")
	// ErrInvalidDB invalid db
	ErrInvalidDB = errors.New("invalid db")
	// ErrInvalidValue invalid value
	ErrInvalidValue = errors.New("invalid value, should be pointer to struct or slice")
	// ErrInvalidValueOfLength invalid values do not match length
	ErrInvalidValueOfLength = errors.New("invalid association values, length doesn't match")
	// ErrPreloadNotAllowed preload is not allowed when count is used
	ErrPreloadNotAllowed = errors.New("preload is not allowed when count is used")
)

posted on 2022-06-11 08:10  进击的davis  阅读(1943)  评论(0编辑  收藏  举报

导航