GORM学习和实战
官方文档如下
GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
1.特性
- 全功能 ORM
- 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
- Create,Save,Update,Delete,Find 中钩子方法
- 支持
Preload、Joins的预加载 - 事务,嵌套事务,Save Point,Rollback To Saved Point
- Context、预编译模式、DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- Auto Migration 迁移
- 自定义 Logger
- 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
- 每个特性都经过了测试的重重考验
- 开发者友好
快速入门1 (但是sqlite版本,这是一个嵌入式的)
package main
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 迁移 schema
db.AutoMigrate(&Product{})
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // 根据整型主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
// Update - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)
// Update - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - 删除 product
db.Delete(&product, 1)
}
-
SQLite:是一种嵌入式的数据库引擎,它的数据库本质上是一个文件。这意味着整个数据库存储在一个本地文件中,应用程序可以通过库文件直接访问这个数据库文件,不需要单独的数据库服务器进程。例如,在一个小型的移动应用或者本地的命令行工具中,SQLite 可以很方便地存储和管理数据。
-
SQLite:
- 适合用于移动应用开发,作为本地数据存储,如安卓和 iOS 应用中的离线数据存储。
- 小型桌面应用,用于存储用户配置、本地缓存数据等。
- 简单的命令行工具或者脚本语言中的数据管理,用于临时存储和处理数据。
快速入门2:mysql版本
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
// UserInfo 用户信息
type UserInfo struct {
ID uint
Name string
Gender string
Hobby string
}
func main() {
db, err := gorm.Open("mysql", "root:wxe13867187633@(127.0.0.1:3306)/gromtest?charset=utf8mb4&parseTime=True&loc=Local")
if err!= nil{
panic(err)
}
defer db.Close()
fmt.Println("mysql已链接")
// 自动迁移
db.AutoMigrate(&UserInfo{})
u1 := UserInfo{1, "七米", "男", "篮球"}
u2 := UserInfo{2, "沙河娜扎", "女", "足球"}
// 创建记录
db.Create(&u1)
db.Create(&u2)
// 查询
var u = new(UserInfo)
db.First(u)
fmt.Printf("%#v\n", u)
var uu UserInfo
db.Find(&uu, "hobby=?", "足球")
fmt.Printf("%#v\n", uu)
// 更新
db.Model(&u).Update("hobby", "双色球")
// 删除
db.Delete(&u)
}
-
root:xxxxx:这是数据库的用户名和密码,格式为用户名:密码。 -
(127.0.0.1:3306):这是数据库服务器的地址和端口。127.0.0.1是本地主机地址(也就是本机),3306是 MySQL 数据库默认的服务端口。 -
/gromtest:指定了要连接的数据库名称是gromtest。 -
?charset = utf8mb4&parseTime=True&loc = Local:这是连接数据库的一些额外参数。charset = utf8mb4:表示使用utf8mb4字符集,这种字符集可以更好地支持各种语言的字符,包括 emoji 等特殊字符。parseTime=True:用于正确解析数据库中的日期和时间类型。loc = Local:用于设置时区相关的参数,确保时间处理的准确性。
gorm库通过后续的操作(如AutoMigrate、Create等)来指定和操作表。例如,如果你有一个User结构体,通过db.AutoMigrate(&User{})会在gromtest数据库中创建与User结构体对应的表。
运行结果:
PS D:\GOfiles\Gormlearn> go run main.go
mysql已链接
&main.UserInfo{ID:0x1, Name:"七米", Gender:"男", Hobby:"篮球"}
main.UserInfo{ID:0x2, Name:"沙河娜扎", Gender:"女", Hobby:"足球"}
2.GORM的model模型
约定
主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。
type User struct {
ID string // 名为`ID`的字段会默认作为表的主键
Name string
}
// 使用`AnimalID`作为主键
type Animal struct {
AnimalID int64 `gorm:"primary_key"`
Name string
Age int64
}
表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。 例如,一个 User 结构体在数据库中的表名变为 users 。
type User struct {}
// 将 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)
// 也可以通过Table()指定表名:
// 使用User结构体创建名为`deleted_users`的表
db.Table("deleted_users").CreateTable(&User{})
var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;
db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
//// DELETE FROM deleted_users WHERE name = 'jinzhu';
列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
列名由字段名称进行下划线分割来生成
type User struct {
ID uint // column name is `id`
Name string // column name is `name`
Birthday time.Time // column name is `birthday`
CreatedAt time.Time // column name is `created_at`
}
可以使用结构体tag指定列名:
type Animal struct {
AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id`
Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}
时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。
模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的Scanner和Valuer接口)。
考虑以下 user 模型的示例:
type User struct {
ID uint // Standard field for the primary key
Name string // A regular string field
Email *string // A pointer to a string, allowing for null values
Age uint8 // An unsigned 8-bit integer
Birthday *time.Time // A pointer to time.Time, can be null
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields
CreatedAt time.Time // Automatically managed by GORM for creation time
UpdatedAt time.Time // Automatically managed by GORM for update time
ignored string // fields that aren't exported are ignored
}
gorm.Model是grom提供的模型
GORM提供了一个预定义的结构体,名为gorm.Model,其中包含常用字段:
gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
其嵌入在您的结构体中: 您可以直接在您的结构体中嵌入 gorm.Model ,以便自动包含这些字段。
示范如下:
// 将 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`字段注入到`User`模型中
type User struct {
gorm.Model
Name string
}
3.高级选项
备注方式达到效果
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 读写会忽略该字段 !注意: 使用 GORM Migrator 创建表时,不会创建被忽略的字段
Name string `gorm:"-:all"` // 通过 struct 读写、迁移会忽略该字段
Name string `gorm:"-:migration"` // 通过 struct 迁移会忽略该字段
}
创建/更新时间追踪(纳秒、毫秒、秒、Time)
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"` // 使用时间戳秒数填充创建时间
}
| 标签名 | 说明 |
|---|---|
| column | 指定 db 列名 |
| type | 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null、size, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
| serializer | 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime |
| size | 定义列数据类型的大小或长度,例如 size: 256 |
| primaryKey | 将列定义为主键 |
| unique | 将列定义为唯一键 |
| default | 定义列的默认值 |
| precision | 指定列的精度 |
| scale | 指定列大小 |
| not null | 指定列为 NOT NULL |
| autoIncrement | 指定列为自动增长 |
| autoIncrementIncrement | 自动步长,控制连续记录之间的间隔 |
| embedded | 嵌套字段 |
| embeddedPrefix | 嵌入字段的列名前缀 |
| autoCreateTime | 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
| autoUpdateTime | 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
| index | 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 |
| uniqueIndex | 与 index 相同,但创建的是唯一索引 |
| check | 创建检查约束,例如 check:age > 13,查看 约束 获取详情 |
| <- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
| -> | 设置字段读的权限,->:false 无读权限 |
| - | 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 |
| comment | 迁移时为字段添加注释 |
4.连接数据库
官网文档有多种 这里只是mysql
自定义驱动
GORM 允许通过 DriverName 选项自定义 MySQL 驱动,例如config配置:
import (
_ "example.com/my_mysql_driver"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
db, err := gorm.Open(mysql.New(mysql.Config{
DriverName: "my_mysql_driver",
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name, 详情参考:https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})
现有的数据库连接
GORM 允许通过一个现有的数据库连接来初始化 *gorm.D
import (
"database/sql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
sqlDB, err := sql.Open("mysql", "mydb_dsn")
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{})
5.CRUD之创建
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
注意!!!你无法向 ‘create’ 传递结构体,所以你应该传入数据的指针!
我们还可以使用 Create() 创建多项记录:
users := []*User{
{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // pass a slice to insert multiple row
批量插入
要高效地插入大量记录,请将切片传递给Create方法。 GORM 将生成一条 SQL 来插入所有数据,以返回所有主键值,并触发 Hook 方法。 当这些记录可以被分割成多个批次时,GORM会开启一个事务</0>来处理它们。
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
for _, user := range users {
user.ID // 1,2,3
}
你可以通过db.CreateInBatches方法来指定批量插入的批次大小
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
CreateBatchSize: 1000,
})
db := db.Session(&gorm.Session{CreateBatchSize: 1000})
users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}
db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)
创建钩子
GORM允许用户通过实现这些接口 BeforeSave, BeforeCreate, AfterSave, AfterCreate来自定义钩子。 这些钩子方法会在创建一条记录时被调用,关于钩子的生命周期请参阅Hooks。
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if u.Role == "admin" {
return errors.New("invalid role")
}
return
}
创建时可用的 hook
// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务
具体代码示例:
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
err = errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
tx.Model(u).Update("role", "admin")
}
return
}
注意 在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。
详见Hook | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
跳过钩子hook
如果你想跳过Hooks方法,可以使用SkipHooks会话模式,例子如下
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)
根据map创建
GORM支持通过 map[string]interface{} 与 []map[string]interface{}{}来创建记录。
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
注意 当使用map来创建时,钩子方法不会执行,关联不会被保存且不会回写主键。
默认值
你可以通过结构体Tag default来定义字段的默认值,示例如下:
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
这些默认值会被当作结构体字段的零值插入到数据库中
注意,当结构体的字段默认值是零值的时候比如 0, '', false,这些字段值将不会被保存到数据库中,你可以使用指针类型或者Scanner/Valuer来避免这种情况。如下:
type User struct {
gorm.Model
Name string
Age *int `gorm:"default:18"`
Active sql.NullBool `gorm:"default:true"`
}
或如:
// 使用 Scanner/Valuer
type User struct {
ID int64
Name sql.NullString `gorm:"default:'小王子'"` // sql.NullString 实现了Scanner/Valuer接口
Age int64
}
user := User{Name: sql.NullString{"", true}, Age:18}
db.Create(&user) // 此时数据库中该条记录name字段的值就是''
注意,若要让字段在数据库中拥有默认值则必须使用defaultTag来为结构体字段设置默认值。如果想要在数据库迁移的时候跳过默认值,可以使用 default:(-),示例如下:
type User struct {
ID string `gorm:"default:uuid_generate_v3()"` // db func
FirstName string
LastName string
Age uint8
FullName string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}
6.CRUD之查询
一般查询
1. 根据主键查询第一条记录
db.First(&user)
// 即SELECT * FROM users ORDER BY id LIMIT 1;
2. 随机获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
3.获取最后一条记录
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
4.查询所有记录
db.Find(&user)
// SELECT * FROM users;
5.其他
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
根据主键检索
如果主键是数字类型,您可以使用 内联条件 来检索对象。 当使用字符串时,需要额外的注意来避免SQL注入;查看 Security 部分来了解详情。
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);
主键是字符串,例如,查询如下:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;
var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;
根据条件检索
(1)string 条件 //即where条件
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
// LIKE模糊查询
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00'
(2)Struct & Map查询
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 主键的切片
db.Where([]int64{20, 21, 22}).Find(&users)
//// SELECT * FROM users WHERE id IN (20, 21, 22);
注意 当通过结构体进行查询时,GORM将会只通过非零值字段查询,这意味着如果你的字段值为0,'',false或者其他零值时,将不会被用于构建查询条件,例如:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu";
你可以使用指针或实现 Scanner/Valuer 接口来避免这个问题.
7.CRUD之更新
更新所有字段
db.First(&user)
user.Name = "七米"
user.Age = 99
db.Save(&user)
//// UPDATE `users` SET `created_at` = '2020-02-16 12:52:20', `updated_at` = '2020-02-16 12:54:55', `deleted_at` = NULL, `name` = '七米', `age` = 99, `active` = true WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 1
更新修改字段
如果你只希望更新指定字段,可以使用Update或者Updates
// 更新单个属性,如果它有变化
db.Model(&user).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据给定的条件更新单个属性
db.Model(&user).Where("active = ?", true).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
// 使用 map 更新多个属性,只会更新其中有变化的属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 使用 struct 更新多个属性,只会更新其中有变化且为非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 警告:当使用 struct 更新时,GORM只会更新那些非零值的字段
// 对于下面的操作,不会发生任何更新,"", 0, false 都是其类型的零值
db.Model(&user).Updates(User{Name: "", Age: 0, Active: false})
更新选定字段
如果你想更新或忽略某些字段,你可以使用 Select,Omit
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
无Hooks更新
上面的更新操作会自动运行 model 的 BeforeUpdate, AfterUpdate 方法,更新 UpdatedAt 时间戳, 在更新时保存其 Associations, 如果你不想调用这些方法,你可以使用 UpdateColumn, UpdateColumns
// 更新单个属性,类似于 `Update`
db.Model(&user).UpdateColumn("name", "hello")
//// UPDATE users SET name='hello' WHERE id = 111;
// 更新多个属性,类似于 `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18 WHERE id = 111;
批量更新
批量更新时Hooks(钩子函数)不会运行。
db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
// 使用 struct 更新时,只会更新非零值字段,若想更新所有字段,请使用map[string]interface{}
db.Model(User{}).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18;
// 使用 `RowsAffected` 获取更新记录总数
db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected
使用SQL表达式更新
先查询表中的第一条数据保存至user变量。
var user User
db.First(&user)
db.Model(&user).Update("age", gorm.Expr("age * ? + ?", 2, 100))
//// UPDATE `users` SET `age` = age * 2 + 100, `updated_at` = '2020-02-16 13:10:20' WHERE `users`.`id` = 1;
db.Model(&user).Updates(map[string]interface{}{"age": gorm.Expr("age * ? + ?", 2, 100)})
//// UPDATE "users" SET "age" = age * '2' + '100', "updated_at" = '2020-02-16 13:05:51' WHERE `users`.`id` = 1;
db.Model(&user).UpdateColumn("age", gorm.Expr("age - ?", 1))
//// UPDATE "users" SET "age" = age - 1 WHERE "id" = '1';
db.Model(&user).Where("age > 10").UpdateColumn("age", gorm.Expr("age - ?", 1))
//// UPDATE "users" SET "age" = age - 1 WHERE "id" = '1' AND quantity > 10;
8.CRUD之删除
删除记录
警告 删除记录时,请确保主键字段有值,GORM 会通过主键去删除记录,如果主键为空,GORM 会删除该 model 的所有记录。
// 删除现有记录
db.Delete(&email)
//// DELETE from emails where id=10;
// 为删除 SQL 添加额外的 SQL 操作
db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email)
//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN);
批量删除
删除全部匹配的记录
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
//// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
//// DELETE from emails where email LIKE "%jinzhu%";
软删除
如果一个 model 有 DeletedAt 字段,他将自动获得软删除的功能! 当调用 Delete 方法时, 记录不会真正的从数据库中被删除, 只会将DeletedAt 字段的值会被设置为当前时间
db.Delete(&user)
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// 查询记录时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
// Unscoped 方法可以查询被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
//// SELECT * FROM users WHERE age = 20;
物理删除
// Unscoped 方法可以物理删除记录
db.Unscoped().Delete(&order)
//// DELETE FROM orders WHERE id=10;
9.开启博客之路
快速进入状态
go mod init 是Gromlearn
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong!"})
})
err := r.Run(":9000")
if err != nil {
panic("route start defeat!")
}
}
我们可以看到启动了服务后,输出了许多运行信息,如果你第一次看,可能会有些懵,在这里我们对运行信息做一个初步的概括分析,分为以下四大块:
- 默认 Engine 实例:当前默认使用了官方所提供的 Logger 和 Recovery 中间件创建了 Engine 实例。
- 运行模式:当前为调试模式,并建议若在生产环境时切换为发布模式。
- 路由注册:注册了
GET /ping的路由,并输出其调用方法的方法名。 - 运行信息:本次启动时监听 9000端口。
gin一点源码分析
对源码进行大体分析,一探究竟,简单的解剖一下里面的秘密,整体分析流程如下:
gin.Default()创建默认Engine实例,引入两个中间件logger和recovery。
gin.New 该方法会进行Engine实例初始化动作并返回
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
RouterGroup:路由组
RedirectTrailingSlash:是否自动重定向
RedirectFixedPath:是否尝试修复当前请求路径
HandleMethodNotAllowed:判断当前路由是否允许调用其他方法
ForwardedByClientIP:如果开启,则尽可能的返回真实的客户端 IP,先从 X-Forwarded-For 取值,如果没有再从 X-Real-Ip。
UseRawPath:如果开启,则会使用
url.RawPath来获取请求参数,不开启则还是按 url.Path 去获取。UnescapePathValues:是否对路径值进行转义
MaxMultipartMemory:相对应
http.Request ParseMultipartForm方法,用于控制最大的文件上传大小。trees:多个压缩字典树(Radix Tree),每个树都对应着一种 HTTP Method。
delims:用于 HTML 模板的左右定界符。
r.GET()
其注册的具体内容
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
1.计算路由的绝对路径
2.合并现有和新注册的 Handler,并创建一个函数链 HandlersChain。
3.追加:将当前注册的路由规则(含 HTTP Method、Path、Handlers)追加到对应的树中。
r.Run()
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
该方法会通过解析地址,再调用
http.ListenAndServe将 Engine 实例作为Handler注册进去,然后启动服务,开始对外提供 HTTP 服务。
在 gin 中,Engine 这一个结构体确确实实是实现了 ServeHTTP 方法的,也就是符合 http.Handler 接口标准,代码如下:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
- 从
sync.Pool对象池中获取一个上下文对象。- 重新初始化取出来的上下文对象。
- 处理外部的 HTTP 请求。
- 处理完毕,将取出的上下文对象返回给对象池。
10.进行项目设计
项目结构
先将项目的标准目录结构创建起来,便于后续的开发,最终目录结构如下:
blog-service
├── configs
├── docs
├── global
├── internal
│ ├── dao
│ ├── middleware
│ ├── model
│ ├── routers
│ └── service
├── pkg
├── storage
├── scripts
└── third_party
-
configs:配置文件。
-
docs:文档集合。
-
global:全局变量。
-
internal:内部模块。
- dao:数据访问层(Database Access Object),所有与数据相关的操作都会在 dao 层进行,例如 MySQL、ElasticSearch 等。
- middleware:HTTP 中间件。
- model:模型层,用于存放 model 对象。
- routers:路由相关逻辑处理。
- service:项目核心业务逻辑。
-
pkg:项目相关的模块包。
-
storage:项目生成的临时文件。
-
scripts:各类构建,安装,分析等操作的脚本。
-
third_party:第三方的资源工具,例如 Swagger UI。
创建数据库
CREATE DATABASE
IF
NOT EXISTS blog_service DEFAULT CHARACTER
SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
通过上述 SQL 语句,如果名为 blog_service 的数据库不存在,那么就在 MySQL 中创建它,并将其默认字符编码设置为 utf8mb4,默认排序规则设置为 utf8mb4_general_ci。
注解
DEFAULT CHARACTER SET utf8mb4:这部分是在设置数据库的默认字符编码。utf8mb4是一种 Unicode 字符编码格式,它是utf8的超集,能够支持更多的 Unicode 字符,包括一些特殊字符和 emoji 表情等。通过设置为utf8mb4作为默认字符编码,可以确保在这个数据库中存储的文本数据(如文章内容、用户评论等)能够正确地处理各种字符,减少因字符编码不匹配而导致的乱码等问题。
DEFAULT COLLATE utf8mb4_general_ci:这里是在设置数据库的默认排序规则。COLLATE用于指定字符数据的排序方式。utf8mb4_general_ci是一种比较常用的utf8mb4字符编码下的排序规则,其中ci表示不区分大小写(Case Insensitive)。也就是说,在对数据库中的字符数据进行排序操作时(比如在执行ORDER BY语句时),会按照不区分大小写的方式进行排序,这在很多实际应用场景中是比较方便的,例如在查询用户列表时,无论用户输入的是大写还是小写的用户名,都能按照统一的规则进行排序和展示。在数据库相关的 SQL 语句中,“default character”(完整的是 “default character set”)是用于指定数据库、表或者列的默认字符集。
以 “CREATE DATABASE blog_service DEFAULT CHARACTER SET utf8mb4” 为例,“default character set” 部分传达了要为数据库 “blog_service” 定义默认字符集的意图,而 “set” 关键字引导出具体的字符集(utf8mb4),这个字符集将被应用作为该数据库的默认字符编码方式。这就好像是一个指令的两个部分,前面部分说明要做什么(设置默认字符集),后面部分通过 “set” 关键字具体执行这个操作(指定 utf8mb4 为字符集)。
创建成功
创建表
所有表都会包含如下公共字段:
`created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
`modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
`deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间',
`is_del` tinyint(3) unsigned DEFAULT '0' COMMENT '是否删除 0 为未删除、1 为已删除',
(1)创建标签表
创建标签表,表字段主要为标签的名称、状态以及公共字段。
CREATE TABLE `blog_tag` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT '' COMMENT '标签名称',
# 此处请写入公共字段
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0 为禁用、1 为启用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签管理';
井号处写公共字段,执行如下
ENGINE=InnoDB:
ENGINE是指存储引擎。存储引擎是数据库管理系统用于存储、处理和检索数据的底层软件组件。MySQL 支持多种存储引擎,如MyISAM、InnoDB等。InnoDB是目前 MySQL 中最常用的存储引擎之一。它支持事务处理(ACID 特性),具有行级锁定、外键约束等高级功能,适合用于处理大量的读写操作,尤其是在涉及事务操作的应用场景(如电商系统中的订单处理、金融系统中的交易记录等)中表现出色。
DEFAULT CHARSET=utf8mb4:
- 这部分与前面提到的设置字符集相关内容类似。
DEFAULT CHARSET表示设置默认的字符编码。utf8mb4是指定的字符集,它是一种广泛使用的 Unicode 字符编码格式,能够处理包括各种语言文字以及 emoji 表情等多种字符形式,确保在表中存储的文本数据可以正确地进行编码和解码,避免出现字符乱码等问题。
COMMENT关键字用于为表添加注释。注释内容在 SQL 语句中主要起到说明的作用,对数据库的功能和用途没有直接的影响。
(2)创建文章表
创建文章表,表字段主要为文章的标题、封面图、内容概述以及公共字段。
CREATE TABLE `blog_article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(100) DEFAULT '' COMMENT '文章标题',
`desc` varchar(255) DEFAULT '' COMMENT '文章简述',
`cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址',
`content` longtext COMMENT '文章内容',
# 此处请写入公共字段
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0 为禁用、1 为启用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章管理';
(3)创建文章标签关联表
创建文章标签关联表,这个表主要用于记录文章和标签之间的 1:N 的关联关系。
CREATE TABLE `blog_article_tag` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`article_id` int(11) NOT NULL COMMENT '文章 ID',
`tag_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '标签 ID',
# 此处请写入公共字段
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联';
建成如下
创建model
(1)创建公共model
在 internal/model 目录下创建 model.go 文件,写入如下代码:
type Model struct {
ID uint32 `gorm:"primary_key" json:"id"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
CreatedOn uint32 `json:"created_on"`
ModifiedOn uint32 `json:"modified_on"`
DeletedOn uint32 `json:"deleted_on"`
IsDel uint8 `json:"is_del"`
}
(2)创建标签model
在 internal/model 目录下创建 article.go 文件,写入如下代码:
type Tag struct {
*Model
Name string `json:"name"`
State uint8 `json:"state"`
}
func (t Tag) TableName() string {
return "blog_tag"
}
(3)创建文章标签 model
在 internal/model 目录下创建 article_tag.go 文件,写入如下代码:
type ArticleTag struct {
*Model
TagID uint32 `json:"tag_id"`
ArticleID uint32 `json:"article_id"`
}
func (a ArticleTag) TableName() string {
return "blog_article_tag"
}
路由
在完成数据库的设计后,我们需要对业务模块的管理接口进行设计,而在这一块最核心的就是增删改查的 RESTful API 设计和编写,在 RESTful API 中 HTTP 方法对应的行为动作分别如下:
- GET:读取/检索动作。
- POST:新增/新建动作。
- PUT:更新动作,用于更新一个完整的资源,要求为幂等。
- PATCH:更新动作,用于更新某一个资源的一个组成部分,也就是只需要更新该资源的某一项,就应该使用 PATCH 而不是 PUT,可以不幂等。
- DELETE:删除动作。
(1)标签管理
| 功能 | HTTP 方法 | 路径 |
|---|---|---|
| 新增标签 | POST | /tags |
| 删除指定标签 | DELETE | /tags/:id |
| 更新指定标签 | PUT | /tags/:id |
| 获取标签列表 | GET | /tags |
(2)文章管理
| 功能 | HTTP 方法 | 路径 |
|---|---|---|
| 新增文章 | POST | /articles |
| 删除指定文章 | DELETE | /articles/:id |
| 更新指定文章 | PUT | /articles/:id |
| 获取指定文章 | GET | /articles/:id |
| 获取文章列表 | GET | /articles |
(3)路由管理
在确定了业务接口设计后,需要对业务接口进行一个基础编码,确定其方法原型,把当前工作区切换到项目目录的 internal/routers 下,并新建 router.go 文件,写入代码:
func NewRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
apiv1 := r.Group("/api/v1")
{
apiv1.POST("/tags")
apiv1.DELETE("/tags/:id")
apiv1.PUT("/tags/:id")
apiv1.PATCH("/tags/:id/state")
apiv1.GET("/tags")
apiv1.POST("/articles")
apiv1.DELETE("/articles/:id")
apiv1.PUT("/articles/:id")
apiv1.PATCH("/articles/:id/state")
apiv1.GET("/articles/:id")
apiv1.GET("/articles")
}
return r
}
如图:
处理程序
接下来编写对应路由的处理方法,我们在项目目录下新建 internal/routers/api/v1 文件夹,并新建 tag.go(标签)和 article.go(文章)文件,写入代码下述代码。
(1)tag.go 文件
type Tag struct {}
func NewTag() Tag {
return Tag{}
}
func (t Tag) Get(c *gin.Context) {}
func (t Tag) List(c *gin.Context) {}
func (t Tag) Create(c *gin.Context) {}
func (t Tag) Update(c *gin.Context) {}
func (t Tag) Delete(c *gin.Context) {}
(2)article.go 文件
type Article struct{}
func NewArticle() Article {
return Article{}
}
func (a Article) Get(c *gin.Context) {}
func (a Article) List(c *gin.Context) {}
func (a Article) Create(c *gin.Context) {}
func (a Article) Update(c *gin.Context) {}
func (a Article) Delete(c *gin.Context) {}
(3) 路由管理
在编写好路由的 Handler 方法后,我们只需要将其注册到对应的路由规则上就好了,打开项目目录下 internal/routers 的 router.go 文件,修改如下:
...
article := v1.NewArticle()
tag := v1.NewTag()
apiv1 := r.Group("/api/v1")
{
apiv1.POST("/tags", tag.Create)
apiv1.DELETE("/tags/:id", tag.Delete)
apiv1.PUT("/tags/:id", tag.Update)
apiv1.PATCH("/tags/:id/state", tag.Update)
apiv1.GET("/tags", tag.List)
apiv1.POST("/articles", article.Create)
apiv1.DELETE("/articles/:id", article.Delete)
apiv1.PUT("/articles/:id", article.Update)
apiv1.PATCH("/articles/:id/state", article.Update)
apiv1.GET("/articles/:id", article.Get)
apiv1.GET("/articles", article.List)
}
启动接入
在完成了模型、路由的代码编写后,我们修改前面章节所编写的 main.go 文件,把它改造为这个项目的启动文件,修改代码如下:
func main() {
router := routers.NewRouter()
s := &http.Server{
Addr: ":9000",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
我们通过自定义 http.Server,设置了监听的 TCP Endpoint、处理的程序、允许读取/写入的最大时间、请求头的最大字节数等基础参数,最后调用 ListenAndServe 方法开始监听。
运行
11.编写公共组件
错误码标准化
(1)公共错误码
我们需要在在项目目录下的 pkg/errcode 目录新建 common_code.go 文件,用于预定义项目中的一些公共错误码,便于引导和规范大家的使用,如下:
var (
Success = NewError(0, "成功")
ServerError = NewError(10000000, "服务内部错误")
InvalidParams = NewError(10000001, "入参错误")
NotFound = NewError(10000002, "找不到")
UnauthorizedAuthNotExist = NewError(10000003, "鉴权失败,找不到对应的 AppKey 和 AppSecret")
UnauthorizedTokenError = NewError(10000004, "鉴权失败,Token 错误")
UnauthorizedTokenTimeout = NewError(10000005, "鉴权失败,Token 超时")
UnauthorizedTokenGenerate = NewError(10000006, "鉴权失败,Token 生成失败")
TooManyRequests = NewError(10000007, "请求过多")
)
(2)错误处理
接下来我们在项目目录下的 pkg/errcode 目录新建 errcode.go 文件,编写常用的一些错误处理公共方法,标准化我们的错误输出,如下:
type Error struct {
code int `json:"code"`
msg string `json:"msg"`
details []string `json:"details"`
}
var codes = map[int]string{}
func NewError(code int, msg string) *Error {
if _, ok := codes[code]; ok {
panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
}
codes[code] = msg
return &Error{code: code, msg: msg}
}
func (e *Error) Error() string {
return fmt.Sprintf("错误码:%d, 错误信息:%s", e.Code(), e.Msg())
}
func (e *Error) Code() int {
return e.code
}
func (e *Error) Msg() string {
return e.msg
}
func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}
func (e *Error) Details() []string {
return e.details
}
func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
for _, d := range details {
newError.details = append(newError.details, d)
}
return &newError
}
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case ServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case UnauthorizedAuthNotExist.Code():
fallthrough
case UnauthorizedTokenError.Code():
fallthrough
case UnauthorizedTokenGenerate.Code():
fallthrough
case UnauthorizedTokenTimeout.Code():
return http.StatusUnauthorized
case TooManyRequests.Code():
return http.StatusTooManyRequests
}
return http.StatusInternalServerError
}
在错误码方法的编写中,我们声明了 Error 结构体用于表示错误的响应结果,并利用 codes 作为全局错误码的存储载体,便于查看当前注册情况,并在调用 NewError 创建新的 Error 实例的同时进行排重的校验。
另外相对特殊的是 StatusCode 方法,它主要用于针对一些特定错误码进行状态码的转换,因为不同的内部错误码在 HTTP 状态码中都代表着不同的意义,我们需要将其区分开来,便于客户端以及监控/报警等系统的识别和监听。
配置管理
(1)安装viper
为了完成文件配置的读取,我们需要借助第三方开源库 viper,在项目根目录下执行以下安装命令:
$ go get -u github.com/spf13/viper@v1.4.0
Viper 是适用于 Go 应用程序的完整配置解决方案,是目前 Go 语言中比较流行的文件配置解决方案,它支持处理各种不同类型的配置需求和配置格式。
(2)配置
在项目目录下的 configs 目录新建 config.yaml 文件,写入以下配置:
Server:
RunMode: debug
HttpPort: 8000
ReadTimeout: 60
WriteTimeout: 60
App:
DefaultPageSize: 10
MaxPageSize: 100
LogSavePath: storage/logs
LogFileName: app
LogFileExt: .log
Database:
DBType: mysql
Username: root # 填写你的数据库账号
Password: rootroot # 填写你的数据库密码
Host: 127.0.0.1:3306
DBName: blog_service
TablePrefix: blog_
Charset: utf8
ParseTime: True
MaxIdleConns: 10
MaxOpenConns: 30
在配置文件中,我们分别针对如下内容进行了默认配置:
- Server:服务配置,设置 gin 的运行模式、默认的 HTTP 监听端口、允许读取和写入的最大持续时间。
- App:应用配置,设置默认每页数量、所允许的最大每页数量以及默认的应用日志存储路径。
- Database:数据库配置,主要是连接实例所必需的基础参数。
(3)编写组件
在完成了配置文件的确定和编写后,我们需要针对读取配置的行为进行封装,便于应用程序的使用,我们在项目目录下的 pkg/setting 目录下新建 setting.go 文件,写入如下代码:
type Setting struct {
vp *viper.Viper
}
func NewSetting() (*Setting, error) {
vp := viper.New()
vp.SetConfigName("config")
vp.AddConfigPath("configs/")
vp.SetConfigType("yaml")
err := vp.ReadInConfig()
if err != nil {
return nil, err
}
return &Setting{vp}, nil
}
在这里我们编写了 NewSetting 方法,用于初始化本项目的配置的基础属性,设定配置文件的名称为 config,配置类型为 yaml,并且设置其配置路径为相对路径 configs/,以此确保在项目目录下执行运行时能够成功启动。
另外 viper 是允许设置多个配置路径的,这样子可以尽可能的尝试解决路径查找的问题,也就是可以不断地调用 AddConfigPath 方法,这块在后续会再深入介绍。
接下来我们新建 section.go 文件,用于声明配置属性的结构体并编写读取区段配置的配置方法,如下:
type ServerSettingS struct {
RunMode string
HttpPort string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type AppSettingS struct {
DefaultPageSize int
MaxPageSize int
LogSavePath string
LogFileName string
LogFileExt string
}
type DatabaseSettingS struct {
DBType string
UserName string
Password string
Host string
DBName string
TablePrefix string
Charset string
ParseTime bool
MaxIdleConns int
MaxOpenConns int
}
func (s *Setting) ReadSection(k string, v interface{}) error {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
return nil
}
(4)包全局变量
在读取了文件的配置信息后,还是不够的,因为我们需要将配置信息和应用程序关联起来,我们才能够去使用它,因此在项目目录下的 global 目录下新建 setting.go 文件,写入如下代码:
var (
ServerSetting *setting.ServerSettingS
AppSetting *setting.AppSettingS
DatabaseSetting *setting.DatabaseSettingS
)
我们针对最初预估的三个区段配置,进行了全局变量的声明,便于在接下来的步骤将其关联起来,并且提供给应用程序内部调用。
另外全局变量的初始化,是会随着应用程序的不断演进不断改变的,因此并不是一成不变,也就是这里展示的并不一定是最终的结果。
(5)初始化配置读取
在完成了所有的预备行为后,我们回到项目根目录下的 main.go 文件,修改代码如下:
func init() {
err := setupSetting()
if err != nil {
log.Fatalf("init.setupSetting err: %v", err)
}
}
func main() {...}
func setupSetting() error {
setting, err := setting.NewSetting()
if err != nil {
return err
}
err = setting.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = setting.ReadSection("App", &global.AppSetting)
if err != nil {
return err
}
err = setting.ReadSection("Database", &global.DatabaseSetting)
if err != nil {
return err
}
global.ServerSetting.ReadTimeout *= time.Second
global.ServerSetting.WriteTimeout *= time.Second
return nil
}
我们新增了一个 init 方法,在 Go 语言中,init 方法常用于应用程序内的一些初始化操作,它在 main 方法之前自动执行,它的执行顺序是:全局变量初始化 =》init 方法 =》main 方法,但并不是建议滥用,因为如果 init 过多,你可能会迷失在各个库的 init 方法中,会非常麻烦。
而在我们的应用程序中,该 init 方法主要作用是进行应用程序的初始化流程控制,整个应用代码里也只会有一个 init 方法,因此我们在这里调用了初始化配置的方法,达到配置文件内容映射到应用配置结构体的作用。
(6)修改服务端配置
接下来我们只需要在启动文件 main.go 中把已经映射好的配置和 gin 的运行模式进行设置,这样的话,在程序重新启动时后就可以生效,如下:
func main() {
gin.SetMode(global.ServerSetting.RunMode)
router := routers.NewRouter()
s := &http.Server{
Addr: ":" + global.ServerSetting.HttpPort,
Handler: router,
ReadTimeout: global.ServerSetting.ReadTimeout,
WriteTimeout: global.ServerSetting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
验证如下
日志如下 证明结构体配置正确
PS D:\GOfiles\Gormlearn> go run main.go
2024/11/28 15:08:30 global.ServerSetting: &{RunMode:debug HttpPort:9000 ReadTimeout:1m0s WriteTimeout:1m0s}
2024/11/28 15:08:30 global.AppSetting: &{DefaultPageSize:10 MaxPageSize:100 LogSavePath:storage/logs LogFileName:app LogFileExt:.log}
2024/11/28 15:08:30 global.DatabaseSetting: &{DBType:mysql UserName:root Password:wxe13867187633 Host:127.0.0.1:3306 DBName:blog_service TablePrefix:blog_ Charset:utf8 ParseTime:true MaxIdleConns:10 MaxOpenConns:30}
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /api/v1/tags --> Gormlearn/internal/routers/api/v1.Tag.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> Gormlearn/internal/routers/api/v1.Tag.Delete-fm (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PATCH /api/v1/tags/:id/state --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] GET /api/v1/tags --> Gormlearn/internal/routers/api/v1.Tag.List-fm (3 handlers)
[GIN-debug] POST /api/v1/articles --> Gormlearn/internal/routers/api/v1.Article.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Delete-fm (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] PATCH /api/v1/articles/:id/state --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Get-fm (3 handlers)
[GIN-debug] GET /api/v1/articles --> Gormlearn/internal/routers/api/v1.Article.List-fm (3 handlers)
数据库链接
(1)gorm安装
我们在本项目中数据库相关的数据操作将使用第三方的开源库 gorm,它是目前 Go 语言中最流行的 ORM 库(从 Github Star 来看),同时它也是一个功能齐全且对开发人员友好的 ORM 库,目前在 Github 上相当的活跃,具有一定的保障,安装命令如下:
$ go get -u github.com/jinzhu/gorm@v1.9.12
(2)编写组件
我们打开项目目录 internal/model 下的 model.go 文件,新增 NewDBEngine 方法,如下:
import (
...
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type Model struct {...}
func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) {
s := "%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local"
db, err := gorm.Open(databaseSetting.DBType, fmt.Sprintf(s,
databaseSetting.UserName,
databaseSetting.Password,
databaseSetting.Host,
databaseSetting.DBName,
databaseSetting.Charset,
databaseSetting.ParseTime,
))
if err != nil {
return nil, err
}
if global.ServerSetting.RunMode == "debug" {
db.LogMode(true)
}
db.SingularTable(true)
db.DB().SetMaxIdleConns(databaseSetting.MaxIdleConns)
db.DB().SetMaxOpenConns(databaseSetting.MaxOpenConns)
return db, nil
}
我们通过上述代码,编写了一个针对创建 DB 实例的 NewDBEngine 方法,同时增加了 gorm 开源库的引入和 MySQL 驱动库 github.com/jinzhu/gorm/dialects/mysql 的初始化(不同类型的 DBType 需要引入不同的驱动库,否则会存在问题)。
增删改查最终实现
部分代码如下
package main
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"net/http"
)
var DB *gorm.DB
func initMySQL() (err error) {
dsn := "root:root@tcp(127.0.0.1:13306)/bubble?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
return
}
return nil
}
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Status bool `json:"status"`
}
func main() {
// 连接数据库
err := initMySQL()
if err != nil {
panic(err)
}
// 模型绑定
err = DB.AutoMigrate(&Todo{})
if err != nil {
return
}
r := gin.Default()
v1Group := r.Group("/v1")
{
// 添加
v1Group.POST("/todo", func(c *gin.Context) {
// 1.从请求中取出数据
var todo Todo
if err = c.ShouldBindJSON(&todo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
// 2.存入数据库
if err = DB.Create(&todo).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
} else {
// 3.返回响应
c.JSON(http.StatusOK, gin.H{"data": todo})
}
})
// 查看所有的待办事项
v1Group.GET("/todo", func(c *gin.Context) {
var todoList []Todo
if err = DB.Find(&todoList).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"data": todoList})
}
})
// 查看某个待办事项
v1Group.GET("/todo/:id", func(c *gin.Context) {
var todo Todo
if err = DB.First(&todo, c.Param("id")).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"data": todo})
}
})
// 更新某一个待办事项
v1Group.PUT("/todo/:id", func(c *gin.Context) {
id, ok := c.Params.Get("id")
if !ok {
c.JSON(http.StatusOK, gin.H{"error": "无效的id"})
}
var todo Todo
if err = DB.Where("id = ?", id).First(&todo).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.ShouldBindJSON(&todo)
if err = DB.Save(&todo).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"data": todo})
}
})
// 删除某个待办事项
v1Group.DELETE("/todo/:id", func(c *gin.Context) {
var todo Todo
id, ok := c.Params.Get("id")
if !ok {
c.JSON(http.StatusOK, gin.H{"error": "无效的id"})
}
if err = DB.Where("id = ?", id).Delete(&Todo{}).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"data": todo})
}
})
}
err = r.Run(":8080")
if err != nil {
return
}
}
部分日志
PS D:\GOfiles\Gormlearn> go run main.go
2024/11/28 15:08:30 global.ServerSetting: &{RunMode:debug HttpPort:9000 ReadTimeout:1m0s WriteTimeout:1m0s}
2024/11/28 15:08:30 global.AppSetting: &{DefaultPageSize:10 MaxPageSize:100 LogSavePath:storage/logs LogFileName:app LogFileExt:.log}
2024/11/28 15:08:30 global.DatabaseSetting: &{DBType:mysql UserName:root Password:wxe13867187633 Host:127.0.0.1:3306 DBName:blog_service TablePrefix:blog_ Charset:utf8 ParseTime:true MaxIdleConns:10 MaxOpenConns:30}
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /api/v1/tags --> Gormlearn/internal/routers/api/v1.Tag.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> Gormlearn/internal/routers/api/v1.Tag.Delete-fm (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PATCH /api/v1/tags/:id/state --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] GET /api/v1/tags --> Gormlearn/internal/routers/api/v1.Tag.List-fm (3 handlers)
[GIN-debug] POST /api/v1/articles --> Gormlearn/internal/routers/api/v1.Article.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Delete-fm (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] PATCH /api/v1/articles/:id/state --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> Gormlearn/internal/routers/api/v1.Article.Get-fm (3 handlers)
[GIN-debug] GET /api/v1/articles --> Gormlearn/internal/routers/api/v1.Article.List-fm (3 handlers)
复杂实现
简单实现之一