# Gorm 基础

### Gorm 快速入门

> GORM github地址: <https://github.com/go-gorm/gorm> GORM 官网地址： <https://gorm.io/zh\\_CN/docs/>

主操作： MYSQL

* 安装

```go
//安装MySQL驱动、gorm包
go get -u gorm.io/driver/mysql
go get -u gorm.io/gorm
```

增删改查

```go
package main

import (
    "errors"
    "fmt"
    "time"

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

type User struct {
    gorm.Model   // gorm 自带模型继承
    Username string `gorm:"column:username;not null"` // 列名（字段名)为username,不为空
    Password string `gorm:"column:password;not null"`
}

// 设置表名，可以通过给struct类型定义 TableName函数，返回当前struct绑定的mysql表名是什么
func (u User) TableName() string {
    return "users"
}

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

    /*
      通过前面的数据库参数，拼接 MYSQL DSN， 其实就是数据库连接串（数据源名称）
      MYSQL dsn格式： {username}:{password}@tcp({host}:{port})/{Dbname}?charset=utf8&parseTime=True&loc=Local
    */
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, Dbname)

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }

    // 自动创建表 users, 也就是上面模型 User
    db.AutoMigrate(&User{})

    /* 增 */
    u := User{
        Username:   "tizi365",
        Password:   "123456",
    }
    // 错误处理,默认gorm.DB的Error属性为 nil, 检测 Error 是否为nil
    if err := db.Create(&u).Error; err != nil {
        fmt.Println("插入失败", err)
        return
    }

    /* 查 */
    u = User{}
    result := db.Where("username = ?", "tizi365").First(&u)
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        fmt.Println("找不到记录")
        return
    }
    fmt.Println(u.Username, u.Password)

    /* 改 */
    db.Model(&User{}).Where("username = ?", "tizi365").Update("password", "654321")

    /* 删 */
    db.Where("username = ?", "tizi365").Delete(&User{})
}
```

### GORM 调试模式

> 会打印执行的 sql 语句

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

### GORM 高并发

> 提高数据库连接的使用率，避免重复建立数据库连接带来的性能消耗，会经常使用数据库连接池技术来维护数据库连接。

* 1、定义tools包，负责数据库初始化工作

```go
//定义一个工具包，用来管理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
}
```

* 使用例子

```go
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)
}
```

### GORM 插入数据

```go
    /* 增 */
    u := User{
        Username: "张三",
        Password: "00000",
    }

    // db.Debug().Create(&u)
    // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`username`,`password`,`id`) VALUES ('2022-08-23 18:32:01.618','2022-08-23 18:32:01.618',NULL,'张三','00000',1)

    if err := db.Create(&u).Error; err != nil {
        fmt.Println(err)
        return
    }

    // fmt.Println(u.ID)
    fmt.Println(u.Username)
```

### GORM 查询数据

* Take

> 查询一条数据

```go
db.Take(&User{}) 
// SELECT * FROM `users`   LIMIT 1 
```

* First

> 查询一条记录，根据主键ID排序(正序)，返回第一条记录

```go
db.First(&User{})
// SELECT * FROM `users`   ORDER BY `users`.`id` ASC LIMIT 1    
```

* Last

> 查询一条记录, 根据主键ID排序(倒序)，返回第一条记录

```go
db.Last(&User{}) 
// SELECT * FROM `users`   ORDER BY `users`.`id` DESC LIMIT 1   
```

* Find

> 查询多条记录，Find函数返回的是一个数组

```go
var us []User
db.Find(&us)

// SELECT * FROM `users`
```

* Pluck

> 查询一列值

```go
var username []string
db.Model(&User{}).Pluck("username", &username)
```

* where

> 语法：db.Where(query interface{}, args ...interface{})

```go
// 传递单值
db.Where("id = ?", 10).Take(&User{})
// SELECT * FROM `users`  WHERE (id = '10') LIMIT 1


// 传递数组
db.Where("id in (?)", []int{1,2,5,6}).Take(&User{})
// SELECT * FROM `users`  WHERE (id in ('1','2','5','6')) LIMIT 1 


// 传递两个占位符,返回数组
var us []User
db.Where("created_at >= ? and created_at <= ?", "2018-11-06 00:00:00", "2018-11-06 23:59:59").Find(&us)
// SELECT * FROM `users`  WHERE (created_at >= '2018-11-06 00:00:00' and created_at <= '2018-11-06 23:59:59')


// 通配符,返回数组
var us []User
db.Where("username ?", "%可乐%").Find(&us)
// SELECT * FROM `users`  WHERE (username like '%可乐%')
```

* select

> 设置select子句, 指定返回的字段

```go
// 返回指定字段
db.Select("username").Where("id = ?", 1).Take(&User{})
// SELECT id,title FROM `users`  WHERE `username`.`id` = '1' AND ((id = '1')) LIMIT 1  


// 返回指定多个字段
db.Select([]string{"username", "password"}).Where("id = ?", 1).Take(&User{})


// 聚合查询，统计数量
total := []int{}
db.Debug().Model(&User{}).Select("count(*) as total").Pluck("total", &total)
fmt.Println(total[0])
// SELECT count(*) as total FROM `users` WHERE `users`.`deleted_at` IS NULL
```

* order

