在gorm中如何支持Json字段(结构体字段)和数组字段的模型迁移

如果我们只使用一张表来存储这个Blog数据结构的话,通常的做法是使用Json类型和数组类型的字段来存储

目前Mysql和Postgresql的新版都已支持Json类型和数组类型。

本文所使用的数据库是Postgresql,具体版本不影响,Mysql的话可能需要新版的

可是在gorm里面我们直接将这个结构体进行模型迁移的话,会遇到不少问题!

下面我们来分析一下:

如果我们之间将这个结构体进行模型迁移的话,就会报下面的错误

可以看到,gorm是不支持数组类型的,咋办呢?很简单,只需要做一个简单的改动,就能让它支持数据类型。为Tags字段加上标签gorm:"type:varchar(255)[]"就不会报错了。

type Blog struct {
	ID      int      `json:"id" gorm:"primaryKey"`
	Tags    []string `json:"tags" gorm:"type:varchar(255)[]"`
	Author  Author   `json:"author"`
	Title   string   `json:"title"`
	Content string   `json:"content"`
}

但此时仍然无法进行迁移,还会报错:

[error] invalid field found for struct main.Blog's field Author: define a valid foreign key for relations or implement the Valuer/Scanner interface

2024/01/20 00:54:37 invalid field found for struct main.Blog's field Author: define a valid foreign key for relations or implement the Valuer/Scanner interface

这是因为gorm不能直接迁移Bolg中的Author字段。解决也很简单,跟上面一样,只需要添加一个gorm:"type:json"标签即可。

type Blog struct {
	ID      int      `json:"id" gorm:"primaryKey"`
	Tags    []string `json:"tags" gorm:"type:varchar(255)[]"`
	Author  Author   `json:"author" gorm:"type:json"`
	Title   string   `json:"title"`
	Content string   `json:"content"`
}

此时就能迁移成功了!查看数据库,表也建起来了!

不过现在还没结束,我们再来尝试读取和写入的操作

func InsertData() {
	author := Author{
		Name:  "John Doe",
		Email: "[email protected]",
	}

	blog := Blog{
		Tags:    []string{"tag1", "tag2"},
		Author:  author,
		Title:   "Hello, World!",
		Content: "This is a sample blog post.",
	}

	result := DB.Create(&blog)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
}

试着插入数据,执行完代码后,发现还是报错:

原因是因为tags字段不支持直接插入切片类型的数据,这里有一个比较简单的做法,就是把切片类型修改成一个第三方库提供的类型,具体后面会给出完整代码:

type Blog struct {
    ID      int            `json:"id" gorm:"primaryKey"`
    Tags    pq.StringArray `json:"tags" gorm:"type:varchar(255)[]"`
    Author  Author         `json:"author" gorm:"type:json"`
    Title   string         `json:"title"`
    Content string         `json:"content"`
}

现在再来试试插入数据,发现已经插入成功,数据库表里也有数据了。

我们再试试读数据

相信也能猜到了,很显然,还是会出现报错,而这次报错的原因,是出在author字段无法scan到对应的结构体里面。

2024/01/20 01:14:26 sql: Scan error on column index 2, name "author": unsupported Scan, storing driver.Value type []uint8 into type *main.Author

咋办呢?其实说白了就是gorm不认识我们的Author类型,而我们要做的就是需要让gorm认识,而相关的文档,在官网里有提到自定义数据类型。

这里直接贴出代码,就是给我们的Author实现两个规定的方法:

其实这里做了很简单的事情,就是在数据库插入数据前,把我们的结构体序列化成字节数组,在数据库查询到数据时,将对应的数据反序列成对应的结构体:

// Scan 将数据库中的值转换为Author类型
func (o *Author) Scan(value interface{}) error {
    b, ok := value.([]byte)
    if !ok {
        return errors.New("failed to unmarshal Author value")
    }
    var config Author
    err := json.Unmarshal(b, &config)
    if err != nil {
        return err
    }
    *o = config
    return nil
}

// Value 将Author类型转换为数据库可存储的值
func (o Author) Value() (driver.Value, error) {
    return json.Marshal(o)
}

此时,就能可读可写了!

本文完整代码:

package main

import (
    "database/sql/driver"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/lib/pq"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "log"
)

type Author struct {
    Name  string
    Email string
}

// Scan 将数据库中的值转换为Author类型
func (o *Author) Scan(value interface{}) error {
    b, ok := value.([]byte)
    if !ok {
        return errors.New("failed to unmarshal Author value")
    }
    var config Author
    err := json.Unmarshal(b, &config)
    if err != nil {
        return err
    }
    *o = config
    return nil
}

// Value 将Author类型转换为数据库可存储的值
func (o Author) Value() (driver.Value, error) {
    return json.Marshal(o)
}

type Blog struct {
    ID      int            `json:"id" gorm:"primaryKey"`
    Tags    pq.StringArray `json:"tags" gorm:"type:varchar(255)[]"`
    Author  Author         `json:"author" gorm:"type:json"`
    Title   string         `json:"title"`
    Content string         `json:"content"`
}

func (Blog) TableName() string {
    return "t_blog"
}

var DB *gorm.DB

func InitDB() {
    var err error
    dsn := "user=postgres password=xxx dbname=test port=5432 sslmode=disable TimeZone=Asia/Shanghai"
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }
    //数据库迁移
    err = DB.AutoMigrate(Blog{})
    if err != nil {
        log.Fatal(err)
    }
}

func InsertData() {
    author := Author{
        Name:  "John Doe",
        Email: "[email protected]",
    }

    blog := Blog{
        Tags:    []string{"tag1", "tag2"},
        Author:  author,
        Title:   "Hello, World!",
        Content: "This is a sample blog post.",
    }

    result := DB.Create(&blog)
    if result.Error != nil {
        log.Fatal(result.Error)
    }
}

func ReadData() {
    blog := Blog{}
    result := DB.First(&blog)
    if result.Error != nil {
        log.Fatal(result.Error)
    }
    fmt.Printf("ID: %d
", blog.ID)
    fmt.Printf("Tags: %v
", blog.Tags)
    fmt.Printf("Author: %v
", blog.Author)
    fmt.Printf("Title: %s
", blog.Title)
    fmt.Printf("Content: %s
", blog.Content)
}

func main() {
    InitDB()
    InsertData()
    ReadData()
}