Skip to content

实战笔记:从内存 Map 到 GORM 持久化

日期: 2026-01-23
标签: Go, GORM, Architecture, SQL
分类: 实战复盘

记录我在将后端从内存存储升级为 SQLite 的全过程。包含对 GORM 核心机制(Save 更新原理、db.Model 定位逻辑、删除机制)的深度伪代码分析。


引言

在完成了三层架构的搭建后,我的后端服务依然面临一个致命问题:重启即失忆

为了让数据持久化,我决定引入 GORM。作为一个 SQL 基础薄弱且习惯了 C++ 思维的开发者,这个过程并非一帆风顺。本文记录了从环境选择到底层原理拷问的完整心路历程。

第一阶段:战术选择与环境搭建

1. SQLite vs MySQL?

在 Week 2 阶段,我选择了 SQLite

理由

  • 它不需要安装任何服务,只是一个 .db 文件
  • 对于验证架构和 GORM 逻辑来说,它和 MySQL 99% 通用

战略:得益于我在架构中使用了 Repository 接口,未来切换到 MySQL 只需修改 main.go 里的连接代码,业务逻辑一行不用动。

2. 实战四步走

Step 1: 安装武器

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

Step 2: 改造 Model (告诉 GORM 表长什么样)

go
type User struct {
    gorm.Model // 自动获得 ID, CreatedAt, UpdatedAt, DeletedAt
    Username string `gorm:"uniqueIndex"` // 唯一索引
    Password string
}

Step 3: 实现 Repository (GORM 版零件)

go
type GormUserRepo struct { db *gorm.DB }

func (r *GormUserRepo) GetUserByUsername(name string) (*model.User, error) {
    var user model.User
    // 翻译:SELECT * FROM users WHERE username = ? LIMIT 1
    err := r.db.Where("username = ?", name).First(&user).Error
    return &user, err
}

Step 4: 依赖注入 (Main 函数换心手术)

go
// 1. 连接文件数据库
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&model.User{}) // 自动建表

// 2. 替换掉旧的内存 Repo
userRepo := repository.NewGormUserRepo(db) 
// Service 和 Controller 完全不知情,服务平滑升级!

第二阶段:灵魂拷问 —— GORM 到底是什么?

代码跑通了,但我的疑惑更多了。GORM 到底在底下干了什么?

Q1: 什么是表、行、列?

如果不懂 SQL,可以用 Excel 来类比:

概念类比说明
表 (Table)一个 Excel 文件users.xlsx
列 (Column)表头Username, Password,建表时定好,是死的
行 (Row)每一条数据具体的用户数据,是活的
┌─────────────────────────────────────────────────────────┐
│                    users 表                             │
├──────┬──────────────┬──────────────┬───────────────────┤
│  ID  │   Username   │   Password   │    CreatedAt      │  ← 列 (Column)
├──────┼──────────────┼──────────────┼───────────────────┤
│  1   │    admin     │    ****      │   2026-01-01      │  ← 行 (Row)
│  2   │    alice     │    ****      │   2026-01-02      │  ← 行 (Row)
│  3   │     bob      │    ****      │   2026-01-03      │  ← 行 (Row)
└──────┴──────────────┴──────────────┴───────────────────┘

GORM 的本质

它是一个 翻译官

  • 它把 Go 的 Struct 翻译成 表结构
  • 它把 Go 的 对象 翻译成 数据行

Q2: 加一个新字段就要建新表吗?

误区:我以为一行数据有这个字段,另一行没有,就得拆表。

真相:不用。数据库支持 NULL(空值)。

  • 加一列 = 表头变宽了
  • 老数据这列填 NULL,新数据填具体值
  • 只有当出现"一对多"(如一个用户有多个手机号)时,才需要建新表

第三阶段:核心操作的伪代码透视 (CRUD 完全解剖)

为了搞懂 GORM 的魔法,我把 增删改查 (CRUD) 的每一个动作都拆解成了 "GORM 写法" vs "SQL" vs "底层伪代码"。

1. 新增 (Create)

场景:注册一个新用户。

GORM 代码

go
user := model.User{Username: "new_guy", Password: "123"}
// 传递指针,为了让 GORM 填回自增 ID
db.Create(&user)

底层伪代码逻辑

go
func Create(val interface{}) {
    // 1. 反射分析: 拿到 User 结构体的所有字段名 (Username, Password...)
    tableName := toSnakeCase(reflect.TypeOf(val).Name) // "User" -> "users"
    
    // 2. 自动填补: CreatedAt, UpdatedAt 为空?填入 time.Now()
    
    // 3. 生成 SQL: 
    sql := "INSERT INTO users (username, password, created_at...) VALUES (?, ?, ?...)"
    
    // 4. 执行并回填 ID
    driver.Exec(sql)
    fillBackID(val) // 把数据库生成的 ID=42 填回 user.ID
}

2. 查询 (Read)

场景:根据用户名找人(单条数据)。

GORM 代码

go
var user model.User
// First 会自动加上 LIMIT 1,并把结果填进 &user
// 如果查不到,会返回 gorm.ErrRecordNotFound
db.Where("username = ?", "admin").First(&user)

实际生成的 SQL

sql
SELECT * FROM `users` 
WHERE username = 'admin' 
AND `users`.`deleted_at` IS NULL  -- 自动过滤掉软删除的数据
ORDER BY `users`.`id` ASC         -- First 默认按主键排序
LIMIT 1

底层伪代码逻辑