> 设置排序语句，order by子句

```go
var us []User
db.Where("created_at >= ?", "2018-11-06 00:00:00").Order("created_at desc").Find(&us)
// SELECT * FROM `users`  WHERE (created_at >= '2018-11-06 00:00:00') ORDER BY created_at desc
```

* limit & Offset

> 设置limit和Offset子句，分页的时候常用语句。

```go
// 指定页的几条数据
var us []User
db.Order("created_at desc").Limit(10).Offset(1).Find(&us)
// SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY created_at desc LIMIT 10 OFFSET 1
```

* count

> Count函数，直接返回查询匹配的行数。

```go
//这里也需要通过model设置模型，让gorm可以提取模型对应的表名
var total int64 = 0
db.Model(User{}).Count(&total)
fmt.Println(total)   // 6
// SELECT count(*) FROM `users` 
```

* group by

```go
//例子:
//统计每个商品分类下面有多少个商品
//定一个Result结构体类型，用来保存查询结果
type Result struct {
    Type  int
    Total int
}

var results []Result
db.Model(User{}).Select("type, count(*) as  total").Group("type").Having("total > 0").Scan(&results)
// SELECT type, count(*) as  total FROM `users` GROUP BY type HAVING (total > 0)
//scan类似Find都是用于执行查询语句，然后把查询结果赋值给结构体变量，区别在于scan不会从传递进来的结构体变量提取表名.
//这里因为我们重新定义了一个结构体用于保存结果，但是这个结构体并没有绑定foods表，所以这里只能使用scan查询函数。
```

* 直接执行sql语句

> 对于复杂的查询，例如多表连接查询，我们可以直接编写sql语句，然后执行sql语句。 gorm通过db.Raw设置sql语句，通过Scan执行查询。

```go
sql := "SELECT type, count(*) as  total FROM `users` where created_at > ? GROUP BY type HAVING (total > 0)"
//因为sql语句使用了一个问号(?)作为绑定参数, 所以需要传递一个绑定参数(Raw第二个参数).
//Raw函数支持绑定多个参数
db.Raw(sql, "2018-11-06 00:00:00").Scan(&results)
fmt.Println(results)
```

### GORM 更新数据

> 通过结构体变量更新字段值, gorm库会忽略零值字段。就是字段值等于0, nil, "", false这些值会被忽略掉，不会更新。如果想更新零值，可以使用map类型替代结构体。

* Save

> 相当于根据主键id，更新所有模型字段值。

```go
u := User{}
db.Where("id = ?", 1).Take(&u)
u.Username = "三张"
db.Save(&u)

// UPDATE `users` SET `created_at`='2022-08-23 18:32:01.618',`updated_at`='2022-08-23 19:38:31.507',`deleted_at`=NULL,`username`='三张',`password`='00000' WHERE `users`.`deleted_at` IS NULL AND `id` = 1
```

* Update

> 更新单个字段值

```go
u := User{}
db.Where("id = ?", 1).Take(&u)
db.Model(&u).Update("password", "aabbcc")
// UPDATE `users` SET `password`='aabbcc',`updated_at`='2022-08-23 20:30:24.292' WHERE `users`.`deleted_at` IS NULL AND `id` = 1
```

> 自定义条件更新记录

```go
db.Model(&User{}).Where("created_at > ?", "2018-11-06 20:00:00").Update("password", "66666")

// UPDATE `users` SET `password`='66666',`updated_at`='2022-08-24 14:33:17.06' WHERE created_at > '2018-11-06 20:00:00' AND `users`.`deleted_at` IS NULL
```

> 更新某个 id 的多个记录

```go
// 根据 ID 查询更新
data := User{
    Username: "赵四",
    Password: "4444",
}

// 更新 id 为 2 的 username 和 passwor
db.Debug().Model(&User{}).Where("id = ?", 2).Take(&User[]).Updates(&data)
// UPDATE `users` SET `updated_at`='2022-08-24 14:48:23.084',`username`='赵四',`password`='4444' WHERE id = 2 AND `users`.`deleted_at` IS NULL LIMIT 1  
```

> 更新符合条件的记录

```go
data := User{
    Username: "赵四",
    Password: "4444",
}

// 更新 id 大于 3 的字段
db.Debug().Model(&User{}).Where("id > ?", 3).Updates(&data)

// UPDATE `users` SET `updated_at`='2022-08-24 14:51:36.631',`username`='赵四',`password`='4444' WHERE id > 3 AND `users`.`deleted_at` IS NULL
```

> 更新零值，使用 map 代替结构体去更新

```go
// 定义map类型，key为字符串，value为interface{}类型，方便保存任意值
data := make(map[string]interface{})
data["username"] = "王五"
data["Password"] = "5555"

db.Debug().Model(&User{}).Where("id = ?", 2).Updates(data)

// UPDATE `users` SET `password`='5555',`username`='王五',`updated_at`='2022-08-24 14:56:54.841' WHERE id = 2 AND `users`.`deleted_at` IS NULL
```

> 计算更新表达式

*UPDATE `foods` SET `stock` = `stock` + 1 WHERE id = '2'*\
这样的带计算表达式的更新语句gorm怎么写？

gorm提供了Expr函数用于设置表达式

