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 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,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"`
}

image-20230208104805033

安装

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 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成

例如:

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 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

蛇形是指,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 约定使用 CreatedAtUpdatedAt 追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间

要使用不同名称的字段,您可以配置 autoCreateTimeautoUpdateTime 标签

如果您想要保存 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 nullsize, 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 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。

当查询数据库时它添加了 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 Find without a limit for single object db.Find(&user) will query the full table and return only the first object which is not performant and nondeterministic

First 和 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

添加用户表

Copyright © www.yuchaoit.cn 2025 all right reserved,powered by Gitbook作者:于超 2023-02-12 18:00:49

results matching ""

    No results matching ""