Go操作xorm中间表多对多关联实战

c 复制代码
package model

import "time"

// Template 模板主表
type Template struct {
	ID            int64         `xorm:"pk autoincr"`
	Name          string        `xorm:"varchar(64)"`
	// 第一类策略,业务组装字段,不存库
	StrategyIds   []int64       `xorm:"-"`
	Strategys     []*Strategy   `xorm:"-"`
	// 第二类策略,业务组装字段,不存库
	Strategy1Ids  []int64       `xorm:"-"`
	Strategys1    []*Strategy1  `xorm:"-"`
}

// Task 任务(多对一模板,从表)
type Task struct {
	ID         int64     `xorm:"pk autoincr"`
	TaskName   string    `xorm:"varchar(64)"`
	TemplateID int64     `xorm:"index"`
	Template   *Template `xorm:"-"`
	CreateTime time.Time `xorm:"created"`
}

// Strategy 第一种策略主表
type Strategy struct {
	ID   int64  `xorm:"pk autoincr"`
	Name string `xorm:"varchar(64)"`
}

// Strategy1 第二种策略主表
type Strategy1 struct {
	ID   int64  `xorm:"pk autoincr"`
	Name string `xorm:"varchar(64)"`
}

// TemplateStrategy 模板<->Strategy 中间多对多表
type TemplateStrategy struct {
	TemplateID int64 `xorm:"pk"`
	StrategyID int64 `xorm:"pk"`
}

// TemplateStrategy1 模板<->Strategy1 中间多对多表
type TemplateStrategy1 struct {
	TemplateID  int64 `xorm:"pk"`
	Strategy1ID int64 `xorm:"pk"`
}

一、问 :StrategyIds \[\]int64 xorm:"-"已经存储多个策略的id,为什么还要建立中间表

答案:

核心一句话:

StrategyIds []int64 只用于「前后端传参、接口出参」,不落地数据库;数据库存关联必须中间表,不能靠数组字段存关联。

1、两个字段分工完全不一样

go 复制代码
StrategyIds []int64     `xorm:"-"` // 不在数据库建字段!!
Strategys   []*Strategy `xorm:"-"`
  • xorm:"-"xorm同步建表忽略此字段,MySQL表里没有这一列
  • 作用:前端传 [1,2,3] 接收、查询完组装ID列表返回前端,不持久化存储关联关系

想把策略ID数组存库只有两种:JSON字段 / 中间表

  • JSON:不能外键、不能连表JOIN、筛选困难
  • 中间表:标准数据库范式,用来持久保存【模板-策略】绑定关系

2、如果去掉中间表、只用数据库JSON存ID数组的痛点

  1. 没法用 join template inner join strategy 联表查询
  2. 根据策略ID反向查所有绑定的模板:where JSON_CONTAINS(ids,?); 不走索引,大数据慢
  3. 删策略时数据库无法外键级联删除,产生脏数据
  4. 不方便SQL统计:一个策略被多少模板引用

3、业务完整流程(帮你理清逻辑)

  1. 前端提交:strategyIds:[1,2] → 赋值给 Template.StrategyIds
  2. 后端代码:删掉中间表旧数据 → 循环批量插入TemplateStrategy(持久绑定关系)
  3. 查询:查中间表拿到所有策略ID → 塞回 Template.StrategyIds、Template.Strategys 返回前端

精简总结

  • StrategyIds[]int64(xorm:"-"):接口临时载体,不存库
  • TemplateStrategy中间表:数据库持久保存多对多绑定关系,必建

二、给出go操作xorm写入中间表的代码

一、已有model结构体(不变)

go 复制代码
package model

import "time"

// Template 模板
type Template struct {
	ID            int64         `xorm:"pk autoincr"`
	Name          string        `xorm:"varchar(64)"`
	StrategyIds   []int64       `xorm:"-"`
	Strategys     []*Strategy   `xorm:"-"`
	Strategy1Ids  []int64       `xorm:"-"`
	Strategys1    []*Strategy1  `xorm:"-"`
}