```go
//等价于: UPDATE `foods` SET `stock` = stock + 1  WHERE `foods`.`id` = '2'
db.Model(&food).Update("stock", gorm.Expr("stock + 1"))
```

### GORM 删除数据

> 删除单条数据

```go
u := User{}
db.Where("id = ?", 8).Take(&u).Delete(&u)
// UPDATE `users` SET `deleted_at`='2022-08-24 15:04:31.893' WHERE id = 8 AND `users`.`deleted_at` IS NULL AND `users`.`id` = 8 LIMIT 1
```

> 删除符合条件数据

```go
db.Where("id = ?", 8).Delete(&u)
```

### GORM事务处理

* 自动事务

> 通过db.Transaction函数实现事务，如果闭包函数返回错误，则回滚事务。

```go
db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作（从这里开始，您应该使用 'tx' 而不是 'db'）
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
    return err
  }

  // 返回 nil 提交事务
  return nil
})
```

* 手动事务

> 在开发中经常需要数据库事务来保证多个数据库写操作的原子性。例如电商系统中的扣减库存和保存订单。

```go
/ 开启事务
tx := db.Begin()

//在事务中执行数据库操作，使用的是tx变量，不是db。

//库存减一
//等价于: UPDATE `foods` SET `stock` = stock - 1  WHERE `foods`.`id` = '2' and stock > 0
//RowsAffected用于返回sql执行后影响的行数
rowsAffected := tx.Model(&food).Where("stock > 0").Update("stock", gorm.Expr("stock - 1")).RowsAffected
if rowsAffected == 0 {
    //如果更新库存操作，返回影响行数为0，说明没有库存了，结束下单流程
    //这里回滚作用不大，因为前面没成功执行什么数据库更新操作，也没什么数据需要回滚。
    //这里就是举个例子，事务中可以执行多个sql语句，错误了可以回滚事务
    tx.Rollback()
    return
}
err := tx.Create(保存订单).Error

//保存订单失败，则回滚事务
if err != nil {
    tx.Rollback()
} else {
    tx.Commit()
}
```

### 共用: 多表关联测试

```go
package main

import (
    "encoding/json"
    "fmt"

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

// 封装一个将 map 转换为字符串的方法
func MapToJson(data interface{}) string {
    byteStr, _ := json.Marshal(data)
    return string(byteStr)
}



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

    /*
      通过前面的数据库参数，拼接 MYSQL DSN， 其实就是数据库连接串（数据源名称）
      MYSQL dsn格式： {username}:{password}@tcp({host}:{port})/{Dbname}?charset=utf8&parseTime=True&loc=Local
    */
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, Dbname)

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }

    // 一对多
    ...
    // 一多一
    ...
    // 多对多
    ...
}
```

### 一对多

> 继承共用代码

#### 一对多测试模型

```go
// 用户
type User struct {
    Id          int          `gorm:"type:int(11); autoIncrement;primaryKey;column:id;" json:"id"`                   // 主键id
    Name        string       `gorm:"type:varchar(30);not null;" json:"name" `                                       // 姓名
    CreditCards []CreditCard `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"  json:"card"` // UserID 外键名称， 一对多的情况下，外键写在[多]的表结构下; 级联更新，删除时置空
}


// 用户的多张信用卡
type CreditCard struct {
    Id     int    `gorm:"type:int(11); autoIncrement;primaryKey;column:id;" json:"id"` // 主键id
    Name   string `gorm:"type:varchar(30);not null;" json:"name" `                     // 信用卡银行
    UserID uint   `json:"user_id"`
    User   User   `json:"user"` // 反查使用，如通过信用卡查询到归属人
}

// 设置表名，可以通过给struct类型定义 TableName函数，返回当前struct绑定的mysql表名是什么
func (u User) TableName() string {
    return "users"
}

func (u CreditCard) TableName() string {
    return "creditcard"
}

db.AutoMigrate(&User{}, &CreditCard{})
```

#### 添加测试数据

```go
// 先添加一些测试数据
db.Create(&User{Name: "张三"})
db.Create(&User{Name: "赵四"})
db.Create(&User{Name: "王五"})
db.Create(&User{Name: "老六"})

var u1 User
var u2 User
db.First(&u1, "id = ?", 1)
db.Create(&CreditCard{Name: "建设信用卡银行", UserID: uint(u1.Id)})
db.Create(&CreditCard{Name: "中信信用卡银行", UserID: uint(u1.Id)})
db.Create(&CreditCard{Name: "招商信用卡银行", UserID: uint(u1.Id)})

db.First(&u2, "id = ?", 2)
db.Create(&CreditCard{Name: "农业信用卡银行", UserID: uint(u2.Id)})
db.Create(&CreditCard{Name: "中国信用卡银行", UserID: uint(u2.Id)})
db.Create(&CreditCard{Name: "招商信用卡银行", UserID: uint(u2.Id)})
```

#### 查询某用户拥有几张信用卡

> 查询某个用户名下有几张信用卡

```go
var user []User

