问题分析:


函数未导出
在Go语言中,只有首字母大写的标识符(如函数、变量等)才在包外可见。如果希望home函数可以通过service.home调用,需要将其定义为导出的函数:

func Home(c *gin.Context) {
    c.JSON(200, gin.H{"message": "hello world"})
}

这样,主程序可以通过service.Home访问它。小写函数home是私有的,主程序无法访问。

1. Gin中间件的定义

在Gin框架中,中间件(Middleware)是一个函数,它具有以下特点:

func(c *gin.Context) {
    // 中间件逻辑
}

更准确地说,Gin中间件是一个符合gin.HandlerFunc类型的函数,其签名如下:

type HandlerFunc func(*Context)

2. 中间件的核心特征

  1. 访问请求和响应:通过gin.Context参数可以访问请求和操作响应
  2. 处理流程控制

    • 调用c.Next()继续执行后续中间件或处理函数
    • 调用c.Abort()终止后续中间件执行
  3. 执行时机

    • 可以在请求处理前执行(pre-processing)
    • 可以在请求处理后执行(post-processing)

3. 中间件的完整生命周期

一个典型的中间件执行流程:

func LoggerMiddleware(c *gin.Context) {
    // 1. 请求处理前的逻辑
    start := time.Now()

    // 2. 调用下一个中间件或路由处理函数
    c.Next()

    // 3. 请求处理后的逻辑
    latency := time.Since(start)
    log.Printf("请求处理耗时: %v", latency)
}

4. 中间件的分类

(1) 全局中间件

router := gin.Default()
router.Use(LoggerMiddleware) // 全局生效

(2) 路由组中间件

authGroup := router.Group("/admin")
authGroup.Use(AuthMiddleware) // 只对该路由组生效

(3) 单个路由中间件

router.GET("/secure", AuthMiddleware, func(c *gin.Context) {
    // 处理逻辑
})

5. 中间件的常见用途

  1. 日志记录:记录请求信息、处理时间等
  2. 认证鉴权:JWT验证、Session检查等
  3. 错误处理:统一错误捕获和处理
  4. 限流:控制请求频率
  5. 数据预处理:解析通用参数
  6. 响应格式化:统一响应格式
  7. CORS处理:跨域资源共享配置

6. 内置中间件示例

Gin自带了一些常用中间件:

// 1. 恢复中间件(Recovery)
router := gin.Default() // 默认包含Logger和Recovery中间件

// 2. 静态文件服务
router.Static("/assets", "./assets")

// 3. 自定义中间件
router.Use(func(c *gin.Context) {
    c.Set("requestId", uuid.New().String())
    c.Next()
})

7. 自定义中间件最佳实践

一个完整的自定义中间件示例:

func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 生成唯一请求ID
        requestID := uuid.New().String()

        // 设置到上下文和响应头
        c.Set("Request-ID", requestID)
        c.Writer.Header().Set("X-Request-ID", requestID)

        // 继续处理
        c.Next()

        // 可以在这里添加请求后的处理逻辑
    }
}

// 使用方式
router.Use(RequestIDMiddleware())

8. 中间件链的执行顺序

中间件的执行顺序遵循"洋葱模型":

请求 -> 中间件1前 -> 中间件2前 -> 处理函数 -> 中间件2后 -> 中间件1后 -> 响应

9. 注意事项

  1. 性能影响:中间件会增加处理时间,应避免复杂耗时的操作
  2. 错误处理:中间件中应妥善处理错误,避免panic
  3. 资源释放:如果有资源分配,确保在适当时候释放
  4. 避免阻塞:不要长时间阻塞在中间件中

10. 高级用法:中止中间件链

func AuthMiddleware(c *gin.Context) {
    if !checkAuth(c) {
        // 中止后续中间件执行
        c.Abort()
        c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
        return
    }
    c.Next()
}

总结来说,Gin中间件是一种强大的机制,允许开发者在请求处理流程的不同阶段插入自定义逻辑,实现横切关注点(Cross-cutting concerns)的分离和复用。

安装

go install github.com/gin-gonic/gin@latest

// 或 go get -u github.com/gin-gonic/gin

引入

import "github.com/gin-gonic/gin"

开始

生成实例:

// 使用默认中间件(logger 和 recovery 中间件)创建 gin 路由
r := gin.Default()

运行

r.Run()
// 默认在 8080 端口启动服务,除非定义了一个 PORT 的环境变量。
// r.Run(":8888") 传入参数切换端口 

路由

r.GET("/someGet", getting)
r.POST("/somePost", posting)
r.PUT("/somePut", putting)
r.DELETE("/someDelete", deleting)
r.PATCH("/somePatch", patching)
r.HEAD("/someHead", head)
r.OPTIONS("/someOptions", options)

注:

对于每一种路由都是传入两个参数,一个是路由的路径,第二个是处理请求的函数

对于请求函数例如:

func(c *gin.Context) {

    // 其他处理代码
    ...
    // 其他处理代码
    
    // 响应
    c.String(http.StatusOK, "hello world")
}

需要传入一个参数 c 为gin的上下文类型

gin中存在多种不同的响应类型

  1. 返回JSON数据
func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回HTML模板
func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "title": "Main website",
    })
}

// gin.H{} 传递给模板,模板中使用{{.title}}使用,其他的更多用法比如循环,分支,之类可以查找go官方文档
// https://pkg.go.dev/text/template
// https://pkg.go.dev/html/template

需要先设置模板目录:

router := gin.Default()
router.LoadHTMLGlob("templates/*")
  1. 返回XML数据
func(c *gin.Context) {
    c.XML(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回YAML数据
func(c *gin.Context) {
    c.YAML(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回ProtoBuf数据
func(c *gin.Context) {
    data := &protoexample.Test{
        Label: proto.String("hello"),
        Reps:  []int64{1, 2, 3},
    }
    c.ProtoBuf(http.StatusOK, data)
}
  1. 返回文件
func(c *gin.Context) {
    c.File("/path/to/file")
}
  1. 返回文件流
func(c *gin.Context) {
    c.FileFromFS("/path/to/file", http.Dir("/var/www"))
}
  1. 返回数据流
func(c *gin.Context) {
    reader := strings.NewReader("some data")
    c.DataFromReader(http.StatusOK, int64(reader.Len()), "text/plain", reader, nil)
}
  1. 重定向
func(c *gin.Context) {
    c.Redirect(http.StatusMovedPermanently, "<https://www.google.com/>")
}

func(c *gin.Context) {
    c.Request.URL.Path = "/"
    r.HandleContext(c)
}
  1. 返回纯文本
func(c *gin.Context) {
    c.String(http.StatusOK, "hello world")
}

关于Gin框架中的gin.H

gin.H是Gin框架中一个非常实用的快捷方式,它实际上是一个类型别名(type alias),定义如下:

type H map[string]interface{}

详细解释

  1. 本质gin.H就是一个map[string]interface{}的别名

    • string是键的类型
    • interface{}是值的类型,表示可以接受任何类型的值
  2. 用途

    • 主要用于构建JSON/XML/YAML等格式的响应数据
    • 提供了一种简洁的方式来创建动态数据结构
  3. 示例
// 创建一个简单的JSON响应
data := gin.H{
    "name": "John",
    "age":  30,
    "tags": []string{"golang", "gin"},
}
c.JSON(http.StatusOK, data)
  1. 优点

    • 比直接使用map[string]interface{}更简洁
    • 提高了代码可读性
    • 是Gin框架中的惯用写法
  2. 与其他方式的对比
// 使用gin.H
gin.H{"key": "value"}

// 等同于
map[string]interface{}{"key": "value"}

// 也等同于
struct {
    Key string `json:"key"`
}{Key: "value"}
  1. 嵌套使用
data := gin.H{
    "user": gin.H{
        "name": "Alice",
        "age":  25,
    },
    "status": "active",
}
  1. 注意事项

    • 虽然方便,但对于复杂数据结构,有时使用自定义结构体可能更合适
    • 在性能敏感的场景,直接使用结构体可能比gin.H更高效

gin.H是Gin框架中一个简单但强大的工具,它简化了动态数据的构建过程,特别是在构建API响应时非常有用。

在Gin中使用自定义结构体的示例

当然可以。在Gin框架中,使用自定义结构体作为响应通常比gin.H更规范,特别是在处理复杂数据结构时。以下是几个示例:

  1. 基本结构体示例
// 定义自定义结构体
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

func getUser(c *gin.Context) {
    user := User{
        ID:       1,
        Username: "john_doe",
        Email:    "[email protected]",
    }
    c.JSON(http.StatusOK, user)
}
  1. 嵌套结构体示例
type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

type UserProfile struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
}

func getProfile(c *gin.Context) {
    profile := UserProfile{
        Name: "Alice",
        Age:  28,
        Address: Address{
            City:    "Shanghai",
            Country: "China",
        },
    }
    c.JSON(http.StatusOK, profile)
}
  1. 带切片的结构体
type Post struct {
    ID      int      `json:"id"`
    Title   string   `json:"title"`
    Tags    []string `json:"tags"`
}

func getPosts(c *gin.Context) {
    posts := []Post{
        {
            ID:    1,
            Title: "Gin框架入门",
            Tags:  []string{"golang", "web"},
        },
        {
            ID:    2,
            Title: "Go语言并发编程",
            Tags:  []string{"golang", "concurrency"},
        },
    }
    c.JSON(http.StatusOK, posts)
}
  1. 作为请求体和响应体
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type LoginResponse struct {
    Token string `json:"token"`
    Exp   int64  `json:"exp"`
}

func login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 验证逻辑...

    resp := LoginResponse{
        Token: "generated_token_here",
        Exp:   time.Now().Add(24 * time.Hour).Unix(),
    }
    c.JSON(http.StatusOK, resp)
}
  1. 带omitempty标签的示例
type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Price       float64 `json:"price"`
    Description string  `json:"description,omitempty"` // 为空时不显示
    Stock       int     `json:"-"`                    // 永远不序列化
}

func getProduct(c *gin.Context) {
    product := Product{
        ID:    101,
        Name:  "Laptop",
        Price: 999.99,
        // Description和Stock未赋值
    }
    c.JSON(http.StatusOK, product)
    // 输出: {"id":101,"name":"Laptop","price":999.99}
}

Gin框架中的参数传递方式

一、路由传入的参数类型

  1. 路径参数 (Path Parameters)
router.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // 获取路径参数
})
  1. 查询参数 (Query Parameters)