// Task 任务
type Task struct {
	ID         int64     `xorm:"pk autoincr"`
	TaskName   string    `xorm:"varchar(64)"`
	TemplateID int64     `xorm:"index"`
	Template   *Template `xorm:"-"`
	CreateTime time.Time `xorm:"created"`
}

// Strategy 第一类策略
type Strategy struct {
	ID   int64  `xorm:"pk autoincr"`
	Name string `xorm:"varchar(64)"`
}

// Strategy1 第二类策略
type Strategy1 struct {
	ID   int64  `xorm:"pk autoincr"`
	Name string `xorm:"varchar(64)"`
}

// TemplateStrategy 模板-策略中间表
type TemplateStrategy struct {
	TemplateID int64 `xorm:"pk"`
	StrategyID int64 `xorm:"pk"`
}

// TemplateStrategy1 模板-策略1中间表
type TemplateStrategy1 struct {
	TemplateID  int64 `xorm:"pk"`
	Strategy1ID int64 `xorm:"pk"`
}

二、保存模板+同步写入两张中间表代码

go 复制代码
package main

import (
	"xorm.io/xorm"
	"yourpath/model"
)

// SaveTemplate 新增/修改模板并维护两张中间关联表
// temp:前端传来的模板数据(包含StrategyIds、Strategy1Ids数组)
func SaveTemplate(engine *xorm.Engine, temp *model.Template) (int64, error) {
	var err error
	// 1.新增/更新模板主表
	if temp.ID == 0 {
		temp.ID, err = engine.Insert(temp)
		if err != nil {
			return 0, err
		}
	} else {
		_, err = engine.ID(temp.ID).Update(temp)
		if err != nil {
			return 0, err
		}
	}

	// ========== 处理第一张中间表 TemplateStrategy ==========
	// 先删除该模板原有绑定
	_, err = engine.Where("template_id = ?", temp.ID).Delete(&model.TemplateStrategy{})
	if err != nil {
		return 0, err
	}
	// 拼装中间表数据批量插入
	var tsList []model.TemplateStrategy
	for _, sid := range temp.StrategyIds {
		tsList = append(tsList, model.TemplateStrategy{
			TemplateID: temp.ID,
			StrategyID: sid,
		})
	}
	if len(tsList) > 0 {
		_, err = engine.Insert(tsList)
		if err != nil {
			return 0, err
		}
	}

	// ========== 处理第二张中间表 TemplateStrategy1 ==========
	_, err = engine.Where("template_id = ?", temp.ID).Delete(&model.TemplateStrategy1{})
	if err != nil {
		return 0, err
	}
	var ts1List []model.TemplateStrategy1
	for _, sid := range temp.Strategy1Ids {
		ts1List = append(ts1List, model.TemplateStrategy1{
			TemplateID:  temp.ID,
			Strategy1ID: sid,
		})
	}
	if len(ts1List) > 0 {
		_, err = engine.Insert(ts1List)
	}

	return temp.ID, err
}

三、查询回填数组字段配套代码(按需使用)

go 复制代码
// FillTemplateRel 查中间表,回填StrategyIds、Strategy1Ids、策略详情
func FillTemplateRel(engine *xorm.Engine, temp *model.Template) error {
	// 填充第一类策略ID和详情
	var ts []model.TemplateStrategy
	err := engine.Where("template_id=?", temp.ID).Find(&ts)
	if err != nil {
		return err
	}
	var sIds []int64
	for _, item := range ts {
		sIds = append(sIds, item.StrategyID)
	}
	temp.StrategyIds = sIds
	if len(sIds) > 0 {
		engine.In("id", sIds).Find(&temp.Strategys)
	}

	// 填充第二类策略ID和详情
	var ts1 []model.TemplateStrategy1
	err = engine.Where("template_id=?", temp.ID).Find(&ts1)
	if err != nil {
		return err
	}
	var s1Ids []int64
	for _, item := range ts1 {
		s1Ids = append(s1Ids, item.Strategy1ID)
	}
	temp.Strategy1Ids = s1Ids
	if len(s1Ids) > 0 {
		engine.In("id", s1Ids).Find(&temp.Strategys1)
	}
	return nil
}