query := db.Where("name = ?", "张三").Preload("CreditCards").Find(&user)
if query.Error != nil {
    panic(query.Error)
}
fmt.Println(MapToJson(user))
```

```go
[{"id":2,"name":"赵四","card":[{"id":4,"name":"农业信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}},{"id":5,"name":"中国信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}},{"id":6,"name":"招商信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}}]}]
```

#### 查询某ID的信用卡和归属用户

```go
var c CreditCard

db.Where("id = ?", 2).First(&c)
db.Joins("User").First(&c)
fmt.Println(MapToJson(c))
```

```go
{"id":2,"name":"中信信用卡银行","user_id":1,"user":{"id":1,"name":"张三","card":null}}
```

#### 查询所有用户和所属信用卡

```go
var u []User

db.Preload("CreditCards").Find(&u)
fmt.Println(MapToJson(u))
```

```go
[{"id":1,"name":"张三","card":[{"id":1,"name":"建设信用卡银行","user_id":1,"user":{"id":0,"name":"","card":null}},{"id":2,"name":"中信信用卡银行","user_id":1,"user":{"id":0,"name":"","card":null}},{"id":3,"name":"招商信用卡银行","user_id":1,"user":{"id":0,"name":"","card":null}}]},{"id":2,"name":"赵四","card":[{"id":4,"name":"农业信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}},{"id":5,"name":"中国信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}},{"id":6,"name":"招商信用卡银行","user_id":2,"user":{"id":0,"name":"","card":null}}]},{"id":3,"name":"王五","card":[]},{"id":4,"name":"老六","card":[]}]
```

#### 查询所有信用卡和归属用户

```go
var c []CreditCard

db.Joins("User").Find(&c)
fmt.Println(MapToJson(c))
for key, value := range c {
    fmt.Println(key, value)
}
```

```go
[{"id":1,"name":"建设信用卡银行","user_id":1,"user":{"id":1,"name":"张三","card":null}},{"id":2,"name":"中信信用卡银行","user_id":1,"user":{"id":1,"name":"张三","card":null}},{"id":3,"name":"招商信用卡银行","user_id":1,"user":{"id":1,"name":"张三","card":null}},{"id":4,"name":"农业信用卡银行","user_id":2,"user":{"id":2,"name":"赵四","card":null}},{"id":5,"name":"中国信用卡银行","user_id":2,"user":{"id":2,"name":"赵四","card":null}},{"id":6,"name":"招商信用卡银行","user_id":2,"user":{"id":2,"name":"赵四","card":null}}]
0 {1 建设信用卡银行 1 {1 张三 []}}
1 {2 中信信用卡银行 1 {1 张三 []}}
2 {3 招商信用卡银行 1 {1 张三 []}}
3 {4 农业信用卡银行 2 {2 赵四 []}}
4 {5 中国信用卡银行 2 {2 赵四 []}}
5 {6 招商信用卡银行 2 {2 赵四 []}}
```

#### 一对多，级联删除

> 删除用户，并会把此用户名下的信用卡所属 user\_id 设置为空

```go
var u User
db.First(&u, "id = ?", 1)
db.Delete(&u) 
```

### 一对一

#### 一对一模型

```go
// 用户
type User struct {
    Id   int    `gorm:"type:int(11); autoIncrement;primaryKey;column:id;" json:"id"` // 主键id
    Name string `gorm:"type:varchar(30);not null;" json:"name" `                     // 姓名
}

// 身份证号码
type IdCard struct {
    Id     int    `gorm:"type:int(11); autoIncrement;primaryKey;column:id;" json:"id"` // 主键id
    Number string `gorm:"type:varchar(30);not null;" json:"number" `                   // 身份证号码
    UserID int    `gorm:"unique;" json:"user_id"`                                      // 设置为唯一,关联外键
    User   User   `json:"user"`                                                        // 反查使用
}

// 设置表名，可以通过给struct类型定义 TableName函数，返回当前struct绑定的mysql表名是什么
func (u User) TableName() string {
    return "users"
}

func (u IdCard) TableName() string {
    return "idcard"
}


db.AutoMigrate(&User{}, &IdCard{})
```

#### 用户查询自身份证

> Joins 连表方式

```go
var us []map[string]interface{}

// 连表查询 table1.id table1.name table2.number
db.Model(&User{}).Select("users.id", "users.name", "idcard.number").Joins("left join idcard on users.id = idcard.user_id").Scan(&us)
fmt.Println(MapToJson(us))

// [{"id":1,"name":"张三","number":"33333333333"},{"id":2,"name":"赵四","number":"44444444444"},{"id":3,"name":"王五","number":"55555555555"}]
```

#### 身份证反向查询用户

```go
var c []IdCard
db.Preload(User").Find(&c)
fmt.Println(MapToJson(c))

[
	{"id":1,"number":"33333333333","user_id":1,"user":{"id":1,"name":"张三"}},
	{"id":2,"number":"44444444444","user_id":2,"user":{"id":2,"name":"赵四"}},
	{"id":3,"number":"55555555555","user_id":3,"user":{"id":3,"name":"王五"}}
]
```

### 多对多

> 举例账号和角色的关联关系； 一个账号可以有多个角色，一个角色也可以给多个账号的关联 如果查看数据库的话，会发现有三张表，多出的一张表是存储 角色和账户的关系： account\_role

```go
// 账户表
type AccountEntity struct {
    Id    int           `json:"id"`
    Name  string        `json:"name"`
    Roles []*RoleEntity `json:"roles" gorm:"many2many:account_role;foreignKey:Id;joinForeignKey:accountId;joinReferences:roleId;"`
}

// 角色
type RoleEntity struct {
    Id       int64            `json:"id"`
    Title    string           `json:"title"`
    Accounts []*AccountEntity `json:"accounts" gorm:"many2many:account_role;foreignKey:Id;joinForeignKey:RoleId;joinReferences:accountId"`
}

// 设置表名，可以通过给struct类型定义 TableName函数，返回当前struct绑定的mysql表名是什么
func (u AccountEntity) TableName() string {
    return "account"
}

func (u RoleEntity) TableName() string {
    return "role"
}
```

* many2many:account\_role 定义中间表名为:account\_role
* foreignKey:Id 使用当前表的id作为外键
* joinForeignKey:accountId 当前数据模型外键关联到中间件表的字段名叫accountId
* joinReferences:roleId 反向引用字段，如果是账号表就要写中间表的roleId

#### 添加测试数据

```go
// 添加测试数据
a1 := AccountEntity{Name: "账户一"}
a2 := AccountEntity{Name: "账户二"}
a3 := AccountEntity{Name: "账户三"}
db.Create(&a1)
db.Create(&a2)
db.Create(&a3)

r1 := RoleEntity{Title: "角色一"}
r2 := RoleEntity{Title: "角色二"}
r3 := RoleEntity{Title: "角色三"}
db.Create(&r1)
db.Create(&r2)
db.Create(&r3)

// 生成用户和角色关联语句
db.Debug().Model(&a1).Association("Roles").Append([]RoleEntity{r1, r2, r3})
db.Debug().Model(&a2).Association("Roles").Append([]RoleEntity{r1, r2})
db.Debug().Model(&a3).Association("Roles").Append([]RoleEntity{r2, r3})
// INSERT INTO `account_role` (`account_id`,`role_id`) VALUES (1,1),(1,2),(1,3) ON DUPLICATE KEY UPDATE `account_id`=`account_id`
```

#### 通过用户查询角色

```go
var accountList []AccountEntity
db.Preload("Roles").Find(&accountList)

fmt.Println(MapToJson(accountList))
```

```go
[
    {
        "id": 1,
        "name": "账户一",
        "roles": [
            {"id": 1,"title": "角色一","accounts": null}, 
            {"id": 2,"title": "角色二","accounts": null}, 
            {"id": 3,"title": "角色三","accounts": null}
        ]
    }, 
    {
        "id": 2,
        "name": "账户二",
        "roles": [
            {"id": 1,"title": "角色一","accounts": null}, 
            {"id": 2,"title": "角色二","accounts": null}
        ]
    }, 
    {
        "id": 3,
        "name": "账户三",
        "roles": [
            {"id": 2,"title": "角色二","accounts": null}, 
            {"id": 3,"title": "角色三","accounts": null}]
    }
]
```

#### 通过角色反查用户

```go
var roleList []RoleEntity
db.Preload("Accounts").Find(&roleList)

fmt.Println(MapToJson(roleList))
```

```go
[
    {
        "id": 1,
        "title": "角色一",
        "accounts": [
            {"id": 1,"name": "账户一","roles": null}, 
            {"id": 2,"name": "账户二","roles": null}
        ]
    }, 
    {
        "id": 2,
        "title": "角色二",
        "accounts": [
            {"id": 1,"name": "账户一","roles": null}, 
            {"id": 2,"name": "账户二","roles": null}, 
            {"id": 3,"name": "账户三","roles": null}
        ]
    }, {
        "id": 3,
        "title": "角色三",
        "accounts": [
            {"id": 1,"name": "账户一","roles": null}, 
            {"id": 3,"name": "账户三","roles": null}
        ]
    }
]
```

### 参考地址

* [关于gorm多表关联关系查询使用案例 - 掘金](https://juejin.cn/post/7067138794788487181#heading-0)
* [gorm中的基本查询](https://www.shuzhiduo.com/A/obzbaxvMdE/)

## Beforce

### GORM 模型定义

#### 内置模型约定

* 默认情况下： GORM 倾向于约定，而不是配置。默认情况下，GORM 使用 ID 作为主键，使用结构体名的 蛇形复数 作为表名，字段名的 蛇形 作为列名，并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间

例如:

```go
type User struct {           // 未自定义，默认为 user 表 
  // ID           uint       // 未自定义，创建表，默认会添加ID字段，并设置为主键
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  // CreatedAt    time.Time  // 未自定义，默认添加; 添加数据，默认添加创建时间
  // UpdatedAt    time.Time  // 未自定义，默认添加; 更新数据，默认更新为当前时间
}
```

#### 自定义匿名字段模型约定

> 默认是有一个 gorm.Model, 但是我不想用，我想自定义，让所有表中都携带我自定义的字段

```go
// 建立 gorm.Model 全局匿名字段模型(可复用到任何模型当中)
type Model struct {
    ID          uint            `gorm:"primaryKey"` // 设置主键
    CreatedAt   time.Time                           // 创建时间
    UpdatedAt   time.Time                           // 更新时间
    DeletedAt   *time.Time                          // 删除时间
}


// 嵌入到你自己创建的模型中， 以 用户表 为例
// 这样就可以将字段 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt` 注入到 `User` 模型中
type User struct {
  gorm.Model
  Name string
}




// 上面 User 表等效于
type User struct {
  ID        uint           `gorm:"primaryKey"`
  Name      string
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt *time.Time
}
```

#### 自定义正常字段模型约定

* 比如某些公共的字段，可以被其他结构体调用

```go
type Author struct {
    Name string
    Email string
}


type Blog struct {
    ID int
    Author Author `gorm:"embedded"`
    Upvotes int32
}


// 等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}
```

| 标签名                    | 说明                                                                                    | 示例                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| column                 | 指定 db 列名                                                                              | `gorm:"column:username"`                                                                          |
| type                   | <p>数据类型<br>bool、int、uint、float、string、time、bytes</p>                                  | <p><code>gorm:"type:int"</code> // 设置类型为int<br><code>gorm:"type:varchar(20);not null "</code></p> |
| size                   | 指定列大小                                                                                 | `gorm:"size:255"` // 设置字段大小为255                                                                   |
| primaryKey             | 指定列为主键                                                                                | `gorm:"primaryKey"`                                                                               |
| unique                 | 指定列为唯一                                                                                | `gorm:"unique"` // 唯一                                                                             |
| default                | 指定列的默认值                                                                               | `gorm:"default:0"` // 默认值为0                                                                       |
| precision              | 指定列的精度                                                                                |                                                                                                   |
| scale                  | 指定列大小                                                                                 |                                                                                                   |
| not null               | 指定列为 NOT NULL                                                                         | `gorm:"not null"` // 不为空                                                                          |
| autoIncrement          | 指定列为自动增长                                                                              | `gorm:"AUTO_INCREMENT"`                                                                           |
| autoIncrementIncrement | 自动步长，控制连续记录之间的间隔                                                                      |                                                                                                   |
| embedded               | 嵌套字段                                                                                  | `gorm:"embedded"`                                                                                 |
| embeddedPrefix         | 嵌入字段的列名前缀                                                                             |                                                                                                   |
| autoCreateTime         | 创建时追踪当前时间,对于 int 字段，它会追踪秒级时间戳,使用 nano/milli 来追踪纳秒、毫秒时间戳                               | `gorm:"embedded;embeddedPrefix:xxxx_"`                                                            |
| autoUpdateTime         | 创建/更新时追踪当前时间,对于 int 字段，它会追踪秒级时间戳，您可以使用 nano/milli 来追踪纳秒、毫秒时间戳，例如：autoUpdateTime:milli |                                                                                                   |
| index                  | 根据参数创建索引，多个字段使用相同的名称则创建复合索引，查看 索引 获取详情                                                | <p><code>gorm:"index"</code> <br><code>gorm:"index:addr"</code>// 给address字段创建名为addr的索引</p>       |
| uniqueIndex            | 与 index 相同，但创建的是唯一索引                                                                  |                                                                                                   |
| check                  | 创建检查约束                                                                                | check:age > 13                                                                                    |
| <-                     | 设置字段写入的权限， <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限                      | \`gorm:"<-:create"                                                                                |
| ->                     | 设置字段读的权限，->:false 无读权限                                                                |                                                                                                   |
| -                      | 忽略该字段，- 无读写权限                                                                         | `gorm:"-"` // 忽略本字段                                                                               |
| comment                | 迁移时为字段添加注释                                                                            | `gorm:"column:beast_id"` // 设置列名为 `beast_id`                                                      |

#### 表字段标签

```go
type User struct {
  Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写（创建和更新）
  Name string `gorm:"<-:false"`  // 允许读，禁止写
  Name string `gorm:"->"`        // 只读（除非有自定义配置，否则禁止写）
  Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建（禁止从 db 读）
  Name string `gorm:"-"`  // 通过 struct 读写会忽略该字段
}
```

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

#### 表关联标签

| 标签               | 描述                            |
| ---------------- | ----------------------------- |
| foreignKey       | 指定当前模型的列作为连接表的外键              |
| references       | 指定引用表的列名，其将被映射为连接表外键          |
| polymorphic      | 指定多态类型，比如模型名                  |
| polymorphicValue | 指定多态值、默认表名                    |
| many2many        | 指定连接表表名                       |
| joinForeignKey   | 指定连接表的外键列名，其将被映射到当前表          |
| joinReferences   | 指定连接表的外键列名，其将被映射到引用表          |
| constraint       | 关系约束，例如：`OnUpdate`、`OnDelete` |

#### 复数表名(表名)

> 表名是结构体名称的复数形式

```go
type User struct {} // 默认的表名是 `users`

// 设置 `User` 的表名为 `profiles`
func (User) TableName() string {
  return "profiles"
}

func (u User) TableName() string {
    if u.Role == "admin" {
        return "admin_users"
    } else {
        return "users"
    }
}

// 如果设置禁用表名复数形式属性为 true，`User` 的表名将是 `user`
db.SingularTable(true)
```

### GORM 数据库连接

> GORM 官方支持的数据库类型有： MySQL, PostgreSQL, SQlite, SQL Server

> 官方通往: <https://gorm.io/zh\\_CN/docs/connecting\\_to\\_the\\_database.html>
>
> 或者参考: [惯例 - 人人学go](http://www.golang.ren/article/5446)[惯例 - 人人学go](http://www.golang.ren/article/5446)

#### MYSQL 官方指南配置

```go
// 简单配置
import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn))
}



