社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

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

脚本之家 • 6 月前 • 450 次点击  
将 脚本之家 设为“星标⭐”
第一时间收到文章更新
图片
来源丨网管叨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(100) NOTNULL,
`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