Gin + Gorm:完整 CRUD API 与关系操作指南
前言
GORM 是 Go 语言中最流行、功能最强大的 ORM(对象关系映射)框架之一。它为开发者提供了简洁、直观的 API 来操作数据库,极大地简化了数据库交互的复杂性。以下是对 GORM 的全面深入介绍。
一、GORM 核心特性
https://gorm.io/zh_CN/
1. 全功能 ORM
GORM 提供了完整的 ORM 功能,包括:
CRUD 操作:创建、读取、更新、删除
关联关系:一对一、一对多、多对多
钩子函数:在操作前后执行自定义逻辑
预加载:避免 N+1 查询问题
事务处理:支持嵌套事务
数据库迁移:自动创建和更新表结构
2. 开发者友好
链式调用:流畅的 API 设计
约定优于配置:智能的默认行为
零侵入性:不强制继承特定结构体
丰富的文档:详细的官方文档和示例
二、集成GORM
1.安装依赖
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
2.定义模型
在 models/models.go 中定义我们的核心模型。我们将创建一个博客系统,包含用户(User)、文章(Post)和标签(Tag)。
package modelsimport ("time"
)type BaseModel struct {ID uint `gorm:"primaryKey" json:"id"`CreatedAt time.Time `json:"created_at"`UpdatedAt time.Time `json:"updated_at"`DeletedAt time.Time `gorm:"index" json:"deleted_at,omitempty"`
}// User 用户模型
type User struct {BaseModelUsername string `gorm:"unique;not null" json:"username"`Email string `gorm:"unique;not null" json:"email"`Password string `json:"-"` // JSON序列化时忽略// 一对一关系:用户拥有一个个人资料, 使用UserID作为外键Profile Profile `gorm:"foreignKey:UserID" json:"profile,omitempty"`// 一对多关系:用户可以发布多篇文章Posts []Post `gorm:"foreignKey:UserID" json:"posts,omitempty"`
}// Profile 个人资料模型 (与 User 一对一)
type Profile struct {BaseModelUserID uint `gorm:"unique;not null" json:"user_id"` // 外键,指向 UserBio string `json:"bio"` // 个人介绍Avatar string `json:"avatar"`
}// Post 文章模型
type Post struct {BaseModelTitle string `gorm:"not null" json:"title"`Content string `gorm:"type:text" json:"content"`UserID uint `json:"user_id"` // 外键,指向 User// 一对多关系:一篇文章属于一个作者User User `gorm:"foreignKey:UserID" json:"user,omitempty"`// 多对多关系:一篇文章可以有多个标签Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty"`
}// Tag 标签模型
type Tag struct {BaseModelID uint `gorm:"primaryKey" json:"id"`Name string `gorm:"unique;not null" json:"name"`// 多对多关系:一个标签可以被多篇文章使用Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`
}
3. 初始化数据库连接
在 initialize/db.go 中初始化 GORM 与数据库的连接。
func InitDB() {dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",global.Config.Database.Username,global.Config.Database.Password,global.Config.Database.Host,global.Config.Database.Port,global.Config.Database.DBName,)var logLevel logger.LogLevelif global.Config.App.DebugMode {// 开发环境显示所有的sqllogLevel = logger.Info} else {// 只打印错误的sqllogLevel = logger.Error}db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{// 打印 SQL 语句, 开发环境显示所有的sql, 生产环境只打印错误的sqlLogger: logger.Default.LogMode(logLevel),})if err != nil {global.Logger.Fatal("数据库连接失败", zap.Error(err))}global.DB = dbglobal.Logger.Info("数据库连接成功", zap.String("dsn", dsn))
}
4. 使用命令行标志控制迁移
var (// migrateFlag 表示是否执行数据库迁移migrateFlag = flag.Bool("migrate", false, "Run database migrations")
)func main() {// 解析命令行标志flag.Parse()// 连接数据库等初始化 ...// 根据 -migrate 标志决定是否执行迁移if *migrateFlag {log.Println("Starting database migrations...")// 自动迁移模式err := global.DB.AutoMigrate(&models.User{}, &models.Profile{}, &models.Post{}, &models.Tag{})if err != nil {panic("数据库迁移失败: " + err.Error())}log.Println("Migrations completed successfully.")// 迁移完成后直接退出,不启动 HTTP 服务return}// 如果没有 -migrate 标志,则正常启动 HTTP 服务initialize.Routers() // ...
}
手动迁移命令
go run main.go -migrate
5. Scope 作用域复用通用的分页逻辑
https://gorm.io/zh_CN/docs/scopes.html
// /api/common.go
func Paginate(r *http.Request) func(db *gorm.DB) *gorm.DB {return func (db *gorm.DB) *gorm.DB {q := r.URL.Query()page, _ := strconv.Atoi(q.Get("page"))if page <= 0 {page = 1}pageSize, _ := strconv.Atoi(q.Get("page_size"))switch {case pageSize > 100:pageSize = 100case pageSize <= 0:pageSize = 10}offset := (page - 1) * pageSizereturn db.Offset(offset).Limit(pageSize)}
}
6. 实现 CRUD API (以 Post 为例)
// api/post.go
package apiimport ("net/http""strconv""gin/models""github.com/gin-gonic/gin""gorm.io/gorm"
)type PostListResponse struct {List []models.Post `json:"list"`Total int64 `json:"total"`
}// CreatePost 创建文章
func CreatePost(c *gin.Context) {var post models.Postif err := c.ShouldBindJSON(&post); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 假设用户ID从JWT token中获取,这里简化为硬编码post.AuthorID = 1 // 示例用户ID// 处理多对多关系:关联标签// 前端传来的标签ID列表var tagIDs []uintif err := c.ShouldBindJSON(&struct{ TagIDs []uint `json:"tag_ids"` }{TagIDs: &tagIDs}); err == nil {var tags []models.Tag// 预加载标签,避免N+1查询if err := models.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load tags"})return}post.Tags = tags}if err := models.DB.Create(&post).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusCreated, post)
}// GetPosts 分页查询文章列表 (包含作者和标签)
func GetPosts(c *gin.Context) {var posts []models.Postvar total int64// 使用预加载 (Preload) 加载关联数据query := models.DB.Model(&models.Post{}).Preload("User").Preload("Tags")// 计算总数if err := query.Count(&total).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}// 分页查询if err := query.Scopes(Paginate(c.Request)).Find(&posts).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusOK, PostListResponse {List: posts,Total: total})
}// GetPostByID 查询单篇文章 (包含完整关系)
func GetPostByID(c *gin.Context) {id := c.Param("id")var post models.Post// 使用 Preload 加载 Author 和 Tagsif err := models.DB.Preload("User").Preload("Tags").First(&post, id).Error; err != nil {if err == gorm.ErrRecordNotFound {c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})return}c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusOK, post)
}// UpdatePost 更新文章
func UpdatePost(c *gin.Context) {id := c.Param("id")var post models.Postif err := models.DB.First(&post, id).Error; err != nil {if err == gorm.ErrRecordNotFound {c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})return}c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}// 绑定更新的数据if err := c.ShouldBindJSON(&post); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 处理标签更新 (使用Association)var tagIDs []uintif err := c.ShouldBindJSON(&struct{ TagIDs []uint `json:"tag_ids"` }{TagIDs: &tagIDs}); err == nil {var tags []models.Tagif err := models.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load tags"})return}// 使用 Association 管理多对多关系models.DB.Model(&post).Association("Tags").Replace(tags)}if err := models.DB.Save(&post).Error; err != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return}c.JSON(http.StatusOK, post)
}// DeletePost 删除文章
func DeletePost(c *gin.Context) {id := c.Param("id")result := models.DB.Delete(&models.Post{}, id)if result.Error != nil {c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})return}if result.RowsAffected == 0 {c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})return}c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
}
7. 注册路由
// router/post.go
func InitPostRouter(Router *gin.RouterGroup) {postRouter := Router.Group("post"){postRouter.POST("create", api.CreatePost)postRouter.GET("list", api.GetPosts)postRouter.GET(":id", api.GetPostById)postRouter.PUT(":id", api.UpdatePost)postRouter.DELETE("id", api.DeletePost)}
}
8. 测试 API
1.创建用户
curl -X POST http://localhost:8080/user/register \-H "Content-Type: application/json" \-d '{"name":"Smile","email":"smile@qq.com","password":"password"}'
2.创建标签
curl -X POST http://localhost:8080/tags \-H "Content-Type: application/json" \-d '{"name":"Golang"}'
3.创建文章(包含多对多标签)
curl -X POST http://localhost:8080/post/crete \-H "Content-Type: application/json" \-d '{"title": "My First Post","content": "This is the content of my first post","user_id": 1,"tags": [1] }'
5.用户列表
curl http://localhost:8080/user/list
5.文章列表
curl http://localhost:8080/post/list
三、总结
一对一 (User ↔ Profile)
- 使用 Profile Profile 字段和 foreignKey:UserID 标签
- 预加载时使用
Preload("Profile")
一对多 (User → Posts)
- 使用 Posts []Post 字段和 foreignKey:UserID 标签
- 预加载时使用
Preload("Posts")
多对多 (User ↔ Tags, Post ↔ Tags)
- 使用 many2many:user_tags 和 many2many:post_tags 标签
- GORM 会自动创建中间表
- 预加载时使用
Preload("Tags")
- 使用
Association
方法(如Append
,Replace
,Delete
)来管理关联关系
分页
Paginate 函数利用 GORM 的 Scopes
功能,将分页逻辑封装成可复用的代码。
错误处理
始终检查 gorm.DB 操作的 Error 字段,并区分 RecordNotFound 和其他数据库错误。
安全性(待补充)
生产环境中,Password 字段应加密存储(使用 bcrypt 等),并通过 JWT 等机制验证用户身份。
代码示例
gitee