// 使用 mysql.New 函数, 传递一个mysql.Config进行详细配置

dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=UTC"

db, err := gorm.Open(mysql.New(mysql.Config{
    DSN:                       dsn,   // DSN data source name
    DefaultStringSize:         256,   // string 类型字段的默认长度
    DisableDatetimePrecision:  true,  // 禁用 datetime 精度，MySQL 5.6 之前的数据库不支持
    DontSupportRenameIndex:    true,  // 重命名索引时采用删除并新建的方式，MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
    DontSupportRenameColumn:   true,  // 用 `change` 重命名列，MySQL 8 之前的数据库和 MariaDB 不支持重命名列
    SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
})) 
```

**mysql.Config的源码**

```go
type Config struct {
    DriverName                string
    DSN                       string
    Conn                      gorm.ConnPool
    SkipInitializeWithVersion bool
    DefaultStringSize         uint
    DefaultDatetimePrecision  *int
    DisableDatetimePrecision  bool
    DontSupportRenameIndex    bool
    DontSupportRenameColumn   bool
    DontSupportForShareClause bool
}
```

#### gorm调试模式

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

#### MYSQL 单机测试使用

* 示例一

```go
package main

import (

    "fmt"

    //_ "github.com/go-sql-driver/mysql"
    //"github.com/jinzhu/gorm"

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

type Students struct {
    gorm.Model
    Username string `gorm:"type:varchar(20);not null " `
    Password string `gorm:"type:varchar(500);not null" `
    Age      int    `gorm:"type:int;DEFAULT:18" `
}

func main() {
    // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
    dsn := "luffyapi:Luffy123?@tcp(127.0.0.1:3306)/luffyapi?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        fmt.Println("连接数据库出错：", err)
    }


    db.AutoMigrate(&Students{})
    user := Students{Username: "Jinzhu", Age: 18, Password: "123456"}

    result := db.First(&user)
    fmt.Println(result)
    db.Take(&user)


    //result := db.Create(&user) // 通过数据的指针来创建
    //fmt.Println(result)


}
```

#### MYSQL 封装测试调用

* main

```go
package main