调用示例

go 复制代码
func demo(engine *xorm.Engine) {
	// 前端入参
	t := &model.Template{
		Name:         "测试模板",
		StrategyIds:  []int64{1, 2, 3},   // 第一类策略id数组
		Strategy1Ids: []int64{5, 6},      // 第二类策略id数组
	}
	// 保存主表+中间表
	_, _ = SaveTemplate(engine, t)

	// 查询模板并自动回填策略数组
	var getT model.Template
	engine.ID(t.ID).Get(&getT)
	_ = FillTemplateRel(engine, &getT)
}

逻辑说明

  1. 删旧存新:每次更新先删当前模板所有历史关联,再批量新增,避免冗余数据;
  2. 中间表存储:一个模板+一个策略 = 一行记录,多条实现数组绑定;
  3. xorm:"-" 字段只做前后端交互,不落地数据库。

三、问 中间表没有自己的id么?

两种中间表写法,按需选用

方式1:当前复合主键(无单独自增ID,你现在代码)

go 复制代码
// 联合主键,没有单独id字段
type TemplateStrategy struct {
	TemplateID int64 `xorm:"pk"`
	StrategyID int64 `xorm:"pk"`
}
  • 主键 = (template_id,strategy_id),天然去重,不能重复绑定同一策略
  • 优点:少字段、省空间、多对多标准设计
  • 缺点:没法通过中间表ID单独删除某一条绑定

方式2:加独立自增ID(常用,推荐改这个)

go 复制代码
// 新增自增主键ID,两个外键单独索引
type TemplateStrategy struct {
	ID         int64 `xorm:"pk autoincr"` // 中间表自身主键
	TemplateID int64 `xorm:"index"`
	StrategyID int64 `xorm:"index"`
}

type TemplateStrategy1 struct {
	ID          int64 `xorm:"pk autoincr"`
	TemplateID  int64 `xorm:"index"`
	Strategy1ID int64 `xorm:"index"`
}

特点:

  1. 每行记录有唯一id,可根据中间表ID单条删除
  2. TemplateID、StrategyID只是普通外键索引,不再做主键
  3. 如需防重复绑定,加唯一联合索引标签
go 复制代码
TemplateID int64 `xorm:"index(uniq_tpl_str)"`
StrategyID int64 `xorm:"index(uniq_tpl_str)"`

保存中间表代码不用改动

删的时候依旧:

go 复制代码
engine.Where("template_id=?",tid).Delete(&model.TemplateStrategy{})

总结

  • 不需要单条精准删关联 → 复合主键无ID(原有写法)
  • 后续可能根据中间表主键操作 → 加ID自增字段(方案2)

四、采用第一种方法怎么增删改查

方案:中间表【复合主键、无自增ID】增删改查完整Xorm代码

go 复制代码
// 中间表结构(沿用你原来复合主键)
type TemplateStrategy struct {
	TemplateID int64 `xorm:"pk"`
	StrategyID int64 `xorm:"pk"`
}
type TemplateStrategy1 struct {
	TemplateID  int64 `xorm:"pk"`
	Strategy1ID int64 `xorm:"pk"`
}

复合主键规则:TemplateID+StrategyID 唯一,同模板不能重复绑同一个策略

1、新增(批量新增,前端传ID数组)

go 复制代码
// 给模板绑定策略,先删全量旧数据再批量新增(标准改绑逻辑)
func BindStrategy(engine *xorm.Engine, templateId int64, sIds []int64) error {
	// 1.删除该模板全部旧关联
	_, err := engine.Where("template_id=?", templateId).Delete(new(model.TemplateStrategy))
	if err != nil {
		return err
	}
	// 2.组装切片批量insert
	var list []model.TemplateStrategy
	for _, sid := range sIds {
		list = append(list, model.TemplateStrategy{
			TemplateID: templateId,
			StrategyID: sid,
		})
	}
	if len(list) > 0 {
		_, err = engine.Insert(&list)
	}
	return err
}

