19-GORM
- 原生sql,用sqlx
- 强需求,GORM,需要额外文档固定用法
- 熟练gorm后,sql效率高一些
官网
https://gorm.io/zh_CN/docs/ 看官网,可以获取gorm所有玩法
The fantastic ORM library for Golang aims to be developer friendly.
特性
- 全功能 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…
- 每个特性都经过了测试的重重考验
- 开发者友好
大公司表设计,都会携带如下字段,因此gorm推荐你创建结构体,嵌入该gorm.Model,引入如下字段。
小项目,有外键。
大项目,DBA不用外键。
package gorm
import "time"
// Model a basic GoLang struct which includes the following fields: ID, CreatedAt, UpdatedAt, DeletedAt
// It may be embedded into your model or you may build your own model without it
// type User struct {
// gorm.Model
// }
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}

安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
mysql链接
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm" //官网包
)
// 对应一张表,对应好字段
type Product struct {
//嵌入包提供的几个字段
gorm.Model
Code string
Price uint
}
func main() {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 迁移 schema,数据库概念
// 生产环境不写,需要人工校验数据库表设计
// 自动建表,代码结构体 > 表更新,增量表更新
//运行后,自动生成mysql表 > products
db.AutoMigrate(&Product{})
}
迁移生成的表
MariaDB [sql_test]> show create table products;
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| products | CREATE TABLE `products` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
`code` longtext,
`price` bigint(20) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_products_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
MariaDB [sql_test]>
MariaDB [sql_test]> desc products;
+------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| created_at | datetime(3) | YES | | NULL | |
| updated_at | datetime(3) | YES | | NULL | |
| deleted_at | datetime(3) | YES | MUL | NULL | |
| code | longtext | YES | | NULL | |
| price | bigint(20) unsigned | YES | | NULL | |
+------------+---------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
增删改查
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm" //官网包
)
// 对应一张表,对应好字段
type Product struct {
//嵌入包提供的几个字段
gorm.Model
Code string
Price uint
}
func main() {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 迁移 schema,数据库概念
// 生产环境不写,需要人工校验数据库表设计
// 自动建表,代码结构体 > 表更新,增量表更新
//运行后,自动生成mysql表 > products
//db.AutoMigrate(&Product{})
// Create,传入结构体,以及字段的值,写入数据
//p1 := &Product{Code: "shop01", Price: 100}
//p1 := &Product{Code: "shop02", Price: 99}
//db.Create(p1)
//自定义结构体,存储表数据
// Read
//数据读出来,写入结构体变量
var p2 Product
db.First(&p2, 2) // 根据整型主键查找
fmt.Printf("p2: %v\n", p2)
//var p3 Product
//db.First(&p3, "code = ?", "shop01") // 查找 code 字段值为 D42 的记录
//fmt.Printf("p3: %#v\n", p3)
// Update - 将 p2 的 price 更新为 9999
//db.Debug().Model(&p2).Update("Price", 1699)
db.Model(&p2).Update("Price", 1055)
fmt.Printf("p2更新后: %v\n", p2)
//db.Model(&p2).Update("Price", 9999)
// Updates方法 - 更新多个字段
//显示,gorm转换的sql语句
//db.Debug().Model(&p3).Updates(Product{Price: 66666, Code: "F42"}) // 仅更新非零值字段
//db.Model(&p3).Updates(Product{Price: 66666, Code: "F42"}) // 仅更新非零值字段
//
//db.First(&p2, 2)
//fmt.Println("最新p2数据:", p2)
//db.First(&p3, "Price=?", 66666)
//
//fmt.Println("最新p3数据:", p3)
//db.Model(&p3).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
//// Delete - 删除 主键为2的数据
db.Debug().Delete(&p2, 2)
}
结果
MariaDB [sql_test]> select * from products;
+----+-------------------------+-------------------------+------------+--------+-------+
| id | created_at | updated_at | deleted_at | code | price |
+----+-------------------------+-------------------------+------------+--------+-------+
| 1 | 2023-02-08 11:02:28.272 | 2023-02-08 11:17:35.832 | NULL | F42 | 200 |
| 2 | 2023-02-08 11:05:59.235 | 2023-02-08 11:30:09.643 | NULL | shop02 | 1055 |
+----+-------------------------+-------------------------+------------+--------+-------+
2 rows in set (0.00 sec)
删除,做了一个软删除,标识了删除时间
MariaDB [sql_test]> select * from products;
+----+-------------------------+-------------------------+-------------------------+--------+-------+
| id | created_at | updated_at | deleted_at | code | price |
+----+-------------------------+-------------------------+-------------------------+--------+-------+
| 1 | 2023-02-08 11:02:28.272 | 2023-02-08 11:17:35.832 | NULL | F42 | 200 |
| 2 | 2023-02-08 11:05:59.235 | 2023-02-08 11:30:09.643 | 2023-02-08 11:33:40.450 | shop02 | 1055 |
+----+-------------------------+-------------------------+-------------------------+--------+-------+
2 rows in set (0.00 sec)
当标记删除时间后,再读取
➜ goStudy go run demo.go
2023/02/08 11:34:46 /Users/yuchao/goStudy/demo.go:44 record not found
[20.835ms] [rows:0] SELECT * FROM `products` WHERE `products`.`id` = 2 AND `products`.`deleted_at` IS NULL ORDER BY `products`.`id` LIMIT 1
读出来p2: {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0}
模型
https://gorm.io/zh_CN/docs/models.html
模型定义
有三方库,支持读取table 自动转成struct。
模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成
例如:
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
表明约定
GORM 倾向于约定优于配置 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间。
蛇形是指,mysql 表中的字段风格,下划线连接。
MariaDB [sql_test]> select * from products;
+----+-------------------------+-------------------------+-------------------------+--------+-------+
| id | created_at | updated_at | deleted_at | code | price |
如果您遵循 GORM 的约定,您就可以少写的配置、代码。 如果约定不符合您的实际要求,GORM 允许你配置它们
修改默认的负数表名,自己临时指定表名
db.Table("user").Where(fmt.Sprintf("name = %v", userIn)).First(&u1)
高级选项
字段级权限控制
可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略
注意: 使用 GORM Migrator 创建表时,不会创建被忽略的字段
<- 读,允许写
-> 只读
->;<- 允许读,写
- 忽略字段
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 读写会忽略该字段
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"` // 使用时间戳秒数填充创建时间
}
嵌入结构体
对于匿名字段,GORM 会将其字段包含在父结构体中,例如:
type User struct {
gorm.Model
Name string
}
// 等效于
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}
对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:
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
}
并且,您可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀,例如:
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
字段标签
// 对应一张表,对应好字段
type Product struct {
//嵌入包提供的几个字段
gorm.Model
//支持传入tag,修改字段属性
Code string `gorm:"column:code,varchar(255)"`
Price uint `gorm:"column:price"`
}
声明 model 时,tag 是可选的,GORM 支持以下 tag: tag 名大小写不敏感,但建议使用 camelCase 风格
| 标签名 | 说明 |
|---|---|
| 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 | 迁移时为字段添加注释 |
关联标签
GORM 允许通过标签为关联配置外键、约束、many2many 表,详情请参考 关联部分
数据库连接
https://gorm.io/zh_CN/docs/connecting_to_the_database.html
例如有公司现成的数据库连接,可以快速引入GORM。
CRUD接口
创建
https://gorm.io/zh_CN/docs/create.html
批量写入
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm" //官网包
)
// 对应一张表,对应好字段
type Product struct {
//嵌入包提供的几个字段
gorm.Model
//支持传入tag,修改字段属性
Code string
Price uint
}
func main() {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//批量写入
var prods = []Product{
{Code: "apple1", Price: 10},
{Code: "apple2", Price: 11},
{Code: "apple3", Price: 12},
{Code: "apple4", Price: 13},
}
db.Debug().CreateInBatches(prods, len(prods))
}
如何区分默认值?指针
https://gorm.io/zh_CN/docs/create.html#%E9%BB%98%E8%AE%A4%E5%80%BC
package main
import (
"database/sql"
"gorm.io/driver/mysql"
"gorm.io/gorm" //官网包
)
// 对应一张表,对应好字段
type Product struct {
//嵌入包提供的几个字段
gorm.Model
//支持传入tag,修改字段属性
Code string
Price uint
//判断值类型,到底是否是零值,用指针,指针不传值是nil
Active *bool
//判断值类型,到底是否是零值,方案2
Active sql.NullBool
}
func main() {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 如何判别默认值
// 结构体字段,默认零值,如 "" 0 false
//没有传值,Active默认值是nil
var p1 = Product{Code: "p1"}
//主动传值了,表示有值,就是false
var p2 = Product{Code: "p2", Active: sql.NullBool{Bool: false,Valid: true}}
}
GORM数据查询
https://gorm.io/zh_CN/docs/query.html
GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。
当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
如果你想避免
ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。Using
Findwithout a limit for single objectdb.Find(&user)will query the full table and return only the first object which is not performant and nondeterministicFirst 和 Last 方法将(分别)找到按主键排序的第一条和最后一条记录。
它们仅在将指向目标结构的指针作为参数传递给方法或使用 db.Model() 指定模型时才起作用。 此外,如果没有为相关模型定义主键,则该模型将按第一个字段排序。
分页
https://gorm.io/zh_CN/docs/scopes.html#pagination
内联条件
https://gorm.io/zh_CN/docs/query.html#%E5%86%85%E8%81%94%E6%9D%A1%E4%BB%B6
高级查询
https://gorm.io/zh_CN/docs/advanced_query.html
Find至map
每一个查出来的数据,都要指定一个结构体,太麻烦,可以构造map,接收多个数据
更新
https://gorm.io/zh_CN/docs/update.html
删除
https://gorm.io/zh_CN/docs/delete.html
gorm一般是软删除
您也可以使用 Unscoped 永久删除匹配的记录
db.Unscoped().Delete(&order)// DELETE FROM orders WHERE id=10;
GORM安全
https://gorm.io/zh_CN/docs/security.html
GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据
注意 Logger 打印的 SQL 并不像最终执行的 SQL 那样已经转义,复制和运行这些 SQL 时应当注意。
用户的输入只能作为参数,例如:
userInput := "jinzhu;drop table users;"
// 安全的,会被转义
db.Where("name = ?", userInput).First(&user)
// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)
危险sql注入问题
package main
import (
"database/sql"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm" //官网包
"time"
)
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//测试sql注入,gorm默认的安全转义
userIn := "'超哥6';drop table products;"
var u1 User
//安全sql,
//SELECT * FROM `users` WHERE name='超哥6;drop table products;' ORDER BY `users`.`id` LIMIT 1
//db.Debug().Where("name=?", userIn).First(&u1)
//危险sql,存在注入,自己拼接字符串,分号语法,导致sql执行
//SELECT * FROM `users` WHERE name=超哥6;drop table products; ORDER BY `users`.`id` LIMIT 1
db.Table("user").Where(fmt.Sprintf("name = %v", userIn)).First(&u1)
fmt.Println(u1)
}
GORM执行原生sql
https://gorm.io/zh_CN/docs/sql_builder.html
k8s里也有DryRun方法,测试执行,不产生结果。
Context
https://gorm.io/zh_CN/docs/context.html
GORM与小清单
使用docker启动一个mysql client端去连接mysql server
docker run -it --network host --rm mysql:8.0.19 mysql --default-character-set=utf8mb4 -h127.0.0.1 -P13306 -uroot -p
后端
//1.获取请求参数
//2.执行业务逻辑
//3.响应结果
package main
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
"net/http"
"strconv"
)
var db *gorm.DB
// 生成表名,todos 复数形式
type Todo struct {
gorm.Model
Title string `json:"title" ` //代办事项 gorm转换 longtext
Status bool `json:"status" ` //完成状态,gorm转换后, tinyint , 0==false 1==true
}
func initDB() (err error) {
//选择链接驱动
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:yuchao666@tcp(yuchaoit.cn:3306)/sql_test?charset=utf8mb4&parseTime=True&loc=Local"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
return err
}
func main() {
//1.数据库初始化
if err := initDB(); err != nil {
log.Println("Connect mysql failed !!")
panic(err)
}
// 生成table
//db.AutoMigrate(&Todo{})
//2.gin路由
r := gin.Default()
//4.小清单,增删改查
//添加
g := r.Group("/api/v1")
//简单的括起来,美观
{
//例如都是统一前缀,/api/v1/todo
g.POST("/todo", createTodoHandler)
g.PUT("/todo", updateTodoHandler)
g.GET("/todo", getTodoHandler)
//拿到path传入的变量
g.DELETE("/todo/:id", deleteTodoHandler)
}
//3.启动server
r.Run(":17799")
}
// 其余是单个创建
/*
{
"title":"超哥学gorm",
"status":true
}
*/
func createTodoHandler(c *gin.Context) {
//1.获取请求参数 ,获得标题
var todo Todo
if err := c.ShouldBind(&todo); err != nil {
log.Printf("Invalid param %v\n", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "无效的参数!!", //生产下,不返回真实错误信息
})
return
}
//2.处理逻辑,写入数据库
log.Printf("用户输入清单:%v\n", todo)
if err := db.Create(&todo).Error; err != nil {
log.Printf("db.Create failed %v\n", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "服务器错误!!",
})
return
}
//3.返回响应
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "createTodoHandler Success~~",
})
}
// 更新清单
func updateTodoHandler(c *gin.Context) {
//1.获取请求参数
var todo Todo
if err := c.ShouldBind(&todo); err != nil {
fmt.Printf("获取请求参数失败 %v\n", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "无效的参数",
})
return
}
fmt.Println("用户传入参数:", todo)
//2.处理逻辑
//获取请求传过来要更新的id
//var obj Todo
//查询用户要更新的id是否存在
//务必注意,结构体指针问题
if err := db.First(&Todo{}, todo.ID).Error; err != nil {
fmt.Printf("用户传入 清单事项ID:%v\n", todo.ID)
//异常断言,递归err,判断是否是具体异常
if errors.Is(err, gorm.ErrRecordNotFound) {
//没有该记录
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "该id不存在",
})
return
}
//其他错误
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "其他错误",
})
return
}
//请求id存在,开始更新数据
//更新单个字段
//go语言支持 .换行
fmt.Println("开始更新。。", todo)
if err := db.Debug().Model(&todo).Update("status", todo.Status).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": err.Error,
})
return
}
log.Printf("本次更新清单是:%v\n", todo)
//3.返回响应
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "updateTodoHandler Success~~",
})
}
// 返回所有待办事项
func getTodoHandler(c *gin.Context) {
//1.获取请求参数,默认显示所有
//2.执行业务逻辑
var todos []Todo
if err := db.Find(&todos).Error; err != nil {
fmt.Println("查询清单失败:", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "查询数据失败",
})
return
}
//3.响应结果
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "Success",
"data": todos, //json自动反序列化 切片
})
}
func deleteTodoHandler(c *gin.Context) {
//1.获取请求参数,改造url,获取path参数, http://127.0.0.1:17799/api/v1/todo/1
idStr := c.Param("id")
id, err := strconv.Atoi(idStr) //转数字
if err != nil {
fmt.Println("无效的id", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "无效的参数",
})
return
}
//2.执行业务逻辑
//2.1 查一下传入的id,数据库有吗
if err := db.First(&Todo{}, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "数据库查询id失败",
})
return
}
fmt.Println("数据库查询id失败,", err)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": err,
})
return
}
//2.2删除数据
if err := db.Delete(&Todo{}, id).Error; err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": err,
})
return
}
//3.响应结果
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "删除success",
})
}
//基于不同http方法的restfulapi
//http://127.0.0.1:7799/api/v1/todo
Vue前端
```
# GORM更新细节
## 坑
```go
// updateDemo gorm更新示例
func updateDemo() {
// 全都改了,基于软删除字段的判断
db.Debug().
Model(&Book{}).
Updates(Book{Title: "hello", Amount: 18, Status: false})
// 1. 当你正好有一个结构体对象(包含数据库主键) -> 能够对应到数据库里的一条记录
// 接口幂等 -> 先查(是否存在这条记录;做状态判断)再更新
var id uint = 1
var b1 Book
//加上
err := db.Where("id = ?", id).First(&b1).Error // 先查一条记录(包含主键)
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("参数错误")
}
db.Debug().
Model(&b1).
Updates(Book{Title: "hello2", Amount: 28, Status: false})
// 2. 直接通过where条件来查找记录并更新
cond := &Book{
Model: gorm.Model{ // 匿名嵌入的结构体,字段名默认就是类型名
ID: id,
},
}
db.Debug().
Where(cond).
Updates(Book{Title: "hello3", Amount: 38, Status: false})
}
gorm更新的注意事项
不做主键where条件,导致更新所有数据。⚠️
db.Debug().
Model(&Book{}).
Updates(Book{Title: "hello", Amount: 18, Status: false})
相当于执行:
UPDATE `books` SET `updated_at`='2022-04-10 15:22:02.061',`title`='hello',`amount`=18 WHERE `books`.`deleted_at` IS NULL
正确写法,考虑参数不存在情况
var id uint = 1
var b1 Book
err := db.Where("id = ?", id).First(&b1).Error // 先查一条记录(包含主键)
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("参数错误")
}
// 如果数据库中当前状态已经是要变更的状态就直接返回成功,没有必要继续执行下去
// ...
db.Debug().
Model(&b1).
Updates(Book{Title: "hello2", Amount: 28, Status: false})
相当于执行:
UPDATE `books` SET `updated_at`='2022-04-10 15:27:51.133',`title`='hello2',`amount`=28 WHERE `books`.`deleted_at` IS NULL AND `id` = 1
给gorm内嵌的gorm.Model传入值
精简写法,传入结构体 >转为SQL的where根据id判断数据
db.Debug().
Where(&Book{Model: gorm.Model{ID: id}}).
Updates(Book{Title: "hello3", Amount: 38, Status: false})
相当于执行:
UPDATE `books` SET `updated_at`='2022-04-10 15:33:52.484',`title`='hello3',`amount`=38 WHERE `books`.`id` = 1 AND `books`.`deleted_at` IS NULL