import (
    "day01/tools" //day01,是初始化项目名，导入tools包
    "fmt"

    "gorm.io/gorm"
)

type Students struct {
    gorm.Model
    Username string `gorm:"type:varchar(20);not null " `
    Password string `gorm:"type:varchar(500);not null" `
    Age      int    `gorm:"type:int;DEFAULT:18" `
}

func main() {
    db := tools.GetDB()

    // 自动创建表
    db.AutoMigrate(&Students{})

    // 创建一条数据
    user := Students{Username: "Jinzhu", Age: 18, Password: "123456"}

    result := db.Create(&user) // 通过数据的指针来创建
    fmt.Println(result)
}
```

* tools/tools.go 封装 mysql （测试使用）

```go
// 1、package tools 定义一个工具包，用来管理 gorm 数据连接池的初始化工作
// 2、var _db *gorm.DB 定义全局的 db 对象, 执行数据库操作通过它实现
// 3、初始化 gorm； golang特性，每个包初始化的时候会自动执行 init 函数
// 4、启用数据库连接池，千万不要使用完db后调用db.Close关闭数据库连接，这样会导致整个数据库连接池关闭，导致连接池没有可用的连接。
package tools

import (
    "fmt"
    "time"

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

var _db *gorm.DB

func init() {
    username := "go_test" // 账号
    password := "go_test" // 密码
    host := "127.0.0.1"   // 数据库地址
    port := 3306          // 数据库端口
    Dbname := "go_test"   // 数据库名
    charset := "utf8"     // 字符集
    parseTime := true     // 是否支持时间类型转换
    loc := "Local"        // 使用系统本地时区
    timeout := "10s"      // 连接超时，10秒
    readTimeout := "30s"  // 读超时时间
    writeTimeout := "60s" // 写超时时间

    // 拼接数据库
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%v&loc=%s&timeout=%s&readTimeout=%s&writeTimeout=%s", username, password, host, port, Dbname, charset, parseTime, loc, timeout, readTimeout, writeTimeout)

    // 声明err变量，下面不能使用:=赋值运算符，否则_db变量会当成局部变量，导致外部无法访问_db变量
    var err error
    _db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }

    sqlDB, _ := _db.DB()

    sqlDB.SetConnMaxLifetime(time.Second * 10) // 5秒内连接没有活跃的话则自动关闭连接
    sqlDB.SetMaxOpenConns(100)                 // 设置数据库连接池最大连接数
    sqlDB.SetMaxIdleConns(20)                  // 连接池最大允许的空闲连接数，如果没有sql任务需要执行的连接数大于20，超过的连接会被连接池关闭。

}