单个新增一条:engine.Insert(&model.TemplateStrategy{TemplateID:1,StrategyID:2})

2、删除

① 删除某个模板全部关联(最常用)

go 复制代码
// 删除模板=tid所有绑定
engine.Where("template_id=?", tid).Delete(new(model.TemplateStrategy))

② 删除【指定模板+指定策略】单条

go 复制代码
// 复合主键精准删一条
_, err := engine.Where("template_id=? and strategy_id=?", tid, sid).Delete(new(model.TemplateStrategy))

3、修改

复合主键没有单条Update用法:多对多常规逻辑:删旧全量→插新数据

没有update单字段,改绑统一:

  1. 删掉当前模板所有中间表数据
  2. 插入新的ID数组,上面BindStrategy就是修改逻辑

场景:模板原来绑1,2 → 修改为2,3:先删全部,再插入2、3

4、查询

① 根据模板ID查所有绑定策略ID

go 复制代码
func GetBindSids(engine *xorm.Engine, tid int64) ([]int64, error) {
	var rel []model.TemplateStrategy
	err := engine.Where("template_id=?", tid).Find(&rel)
	if err != nil {
		return nil, err
	}
	var sids []int64
	for _, v := range rel {
		sids = append(sids, v.StrategyID)
	}
	return sids, nil
}

② 根据策略ID反向查所有关联模板ID

go 复制代码
engine.Where("strategy_id=?", sid).Find(&rel)

③ 判断某个策略是否已绑定该模板

go 复制代码
has, _ := engine.Where("template_id=? and strategy_id=?", tid, sid).Exist(new(model.TemplateStrategy))

第二类策略TemplateStrategy1

代码逻辑完全一模一样,只替换结构体名即可。

补充:保存模板整体调用示例

go 复制代码
// 前端传入模板
tpl := &model.Template{
	Name:         "模板A",
	StrategyIds:  []int64{1,3},
	Strategy1Ids: []int64{2,5},
}
// 1.保存主表
tid,_ := engine.Insert(tpl)
// 2.绑定两类策略
BindStrategy(engine, tid, tpl.StrategyIds)
BindStrategy1(engine, tid, tpl.Strategy1Ids)

五、go语言使用gorm中是怎么操作关联表的?一对多,多对一,多对多

GORM 关联表操作(一对多、多对一、多对多)

GORM v2 版本,基于标签 gorm:"foreignKey,references" 绑定外键,分模型定义、增删改查三部分

一、多对一(N:1):多个子表 → 一个父表

示例:订单Order(N) → 用户User(1),多个订单归属同一个用户

1. 模型定义

go 复制代码
type User struct {
  gorm.Model
  Name string
}

type Order struct {
  gorm.Model
  UserID uint      // 外键:关联User.ID(默认规则:父表名+ID)
  User   User      // 多对一关联字段
  Amount float64
}
  • UserID:外键字段(必须存在)
  • User User:结构体嵌套,用于预加载查询

2. CRUD

go 复制代码
// 1.新增订单(绑定用户)
db.Create(&Order{UserID:1, Amount:99})

// 2.查询订单顺带查所属用户(Preload预加载)
var order Order
db.Preload("User").First(&order,1)

// 3.修改所属用户
db.Model(&order).Update("UserID",2)

二、一对多(1:N):一个父表 → 多个子表

承接上面:User(1) → Order(N),一个用户多个订单

1.模型定义

go 复制代码
type User struct {
  gorm.Model
  Name   string
  Orders []Order // 一对多切片字段
}

type Order struct {
  gorm.Model
  UserID uint
  Amount float64
}
  • 外键依旧在多方Order.UserID,一方用切片存储关联数据

