Go语言GORM多对多关系实战详解
1. 数据库表设计与数据准备
首先创建三张表:用户表、资料表和关联中间表。
用户表(user):
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`user_key` bigint(20) NOT NULL,
`account` char(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
初始化数据(为方便测试,user_key和id保持一致):
+----+----------+---------+
| id | user_key | account |
+----+----------+---------+
| 1 | 1 | user-1 |
+----+----------+---------+
资料表(profile):
CREATE TABLE `profile` (
`id` bigint(20) NOT NULL,
`profile_key` bigint(20) NOT NULL,
`desc` char(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
初始化数据(同理,profile_key与id保持一致):
+----+-------------+-----------+
| id | profile_key | desc |
+----+-------------+-----------+
| 2 | 2 | profile-1 |
| 3 | 3 | profile-2 |
+----+-------------+-----------+
关联表(user_profile):用于存储多对多关系,一个用户能关联多个资料,一个资料也能被多个用户关联。
CREATE TABLE `user_profile` (
`id` bigint(20) NOT NULL,
`profile_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关系数据:
+----+------------+---------+
| id | profile_id | user_id |
+----+------------+---------+
| 4 | 2 | 1 |
| 5 | 3 | 1 |
+----+------------+---------+
2. 标签字段含义速查表
| 标签项 | 说明 |
|---|---|
| many2many | 指定关联中间表的表名,若不指定则GORM默认使用两张表名+下划线组合 |
| foreignKey | 主表中用于关联的字段,对应中间表的joinForeignKey |
| joinForeignKey | 中间表中与主表foreignKey字段对应的列 |
| references | 目标表中用于关联的字段,对应中间表的joinReferences |
| joinReferences | 中间表中与目标表references字段对应的列 |
3. Go代码实现
package test_service
import (
"encoding/json"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// User 用户模型
type User struct {
Id int `json:"id" gorm:"id"`
UserKey int `json:"user_key" gorm:"user_key"`
Account string `json:"account" gorm:"account"`
// 方式一:使用默认主键关联
// 系统会用 user.id → user_profile.user_id,profile.id → user_profile.profile_id
Profiles []Profile `gorm:"many2many:user_profile;joinForeignKey:user_id;joinReferences:profile_id;" json:"profiles"`
// 方式二:使用自定义字段关联
// 通过 foreignKey:user_key 指定 user.user_key 对应 user_profile.user_id
// 通过 references:profile_key 指定 profile.profile_key 对应 user_profile.profile_id
AllProfiles []Profile `gorm:"many2many:user_profile;foreignKey:user_key;joinForeignKey:user_id;references:profile_key;joinReferences:profile_id;" json:"all_profiles"`
}
func (*User) TableName() string {
return "user"
}
// Profile 资料模型
type Profile struct {
Id int `json:"id" gorm:"id"`
ProfileKey int `json:"profile_key" gorm:"profile_key"`
Desc string `json:"desc" gorm:"desc"`
}
func (*Profile) TableName() string {
return "profile"
}
// UserProfile 关联表模型
type UserProfile struct {
Id int `json:"id" gorm:"id"`
ProfileId int `json:"profile_id" gorm:"profile_id"`
UserId int `json:"user_id" gorm:"user_id"`
}
func (*UserProfile) TableName() string {
return "user_profile"
}
var db *gorm.DB
func init() {
addr := "127.0.0.1:3306"
user := "user"
pass := "pass"
name := "test_1"
dsn := "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local"
dsn = fmt.Sprintf(dsn, user, pass, addr, name)
db, _ = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
}
// ManyToMany 执行多对多查询
func ManyToMany() {
var users []User
// Preload 实现关联查询,可同时加载多个关联关系
db.Preload("Profiles").Preload("AllProfiles").Where("id", 1).Find(&users)
by, _ := json.Marshal(users)
fmt.Println(string(by))
}
4. 查询结果示例
[
{
"id": 1,
"user_key": 1,
"account": "user-1",
"profiles": [
{
"id": 2,
"profile_key": 2,
"desc": "profile-1"
},
{
"id": 3,
"profile_key": 3,
"desc": "profile-2"
}
],
"all_profiles": [
{
"id": 2,
"profile_key": 2,
"desc": "profile-1"
},
{
"id": 3,
"profile_key": 3,
"desc": "profile-2"
}
]
}
]
两种关联方式均正确返回了用户关联的两条资料记录。
5. 底层SQL执行流程
GORM在执行Preload时会分三步完成查询:
-- 第一步:查询主表记录
SELECT * FROM `user` WHERE `id` = 1;
-- 第二步:根据主表id查询中间表
SELECT * FROM `user_profile` WHERE `user_profile`.`user_id` = 1;
-- 第三步:根据中间表的profile_id查询目标表
SELECT * FROM `profile` WHERE `profile`.`id` IN (2,3);
通过这种分步查询策略,GORM能高效地完成多对多关系的预加载,避免N+1查询问题。