// 获取gorm db对象，其他包需要执行数据库查询的时候，只要通过tools.getDB()获取db对象即可。
func GetDB() *gorm.DB {
    return _db
}
```

### viper

#### 安装 Viper

```go
go get github.com/spf13/viper
```

#### Viper 作用

* 设置默认值
* 读取 JSON、TOML、YAML（YML）、HCL、envfile 和 Java properties 属性配置文件
* 实时查看和重读配置文件（可选）
* 从环境变量中读取
* 从远程配置系统（etcd 或 Consor）读取数据，并观察变化
* 从命令行标志读取
* 从缓冲区读取
* 设置显式值

#### Viper获取值

> 每一个Get方法在找不到值的时候都会返回零值。为了检查给定的键是否存在，提供了IsSet()方法。

```go
Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetIntSlice(key string) : []int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
AllSettings() : map[string]interface{}
```

```go
// 例如
viper.GetString("logfile") // 不区分大小写的设置和获取
if viper.GetBool("verbose") {
    fmt.Println("verbose enabled")
}
```

#### 读取配置文件

```
viper.SetDefault("ContentDir", "content")     // 设置默认值
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
viper.SetConfigFile("./config.yaml")   // 指定配置文件路径
viper.SetConfigName("config")          // 配置文件名称(无扩展名)
viper.SetConfigType("yaml")            // 如果配置文件的名称中没有扩展名，则需要配置此项
viper.AddConfigPath("/etc/appname/")   // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname")  // 多次调用以添加多个搜索路径
viper.AddConfigPath(".")               // 还可以在工作目录中查找配置
err := viper.ReadInConfig()            // 查找并读取配置文件
if err != nil {                        // 处理读取配置文件的错误
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

// 或者处理加载配置文件出错时

if err := viper.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // 配置文件未找到错误；如果需要可以忽略
    } else {
        // 配置文件被找到，但产生了另外的错误
    }
}