2.CRUD

go 复制代码
// 1.创建用户+同时批量创建订单(事务自动保存关联)
user := User{
  Name:"张三",
  Orders:[]Order{{Amount:100},{Amount:200}},
}
db.Create(&user) // 自动填充Order.UserID=user.ID

// 2.查询用户+预加载所有订单
var u User
db.Preload("Orders").First(&u,1)

// 3.给用户新增关联订单
db.Model(&u).Association("Orders").Append(&Order{Amount:300})

// 4.移除某个订单关联(仅清外键,不删订单数据)
db.Model(&u).Association("Orders").Delete(&order)

Association("关联名"):GORM专用关联操作API(追加/删除/清空/替换)

三、多对多(M:N):两张表互相多关联,自动生成中间表

示例:商品Product ↔ 分类Category,一个商品多个分类,一个分类多个商品

GORM自动生成中间表 (默认表名:product_categories),无需手动建中间表模型

1.模型定义

go 复制代码
type Product struct {
  gorm.Model
  Name      string
  Categories []Category `gorm:"many2many:product_categories;"` // 指定中间表名
}

type Category struct {
  gorm.Model
  Name    string
  Products []Product `gorm:"many2many:product_categories;"`
}
  • many2many:中间表名:必填标签,两边必须一致

2.CRUD

go 复制代码
// 1.新建商品+绑定多个分类
p := Product{
  Name:"手机",
  Categories:[]Category{{Name:"数码"},{Name:"3C"}},
}
db.Create(&p) // 自动维护中间表数据

// 2.查询商品+预加载所有分类
var prod Product
db.Preload("Categories").First(&prod,1)

// 3.追加新分类关联
cat := Category{Name:"电子产品"}
db.Model(&prod).Association("Categories").Append(&cat)

// 4.移除某个分类关联(只删中间表记录,不删商品/分类主表)
db.Model(&prod).Association("Categories").Delete(&cat)

// 5.清空所有关联
db.Model(&prod).Association("Categories").Clear()

四、常用预加载优化

  1. 嵌套预加载(关联里再查关联)
go 复制代码
// 用户→订单,订单里如需扩展关联地址
db.Preload("Orders.Address").First(&user)
  1. 条件预加载
go 复制代码
// 只查询金额>50的订单
db.Preload("Orders",func(tx *gorm.DB)*gorm.DB{
  return tx.Where("amount > ?",50)
}).First(&user)

五、自定义外键/关联名(非常规字段)

不想用默认UserID,自定义外键:

go 复制代码
type Order struct {
  gorm.Model
  Uid    uint `gorm:"foreignKey:Uid;references:UID"` // Uid=本字段,UID=User主键字段
  User   User
}
type User struct {
  UID uint `gorm:"primaryKey"` // 自定义主键
  Name string
}

需要我把关联删除(级联删除)、软删除关联写法补充一份示例吗?

相关推荐
卷无止境1 小时前
C# 与 .NET 中的委托:把方法装进变量里
后端
长栎1 小时前
手写一个表达式计算器,你就理解解释器模式了
后端
长栎1 小时前
foreach 语法糖背后,迭代器模式做了多少脏活
后端
Jun6261 小时前
QT(4)-EXCEL操作
开发语言·qt·excel
fengfuyao9851 小时前
基于MATLAB的HHT变换完整实现(含EMD分解与三维时频谱生成)
开发语言·算法·matlab
HLAIA光子1 小时前
LLM缓存机制:你的API账单可以砍掉75%
后端·llm·ai编程
卷无止境1 小时前
统计质量控制(SQC / SPC):用数据说话的质量哲学
后端
XovH1 小时前
第 44篇 k8s之实战:将 Web 应用迁移到 Kubernetes(上)
后端
晓杰'1 小时前
从0到1实现Balatro游戏后端(7):Boss Blind与特殊规则实现
后端·websocket·typescript·node.js·游戏开发·项目实战·nestjs