// 请求: /search?q=gin&page=1
q := c.Query("q")         // "gin"
page := c.DefaultQuery("page", "1") // 带默认值
  1. 表单参数 (Form Data)
// Content-Type: application/x-www-form-urlencoded
name := c.PostForm("name")
age := c.DefaultPostForm("age", "18") // 带默认值
  1. 多部分表单 (Multipart/Form-Data)

用于文件上传:

file, _ := c.FormFile("file")

// 处理文件上传
if file, err := c.FormFile("avatar"); err == nil {
    // 保存文件逻辑
    user.Avatar = "/uploads/" + file.Filename
}
  1. Header 参数
token := c.GetHeader("Authorization")
  1. Cookie 参数
value, err := c.Cookie("cookie_name")
  1. 原始Body (Raw Body)
body, _ := ioutil.ReadAll(c.Request.Body)

二、POST请求可传递的参数类型

POST请求可以通过以下Content-Type传递不同类型的参数:

  1. application/x-www-form-urlencoded
  • 传统的表单提交格式
  • 通过c.PostForm()获取
  • 示例:
// 客户端发送: name=John&age=30
name := c.PostForm("name")
  1. multipart/form-data
  • 用于文件上传和表单混合数据
  • 通过c.FormFile()获取文件
  • 通过c.PostForm()获取普通字段
  • 示例:
file, _ := c.FormFile("avatar")
name := c.PostForm("username")
  1. application/json
  • 最常用的API数据格式
  • 通过c.ShouldBindJSON()绑定到结构体
  • 示例:
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

var user User
if err := c.ShouldBindJSON(&user); err != nil {
    // 错误处理
}

//例如:
// 绑定JSON或Form数据
//var user User
//if err := c.ShouldBind(&user); err != nil {
//    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
//    return
//}
  1. text/xml / application/xml
  • XML格式数据
  • 通过c.ShouldBindXML()绑定
  • 示例:
if err := c.ShouldBindXML(&obj); err != nil {
    // 错误处理
}
  1. application/protobuf
  • Protocol Buffers格式
  • 高性能二进制格式
  • 示例:
var message pb.Message
if err := proto.Unmarshal(c.Request.Body, &message); err != nil {
    // 错误处理
}
  1. 其他自定义格式

可以通过c.GetRawData()获取原始数据后自行解析:

data, _ := c.GetRawData()
// 自定义解析逻辑

三、参数绑定最佳实践

  1. 对于简单参数:直接使用Query(), PostForm()等方法
  2. 对于复杂结构:使用ShouldBind系列方法绑定到结构体
  3. 文件上传:使用FormFile()
  4. API开发:优先使用JSON格式
  5. 性能敏感场景:考虑使用protobuf

四、完整示例

type User struct {
    ID       int    `json:"id" form:"id"`
    Username string `json:"username" form:"username"`
    Avatar   string `json:"avatar"` // 文件上传单独处理
}

func updateUser(c *gin.Context) {
    // 路径参数
    id := c.Param("id")

    // 绑定JSON或Form数据
    var user User
    if err := c.ShouldBind(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 处理文件上传
    if file, err := c.FormFile("avatar"); err == nil {
        // 保存文件逻辑
        user.Avatar = "/uploads/" + file.Filename
    }

    // 处理其他逻辑...
}

分组路由(Grouping Routes)

如果有一组路由,前缀都是/api/v1开头,是否每个路由都需要加上/api/v1这个前缀呢?答案是不需要,分组路由可以解决这个问题。利用分组路由还可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制。

// group routes 分组路由
defaultHandler := func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "path": c.FullPath(),
    })
}
// group: v1
v1 := r.Group("/v1")
{
    v1.GET("/posts", defaultHandler)
    v1.GET("/series", defaultHandler)
}
// group: v2
v2 := r.Group("/v2")
{
    v2.GET("/posts", defaultHandler)
    v2.GET("/series", defaultHandler)
}

中间件使用

示例:

// 作用于全局
r.Use(gin.Logger())
r.Use(gin.Recovery())

// 作用于单个路由
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// 作用于某个组
authorized := r.Group("/")
authorized.Use(AuthRequired())
{
    authorized.POST("/login", loginEndpoint)
    authorized.POST("/submit", submitEndpoint)
}

参考文档:

https://gin-gonic.com/zh-cn/docs/

https://geektutu.com/post/quick-go-gin.html

gorm golang对象关系映射框架

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

连接数据库

sqlite

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

mysql

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

其他数据库或高级用法:https://gorm.io/zh_CN/docs/connecting_to_the_database.html

声明模型

例:

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

GORM 通过将 Go 结构体(Go structs) 映射到数据库表来简化数据库交互。

模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的ScannerValuer接口)。

约定

  1. 主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。
  2. 表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。 例如,一个 User 结构体在数据库中的表名变为 users 。
  3. 列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
  4. 时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。

gorm.Model

GORM提供了一个预定义的结构体,名为gorm.Model,其中包含常用字段:

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

  • 将其嵌入在您的结构体中: 您可以直接在您的结构体中嵌入 gorm.Model ,以便自动包含这些字段。 这对于在不同模型之间保持一致性并利用GORM内置的约定非常有用,请参考嵌入结构
  • 包含的字段

    • ID :每个记录的唯一标识符(主键)。
    • CreatedAt :在创建记录时自动设置为当前时间。
    • UpdatedAt:每当记录更新时,自动更新为当前时间。
    • DeletedAt:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。

文档:https://gorm.io/zh_CN/docs/models.html

迁移(Migration)

迁移(Migration)是什么?

迁移是指对数据库结构进行版本控制的机制,它允许您:

  1. 以编程方式定义数据库表结构
  2. 跟踪数据库模式的变化历史
  3. 在不同环境(开发/测试/生产)之间保持数据库结构一致
  4. 回滚到之前的数据库版本

迁移与创建数据库的区别

特性迁移(Migration)创建数据库
范围表结构、字段、索引等整个数据库实例
功能修改现有结构、版本控制创建新的数据库容器
粒度细粒度(表/字段级)粗粒度(数据库级)
目的模式演进数据库初始化

GORM中的迁移实现

在GORM中,迁移通常通过AutoMigrate方法实现:

// 自动迁移模型结构到数据库表
db.AutoMigrate(&User{}, &Product{}, &Order{})

// 这会在数据库中创建对应的表(如果不存在)或修改表结构以匹配模型

迁移的典型用途

  1. 初始化表结构:创建新表
  2. 添加字段:向现有表添加新列
  3. 修改字段:更改字段类型或约束
  4. 删除字段:从表中移除列
  5. 创建索引:添加或删除索引

高级迁移功能

GORM还支持更复杂的迁移操作:

// 使用Migrator接口进行精细控制
m := db.Migrator()

// 检查表是否存在
m.HasTable(&User{})

// 创建表
m.CreateTable(&User{})

// 添加列
m.AddColumn(&User{}, "CreditCardNumber")

// 修改列
m.AlterColumn(&User{}, "Name")

// 删除列
m.DropColumn(&User{}, "Age")

基本操作(CRUD)

 // 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)

注:

Product是一个结构体

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

对于CRUD的每一个操作都是传入一个地址,直接修改作用的对象

create

例如对于create来说,就是传入一个Product类型的结构体。传入的结构体实例不需要为每个字段都赋值。

基本赋值规则:

  1. 部分赋值:可以只为结构体的部分字段赋值,其他字段将保持零值或默认值
  2. 零值处理:未赋值的字段会根据类型自动设置为相应的零值:

    • 数字类型:0
    • 字符串:""
    • 布尔:false
    • 指针:nil
    • 时间:0001-01-01 00:00:00 +0000 UTC

示例代码

type User struct {
    ID        uint
    Name      string
    Age       int
    Email     *string  // 指针类型
    IsActive  bool
    CreatedAt time.Time
}

// 只设置部分字段
user := User{
    Name: "张三",
    Age:  25,
    // 其他字段未设置
}

result := db.Create(&user)  // 成功插入,未设置字段使用零值

特殊情况处理

  1. 指针字段

    • 如果字段是指针类型且未赋值,GORM会将其作为NULL插入
    • 如果想明确设置NULL,可以使用指针的nil值
    // 明确设置Email为NULL
    user := User{
        Name:  "李四",
        Email: nil,  // 将存储为NULL
    }
  2. 默认值

    • 可以在模型定义中使用gorm标签设置默认值
    • 未赋值时会使用默认值而非零值
    type Product struct {
        Code  string
        Price float64 `gorm:"default:100"`  // 默认值100
    }
  3. 忽略字段

    • 使用gorm:"-"标签可以完全忽略字段,不会被插入
    type User struct {
        Name string
        Temp string `gorm:"-"`  // 不会存入数据库
    }

数据库约束的影响

  1. NOT NULL约束

    • 如果字段在数据库中有NOT NULL约束且没有默认值
    • 尝试插入时会返回错误
  2. 主键

    • 如果使用自动递增主键,可以省略ID字段
    • 如果想手动设置ID,可以显式赋值

最佳实践

  1. 只设置业务需要的字段,让其他字段保持零值
  2. 对于可选字段,考虑使用指针类型以便明确表示NULL
  3. 重要的必填字段应该在模型层或数据库层添加验证
  4. 对于有默认值的字段,可以省略赋值

read

对于read,可以现在外面定义一个新的对应类型的变量,然后将这个变量传入,因为传的是地址,所以执行完,read之后对于product就是我们需要的一个变量,对应着数据库表中特定的某一行。

update

不可以直接使用未经过Read赋值的product变量进行Update操作。这样会导致更新失败或产生意外行为。

Update操作的工作机制

GORM的Update操作需要两个关键信息:

  1. 要更新的模型实例:用于确定更新哪条记录
  2. 更新内容:指定要修改的字段和值

为什么需要先Read

  1. 主键标识

    • Update操作需要知道要更新哪条记录,这通常通过模型的主键(ID)来确定
    • 未经Read的product变量没有有效的ID值
  2. 模型状态

    • GORM会跟踪模型的状态(是否从数据库加载)
    • 未加载的模型无法正确执行更新

具体问题分析

var product Product  // 未赋值的product变量
db.Model(&product).Update("Price", 200)