// 配置文件找到并成功解析
```

#### 写入配置文件

* WriteConfig - 将当前的viper配置写入预定义的路径并覆盖（如果存在的话）。如果没有预定义的路径，则报错。
* SafeWriteConfig - 将当前的viper配置写入预定义的路径。如果没有预定义的路径，则报错。如果存在，将不会覆盖当前的配置文件。
* WriteConfigAs - 将当前的viper配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
* SafeWriteConfigAs - 将当前的viper配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。

```go
viper.WriteConfig()        // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()    // 不会覆盖任何文件，而是直接创建（如果不存在），而默认行为是创建或截断
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过，所以会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")
```

#### 监控配置文件热加载

> 只需告诉viper实例watchConfig。可选地，你可以为Viper提供一个回调函数，以便在每次发生更改时运行。

```go
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
  // 配置文件发生变更之后会调用的回调函数
    fmt.Println("Config file changed:", e.Name)
})
```

#### 从io.Reader读取配置

> Viper预先定义了许多配置源，如文件、环境变量、标志和远程K/V存储，但你不受其约束。你还可以实现自己所需的配置源并将其提供给viper。

```go
viper.SetConfigType("yaml") // 或者 viper.SetConfigType("YAML")

// 任何需要将此配置添加到程序中的方法。
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // 这里会得到 "steve"
```

#### 远程Key/Value存储示例

```go
// etcd 存储
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显示设置成json
err := viper.ReadRemoteConfig()

fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com

// consul 存储
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显示设置成json
err := viper.ReadRemoteConfig()

fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com

// firestore
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置的格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()
```

#### 更多其他方法参考

> <https://www.liwenzhou.com/posts/Go/viper\\_tutorial/#autoid-1-4-0>

#### 示例读取(ini,yaml)

当前路径下有两个文件

```go
// configA.ini
;MYSQL数据库配置
[db]
username=admin
password=123
host=127.0.0.1
port=3306
dbname=dbname


;网络配置
[web]
port=8080




// configB.yaml
# MYSQL数据库配置
db: 
    username: admin
    password: 123
    host: 127.0.0.1
    port: 3306
    dbname: dbname


# 网络配置
web:
    port: 8088
# APP配置
app: {id: 10,time: 05/30}
```

```go
// main.go

package main

import (
    "fmt"
    "os"

    "github.com/spf13/viper"
)

// 当前文件路径
func getPath() string {
    path, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    return path
}

// 读取文件
func ReadConfig(_path, _name, _type string) {
    v := viper.New()
    v.AddConfigPath(_path + "/")
    v.SetConfigName(_name)
    v.SetConfigType(_type)
    err := v.ReadInConfig()

    if err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); ok {
            fmt.Println("找不到配置文件..")
        } else {
            fmt.Println("配置文件出错..")
        }
    }
    username := v.GetString("db.username")
    password := v.GetString("db.password")
    host := v.GetString("db.host")
    port := v.GetInt("db.port")
    dbname := v.GetString("db.dbname")

    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)

    fmt.Printf("dsn: %v\n", dsn)

}

// 每个 go 文件内都有 init 初始化函数，默认先执行
func init() {
    path := getPath()
    ReadConfig(path, "configA", "ini")  // 读取 configA.ini
    ReadConfig(path, "configB", "yaml") // 读取 configB.yaml
}

func main() {

}
```

```go
$ go run main.go
dsn: admin1:123@tcp(127.0.0.1:3306)/dbname?charset=utf8&parseTime=True&loc=Local
dsn: admin2:123@tcp(127.0.0.1:3306)/dbname?charset=utf8&parseTime=True&loc=Local
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://close.gitbook.io/yun-wei-bi-ji/go/xue-xi-bi-ji/gorm-ji-chu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
