Py学习  »  DATABASE

GO处理MySQL JSON的三种方案,你选哪个?

脚本之家 • 1 周前 • 59 次点击  
将 脚本之家 设为“星标
第一时间收到文章更新
图片
来源丨网管叨bi叨(ID:kevin_tech)

大家好,最近在维护一些先人写的代码时,发现很多先人在处理 MySQL 的JSON 字段时,习惯性地用  string类型。好像没啥毛病,反正存进去取出来再 Unmarshal一下也能用。

但实际上,自从 MySQL 5.7 官方不再维护后,MySQL 8.x 版本中,JSON 字段的功能和性能已经不可同日而语了。如果我们还在用老一套的“字符串一把梭”,不仅浪费了数据库的能力,还给代码埋下了隐患。

今天我们就以设计一个“作家(Writer)”表为例,聊聊在 GORM 中处理 JSON 数据的三种姿势。

假设我们要存储作家的信息,其中有两个典型的 JSON 字段:

  1. milestones (创作年表):结构固定(年份+事件)。
  2. metadata (元数据):结构不固定(可能包含流派、代理商、社交账号等)。

初始化 SQL 如下:

CREATE TABLE`writers` (
`id`INT AUTO_INCREMENT PRIMARY KEY,
`name`VARCHAR(100NOTNULL,
`milestones`JSONDEFAULTNULLCOMMENT'创作年表',
`metadata`JSONDEFAULTNULLCOMMENT'元数据',
`created_at` DATETIME DEFAULTCURRENT_TIMESTAMP
);

方案 A:使用 string存储

这是最简单直接的写法,直接把数据库的 JSON 字段映射成 Go 的 string

Model 定义:

type WriterA struct {
    ID         int    `gorm:"primaryKey"`
    Name       string `gorm:"not null"`
    
    // 直接用 string,完全把 JSON 当字符串处理
    Milestones string `gorm:"column:milestones;type:json"`
    Metadata   string `gorm:"column:metadata;type:json"`
}

存取示例:

// 写入时:需要手动序列化,并强制转换为 string
milestones := []map[string]interface{}{
    {"year"2006"work""三体"},
}
jsonBytes, _ := json.Marshal(milestones)
writer.Milestones = string(jsonBytes) //会发生内存拷贝

// 读取时:需要将 string 转回 []byte 再反序列化
var result []map[string]interface{}
json.Unmarshal([]byte(writer.Milestones), &result)

这个方案能用,但代码里充斥着类型转换,且性能和规范性最差。

方案 B:使用GORM的库 datatypes.JSON

这是 GORM 提供的方案,datatypes.JSON的底层其实是 []byte的别名。

Model 定义:

import "gorm.io/datatypes"


    


type WriterB struct {
    ID         int            `gorm:"primaryKey"`
    Name       string         `gorm:"not null"`
    
    // 使用官方类型
    Milestones datatypes.JSON `gorm:"column:milestones"` 
    Metadata   datatypes.JSON `gorm:"column:metadata"` 
}

存取示例:

// 写入时:依然需要序列化,但直接赋值 []byte
milestones := []map[string]interface{}{
    {"year"2006"work""三体"},
}
jsonBytes, _ := json.Marshal(milestones)
writer.Milestones = datatypes.JSON(jsonBytes) // 零拷贝

// 读取时:直接反序列化
var result []map[string]interface{}
json.Unmarshal(writer.Milestones, &result)

string和 datatypes.JSON 的区别

很多同学会有疑惑:方案 A 和 方案 B,在业务代码里不都要手动 Marshal和 Unmarshal吗?看起来没区别啊?

其实在实际代码的执行过程中,方案 B 是对方案 A 的降维打击:

  1. 查询能力的区别(关键)
  • 方案A (String):GORM 把它当普通字符串。如果你想查“所有科幻流派的作家”,你得手写原生 SQL ( WHERE metadata->>'$.tag' = 'sci-fi'),容易写错且难以维护。
  • 方案B (Datatypes.JSON):GORM 官方包封装了 JSONQuery。你可以写出非常优雅的链式调用,自动兼容不同数据库语法:
// 查找 metadata 中 tags 包含 "Sci-Fi" 的记录
db.Where(datatypes.JSONQuery("metadata").HasKey("tags""Sci-Fi")).Find(&writers)
  1. 性能与内存的区别
  • 方案A (String):json.Marshal出来是 []byte,存入 string字段时发生了一次内存拷贝(Copy)。高并发下这是无谓的 GC 压力。
  • 方案B (Datatypes.JSON):底层就是 []byte零拷贝直接落库。
  • 数据库兼容性
    • 方案A (String):进行数据迁移AutoMigrate时,GORM 不知道你是 JSON,可能会建成 TEXT类型,导致无法享受 MySQL 8.0 的 JSON 索引优化。
    • 方案B (Datatypes.JSON):GORM 明确知道这是 JSON,会自动映射为数据库的最佳 JSON 类型。

    方案 C:自定义结构体 (Scanner/Valuer)

    利用 Go 的接口机制,让 GORM 自动在 Go 结构体和数据库 JSON 之间转换。这是“强类型”的写法。

    具体定义:

    // 1. 定义具体业务结构
    type Milestone struct {
        Year int    `json:"year"`
        Work string`json:"work"`
    }
    type Milestones []Milestone

    // 2. 实现 sql.Scanner (读库:JSON -> Struct)
    func (m *Milestones) Scan(value interface{}) error {
        return json.Unmarshal(value.([]byte), m)
    }

    // 3. 实现 driver.Valuer (写库:Struct -> JSON)
    func (m Milestones) Value() (driver.Value, error) {
        return json.Marshal(m)
    }

    // 4. Model 使用
    type WriterC struct {
        ID         int        `gorm:"primaryKey"`
        // 直接使用强类型,无需再做 json 转换
        Milestones Milestones `gorm:"column:milestones;type:json"`
        // 假设 Metadata 也用结构体(如果结构固定的话)
        // Metadata MetadataStruct `gorm:"column:metadata;type:json"`
    }

    存取示例:

    // 写入:直接赋值结构体!
    writer.Milestones = Milestones{
        {Year: 2006, Work: "《三体》"},
    }
    db.Create(&writer)

    // 读取:直接使用结构体!
    var w WriterC
    db.First(&w, 1)
    fmt.Println(w.Milestones[0].Work) // 直接点出属性

    三种方案横向对比

    方案
    类型
    类型安全
    灵活性
    查询能力
    推荐指数
    适用场景
    A: String
    string
    ⭐⭐⭐
    弱 (需手写SQL)
    别用
    (除非只做透传)
    B: datatypes
    []byte
    ⭐⭐⭐
    强 (官方支持)
    ⭐⭐⭐
    动态结构
    (如配置项、元数据)
    C: 自定义结构
    Struct
    弱 (通常不查内部)
    ⭐⭐⭐⭐⭐
    核心业务
    (如年表、sku信息)

    最佳实践:混合双打

    看完对比,结论就很明显了。针对我们开头的 Writer表:

    • 对于 milestones(创作年表):结构固定,使用方案 C,确保类型安全。
    • 对于 metadata(元数据):结构动态,使用方案 B,保留灵活性,同时支持 JSON 查询。

    最终混合方案代码

    package model

    import (
        "database/sql/driver"
        "encoding/json"
        "gorm.io/datatypes"
        "time"
    )

    // === 1. 固定结构定义 (对应方案 C) ===
    type Milestone struct {
        Year  int    `json:"year"`
        Work  string`json:"work"`
        Award string`json:"award,omitempty"`
    }
    type Milestones []Milestone

    // 实现 Scanner 接口
    func (m *Milestones) Scan(value interface{}) error {
        bytes, ok := value.([]byte)
        if !ok {
            returnnil
        }
        return json.Unmarshal(bytes, m)
    }

    // 实现 Valuer 接口
    func (m Milestones) Value() (driver.Value, error) {
         return json.Marshal(m)
    }

    // === 2. 混合 Model 定义 ===
    type Writer struct {
        ID   int    `gorm:"primaryKey"`
        Name string`gorm:"not null"`
        
        // 强类型:创作年表 (方案 C)
        // 优势:代码自动提示,编译期检查,无需手动转换
        Milestones Milestones `gorm:"column:milestones;type:json"`
        
        // 灵活类型:元数据 (方案 B)
        // 优势:什么都能存,支持 GORM 的 JSON 查询语法
        Metadata   datatypes.JSON `gorm:"column:metadata;type:json"`
        
        CreatedAt  time.Time
    }

    // === 3. 实际使用体验 ===
    func main() {
        // 创建
        writer := Writer{
            Name: "刘慈欣",
            
            // 方案 C 爽点:直接写结构体,不用 Marshal
            Milestones: Milestones{
                {Year: 2006, Work: "《三体》"},
                {Year: 2015, Work: "《三体》", Award: "雨果奖"},
            },
        }
        
        // 方案 B:处理杂项,还是需要手动 Marshal 一下
        meta := map[string]interface{}{
            "tags": []string{"硬科幻""宏大叙事"}, 
            "fans""磁铁",
        }
        jsonBytes, _ := json.Marshal(meta)
        writer.Metadata = datatypes.JSON(jsonBytes) // 零拷贝赋值
        
        db.Create(&writer)
        
        // --- 查询与使用 ---
        var result Writer
        db.First(&result, writer.ID)
        
        // 方案 C 爽点:直接读取,自带类型
        println(result.Milestones[1].Award) // 输出:雨果奖
        
        // 方案 B 爽点:复杂查询
        // 查找 metadata 中 tags 包含 "硬科幻" 的记录
        var list []Writer
        db.Where(datatypes.JSONQuery("metadata").HasKey("tags""硬科幻")).Find(&list)
    }

    总结

    核心数据用 Struct 换安全,扩展数据用 datatypes.JSON 更灵活,坚决不用 string 一把梭。个人总结的 Go 语言处理 MySQL JSON 字段的口诀╮(╯▽╰)╭。

    END

    图片
      推荐阅读:
    1. VS Code 正式倒戈!正式切到 TypeScript 7!
    2. Markdown要被抛弃了?Claude Code工程师自曝:我已彻底放弃使用Markdown!团队倾向使用HTML!网友:其他编辑工具会被淘汰吗?
    3. 截胡苹果,安卓要先用上屏下Face ID了
    4. 你写的每一行代码,都在重复他80年前的一个偷懒决定
    5. 被AI抢走工作的人,在忙什么?

    Python社区是高质量的Python/Django开发社区
    本文地址:http://www.python88.com/topic/196413