这种情况下:

  1. product的ID字段为0(零值)
  2. GORM会尝试更新ID=0的记录,这通常不是您想要的
  3. 如果没有ID=0的记录,则不会更新任何数据

正确做法

  1. 先查询再更新 (推荐):

    var product Product
    db.First(&product, 1) // 或通过其他条件查询
    db.Model(&product).Update("Price", 200)
  2. 直接指定条件更新 (不依赖模型实例):

    // 更新所有code="D42"的记录
    db.Model(&Product{}).Where("code = ?", "D42").Update("Price", 200)
    
    // 更新主键为1的记录
    db.Model(&Product{}).Where("id = ?", 1).Update("Price", 200)

特殊情况

如果明确知道要更新的记录ID,也可以这样:

product := Product{ID: 1} // 只设置ID
db.Model(&product).Update("Price", 200)

但这种方法:

  1. 不如先查询再更新直观
  2. 无法利用GORM的模型状态跟踪功能
  3. 可能引发并发问题

最佳实践

  1. 对于单个记录更新,总是先查询获取完整模型
  2. 对于批量更新,使用Where条件明确指定范围
  3. 避免使用零值模型进行更新操作
  4. 重要更新操作前检查受影响的行数

错误示例

// 错误用法1: 完全未初始化的模型
var p Product
db.Model(&p).Update("Price", 200) // 不会按预期工作

// 错误用法2: 只有部分字段的模型
p := Product{Code: "D42"}
db.Model(&p).Update("Price", 200) // 仍然不知道更新哪条记录

delete

在GORM中执行Delete操作时,如果已经有一个完整赋值的product实例,通常不需要再额外传入primary key。以下是详细说明:

基本删除方式

  1. 使用已赋值的模型实例删除(推荐)
var product Product
db.First(&product, 1) // 先查询获取完整记录

// 方式1:直接使用实例删除
db.Delete(&product) // 不需要再传ID

这种方式:

  • GORM会自动使用模型中的主键值作为删除条件
  • 是最简洁、最安全的删除方式
  • 推荐在日常开发中使用
  1. 同时传入实例和主键(冗余方式)
// 方式2:虽然可以运行,但冗余
db.Delete(&product, 1) // 传入实例同时再传ID

这种方式:

  • 可以运行但没必要
  • 如果传入的ID与实例中的ID不一致,可能导致意外行为
  • 不推荐使用

特殊情况处理

批量删除

如果需要批量删除,可以不使用实例:

// 删除所有price>100的记录
db.Where("price > ?", 100).Delete(&Product{})

// 删除主键为1的记录(不依赖实例)
db.Delete(&Product{}, 1)

软删除考虑

如果模型启用了软删除(有DeletedAt字段):

// 软删除会自动设置DeletedAt时间
db.Delete(&product)

// 要真正硬删除可以加Unscoped
db.Unscoped().Delete(&product)

为什么不需要重复传主键?

  1. GORM的智能处理

    • 当传入模型实例时,GORM会自动提取其主键值
    • 内部会生成类似WHERE id = ?的条件
  2. 数据一致性

    • 避免主键值不一致的风险
    • 减少人为错误的可能性
  3. 代码简洁性

    • 减少冗余参数
    • 提高代码可读性

最佳实践建议

  1. 查询后删除

    var product Product
    if err := db.First(&product, 1).Error; err != nil {
        // 处理错误
    }
    db.Delete(&product)
  2. 批量删除

    // 明确条件
    db.Where("code LIKE ?", "D%").Delete(&Product{})
  3. 避免的做法

    // 不推荐 - 可能误删
    db.Delete(&Product{}, someVariable)
    
    // 危险 - 可能清空整个表
    db.Delete(&Product{})

关于Unscoped()

基本概念

Unscoped()是GORM提供的一个链式调用方法,用于绕过GORM的默认作用域限制,特别是针对软删除(Soft Delete)功能。

核心作用

  1. 绕过软删除机制

    • 默认情况下,GORM会对有DeletedAt字段的模型启用软删除
    • 使用Unscoped()可以操作已被软删除的记录
  2. 执行"硬删除"

    • 真正从数据库中物理删除记录
    • 而不是仅仅设置deleted_at时间戳
  3. 查询所有记录

    • 包括已被软删除的记录