go
func First(dest interface{}) {
    // 1. 拼装 SQL
    sql := "SELECT * FROM users WHERE username = 'admin' ..."
    
    // 2. 执行查询,拿到"生数据" (Rows)
    rows := driver.Query(sql) 
    
    // 3. 扫描 (Scan) - 这是反射最累的一步
    if rows.Next() {
        // GORM 看着 user 结构体的字段,把 rows 里的数据一个个填进去
        dest.ID = rows["id"]
        dest.Username = rows["username"]
        // ...
    } else {
        return error("record not found")
    }
}

3. 更新 (Update)

场景:修改密码。这里有两种完全不同的模式。

模式 A:只更新特定字段 (推荐)

go
// 明确告诉 GORM:我只改 password 这一列
db.Model(&user).Update("password", "new_pass")
// SQL: UPDATE users SET password='new_pass', updated_at=NOW() WHERE id=1;

模式 B:Save (全量保存)

go
user.Password = "new_pass" // 先改内存
db.Save(&user)             // 同步到数据库

底层伪代码逻辑 (Save)

go
func Save(val interface{}) {
    // 1. 检查主键: val.ID 有值吗?
    if val.ID > 0 {
        // 有 ID -> 执行 UPDATE
        // 注意:它会把结构体里的【所有字段】都更新一遍!
        // 哪怕 Username 没变,它也会重写一遍 Username
        sql := "UPDATE users SET password=?, username=?, role=?, updated_at=NOW()... WHERE id=?"
        driver.Exec(sql)
    } else {
        // 没 ID -> 执行 INSERT (新增)
        Create(val)
    }
}

⚠️ Save 的陷阱:如果你只想改一个字段,用 UpdateSave 会覆盖所有字段,可能导致意外的数据覆盖。

4. 删除 (Delete)

场景:删除用户。

GORM 代码

go
// 只要 ID 是对的,其他字段乱填也能删
db.Delete(&User{ID: 5})

实际生成的 SQL (软删除 Magic)

因为 User 结构体嵌入了 gorm.Model,它有 DeletedAt 字段。GORM 默认执行 软删除

sql
-- 注意:不是 DELETE FROM !
UPDATE `users` 
SET `deleted_at` = '2026-01-23 10:10:00' 
WHERE `id` = 5 
AND `deleted_at` IS NULL

底层伪代码逻辑

go
func Delete(val interface{}) {
    // 1. 反射检查:这个 struct 有 DeletedAt 字段吗?
    if hasDeletedAtField(val) {
        // 有 -> 执行 UPDATE (软删除)
        // 只是给这条记录打个"已删除"的标记,以后查询会自动过滤它
        sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?"
    } else {
        // 无 -> 执行 DELETE (真删除/物理删除)
        sql = "DELETE FROM users WHERE id = ?"
    }
    driver.Exec(sql, val.ID)
}

CRUD 速查表

操作GORM 方法SQL 动作关键点
Createdb.Create(&user)INSERT传指针,回填 ID
Readdb.Where().First(&user)SELECT自动过滤软删除
Updatedb.Model(&user).Update()UPDATE推荐精确更新
Updatedb.Save(&user)UPDATE/INSERT全量覆盖,看 ID
Deletedb.Delete(&user)UPDATE/DELETE有 DeletedAt 则软删

第四阶段:终极迷惑 —— db.Model

db.Model 是 GORM 链式调用中最容易混淆的方法。它其实是一个 "定位器"

用法 A:只定表 (To Table)

当我们传入一个空结构体时:

go
db.Model(&User{})

含义:"GORM 听令,目标是 users 表!"

场景:配合 Where 做查询。

go
var count int64
db.Model(&User{}).Where("role = ?", "admin").Count(&count)
// SQL: SELECT COUNT(*) FROM users WHERE role = 'admin'

用法 B:既定表又定行 (To Row)

当我们传入一个带 ID 的结构体时:

go
u := User{ID: 100}
db.Model(&u)

含义:"GORM 听令,目标是 users 表里的 第 100 号记录!"

场景:配合 Update 做更新。

go
db.Model(&u).Update("name", "new_name")
// SQL: UPDATE users SET name = 'new_name' WHERE id = 100

db.Model 决策流程图

┌─────────────────────────────────────────────────────────┐
│                  db.Model(&obj)                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   obj.ID == 0 ?                                         │
│       │                                                 │
│       ├── Yes ──> 只定位到表 (users)                     │
│       │           适合:Count, Find, Where              │
│       │                                                 │
│       └── No ───> 定位到表 + 行 (users WHERE id=?)      │
│                   适合:Update, Delete                  │
│                                                         │
└─────────────────────────────────────────────────────────┘

总结

Week 2 的持久化之路,不仅是代码的迁移,更是对 ORM (对象关系映射) 思想的理解。

收获说明
接口解耦让底层切换如丝般顺滑
反射机制让 GORM 能够"猜"出我们的意图
主键思维是理解数据库操作的核心钥匙
┌─────────────────────────────────────────────────────────┐
│                    架构升级前后对比                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   Before (内存存储)                                      │
│   ┌─────────┐                                           │
│   │ Service │ ──> MemoryRepo ──> map[string]User        │
│   └─────────┘                                           │
│        ↓ 重启                                            │
│     数据清空 💀                                          │
│                                                         │
│   After (GORM 持久化)                                    │
│   ┌─────────┐                                           │
│   │ Service │ ──> GormRepo ──> SQLite ──> test.db       │
│   └─────────┘                                           │
│        ↓ 重启                                            │
│     数据保留 ✅                                          │
│                                                         │
└─────────────────────────────────────────────────────────┘

延伸阅读

Released under the MIT License.