You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用sqlx在PostgreSQL存储递归引用的Go结构体?

解决Go递归结构体与PostgreSQL多对多关联的存储问题

哥们,你现在碰到的核心问题是关系型数据库(比如PostgreSQL)没法直接映射Go里的互相递归切片结构体——你原来想在表字段里存数组来保存关联关系,这在SQL的设计逻辑里是不规范的,而且很难维护和查询。咱们得先从数据库表结构的设计入手,再调整代码逻辑。

一、正确的数据库表设计(多对多关联)

Person和Tenant是典型的多对多关系:一个用户可以属于多个租户,一个租户可以有多个用户。这种关系在SQL里需要用中间关联表来实现,而不是在主表里存数组。

修改后的表结构应该是这样的:

CREATE TABLE IF NOT EXISTS person (
    id INT PRIMARY KEY,
    username TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS tenant (
    id INT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

-- 中间关联表,存储Person和Tenant的对应关系
CREATE TABLE IF NOT EXISTS person_tenant (
    person_id INT REFERENCES person(id) ON DELETE CASCADE,
    tenant_id INT REFERENCES tenant(id) ON DELETE CASCADE,
    PRIMARY KEY (person_id, tenant_id) -- 联合主键避免重复关联
);

二、调整Go结构体定义

原来的结构体里的Person []Person和Tenants *[]Tenant是关联关系,不是数据库表的字段,所以要去掉它们的db标签,只保留表本身的字段:

type Tenant struct {
    Id      int      `db:"id"`
    Name    string   `db:"name"`
    Persons []Person // 这是关联字段,不需要db标签
}

type Person struct {
    Id       int       `db:"id"`
    Username string    `db:"username"`
    Tenants  []Tenant  // 同样是关联字段,不需要db标签
}

三、用sqlx实现存储与查询

1. 插入数据(带关联关系)

因为是多对多,得先插入Person和Tenant的主记录,再插入中间表的关联记录:

func main() {
    // 省略数据库连接代码...

    // 先创建表
    var schema = `
        CREATE TABLE IF NOT EXISTS person (
            id INT PRIMARY KEY,
            username TEXT NOT NULL UNIQUE
        );

        CREATE TABLE IF NOT EXISTS tenant (
            id INT PRIMARY KEY,
            name TEXT NOT NULL UNIQUE
        );

        CREATE TABLE IF NOT EXISTS person_tenant (
            person_id INT REFERENCES person(id) ON DELETE CASCADE,
            tenant_id INT REFERENCES tenant(id) ON DELETE CASCADE,
            PRIMARY KEY (person_id, tenant_id)
        );
    `
    db.MustExec(schema)

    // 定义要插入的数据
    person1 := Person{Id: 1, Username: "newuser"}
    tenant1 := Tenant{Id: 1, Name: "newtenant"}

    // 开启事务
    tx := db.MustBegin()
    // 插入Person
    tx.NamedExec("INSERT INTO person (id, username) VALUES (:id, :username)", &person1)
    // 插入Tenant
    tx.NamedExec("INSERT INTO tenant (id, name) VALUES (:id, :name)", &tenant1)
    // 插入关联关系
    tx.Exec("INSERT INTO person_tenant (person_id, tenant_id) VALUES ($1, $2)", person1.Id, tenant1.Id)
    // 提交事务
    tx.Commit()

    // 查询数据(带关联)
    // 先查Tenant
    var out []Tenant
    db.Select(&out, "SELECT * FROM tenant ORDER BY name ASC")

    // 为每个Tenant查询关联的Person
    for i := range out {
        var persons []Person
        db.Select(&persons, `
            SELECT p.* FROM person p
            JOIN person_tenant pt ON p.id = pt.person_id
            WHERE pt.tenant_id = $1
        `, out[i].Id)
        out[i].Persons = persons
    }

    fmt.Printf("Tenants with persons: %+v\n", out)
}

2. 查询关联数据的逻辑

这里需要注意:sqlx本身不会自动处理关联关系,所以得手动执行两次查询——先查主表数据,再通过中间表查询关联的子数据,最后填充到结构体里。

四、更省心的替代库建议

如果你觉得手动处理关联关系太麻烦,推荐使用GORM(Go的ORM框架),它对多对多关联的支持非常友好,能自动生成中间表、自动处理关联的插入和查询,不用写大量手动关联的SQL。

用GORM实现的话,只需要给结构体加上关联标签:

type Tenant struct {
    ID      int      `gorm:"primaryKey"`
    Name    string
    Persons []Person `gorm:"many2many:person_tenant;"` // 指定中间表名
}

type Person struct {
    ID       int      `gorm:"primaryKey"`
    Username string
    Tenants  []Tenant `gorm:"many2many:person_tenant;"`
}

插入和查询会变得非常简单:

// 插入数据
db.Create(&tenant1)
db.Model(&tenant1).Association("Persons").Append(&person1)

// 查询数据(自动带关联)
var tenant Tenant
db.Preload("Persons").First(&tenant, 1)

这样就完全不用手动处理中间表的SQL了,GORM会帮你搞定所有关联逻辑。

内容的提问来源于stack exchange,提问作者Battalgazi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:51:42