使用场景

  1. 真正删除记录(硬删除)
// 普通删除(如果是软删除模型,只是设置deleted_at)
db.Delete(&user)

// 硬删除(真正从数据库删除)
db.Unscoped().Delete(&user)
  1. 查询包含已删除的记录
// 默认查询会排除已软删除的记录
db.Find(&users)

// 查询所有记录(包括已软删除的)
db.Unscoped().Find(&users)
  1. 恢复软删除的记录
// 将deleted_at设置为NULL来恢复记录
db.Unscoped().Model(&user).Update("DeletedAt", nil)

实现原理

GORM的软删除是通过在SQL查询中自动添加条件实现的:

  • 默认查询会自动添加:WHERE deleted_at IS NULL
  • 使用Unscoped()后会移除这个条件

注意事项

  1. 数据安全

    • 硬删除是不可逆的操作
    • 生产环境慎用,确保有备份机制
  2. 关联删除

    • 如果模型有关联关系,硬删除可能导致外键约束问题
    • 需要考虑级联删除或手动处理关联记录
  3. 审计要求

    • 某些业务场景要求保留所有历史记录
    • 硬删除可能违反合规要求

最佳实践

// 1. 硬删除前先确认
if isAdmin {
    db.Unscoped().Delete(&sensitiveData)
}

// 2. 批量硬删除时添加条件限制
db.Unscoped().Where("created_at < ?", time.Now().AddDate(-5,0,0)).Delete(&OldRecords{})

// 3. 开发环境可以设置全局禁用软删除
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&TestData{})

与其他方法的对比

方法作用影响
Delete()普通删除对软删除模型是逻辑删除
Unscoped().Delete()硬删除物理删除记录
Where().Delete()条件删除遵循模型作用域
Unscoped().Where().Delete()条件硬删除无视软删除限制

总结:Unscoped()是GORM中处理软删除边界的强大工具,但需要谨慎使用,特别是在生产环境中。理解它的工作原理有助于您更好地控制数据删除行为。

参考文档:https://gorm.io/zh_CN/docs/

ORM 是 Object-Relational Mapping(对象关系映射) 的缩写,它是一种编程技术,用于在面向对象编程语言中将数据库表与程序中的对象自动映射,从而避免直接编写繁琐的 SQL 语句。

核心概念

  1. 对象与表的映射

    • 数据库中的一张表 ↔ 程序中的一个类(如 User 类对应 users 表)。
    • 表中的一行数据 ↔ 类的一个实例对象(如 User(name="Alice") 对应表中一行记录)。
    • 表的字段 ↔ 类的属性(如 user.name 对应 users.name 列)。
  2. 操作抽象化

    • 通过操作对象(如 user.save())自动生成并执行底层 SQL(如 INSERT INTO users...),开发者无需手动拼接 SQL。

常见 ORM 框架示例

语言ORM 框架
PythonDjango ORM, SQLAlchemy
JavaHibernate, MyBatis
JavaScriptSequelize, TypeORM
PHPEloquent (Laravel)

优缺点

优点

  • 提高开发效率:减少重复的 SQL 编写。
  • 跨数据库兼容:切换数据库时只需修改配置(如从 MySQL 到 PostgreSQL)。
  • 安全性:内置防 SQL 注入机制(如参数化查询)。

缺点

  • 性能损耗:复杂查询可能不如手写 SQL 高效。
  • 学习成本:需掌握 ORM 的特定语法和约定。

简单示例(Python + SQLAlchemy)

from sqlalchemy import create_engine, Column, String
from sqlalchemy.ext.declarative import declarative_base

# 1. 定义表结构(通过类)
Base = declarative_base()
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

# 2. 创建数据库连接
engine = create_engine('sqlite:///mydb.db')
Base.metadata.create_all(engine)

# 3. 操作数据库(无需写 SQL)
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()

# 插入数据(Create)
new_user = User(name="Alice")
session.add(new_user)
session.commit()

# 查询数据(Read)
users = session.query(User).filter_by(name="Alice").all()

何时使用 ORM?

  • 适合大多数业务逻辑简单的 CRUD 操作。
  • 复杂查询或高性能场景可结合原生 SQL 使用(如 ORM 提供 raw SQL 支持)。