一对一关联

一对一关联是最简单的关联关系。一个模型拥有另一个模型,比如用户和用户资料、文章和作者。

Has One 关系

用户有一个资料卡:

type User struct {
    ID      uint
    Name    string
    Profile Profile
}

type Profile struct {
    ID     uint
    UserID uint
    Bio    string
    Avatar string
}

Profile 通过 UserID 外键关联到 User。GORM 自动识别这种关系。

外键约定

默认外键名是 模型名 + ID,即 UserID

自定义外键:

type User struct {
    ID      uint
    Name    string
    Profile Profile `gorm:"foreignKey:UserRef"`
}

type Profile struct {
    ID      uint
    UserRef uint
    Bio     string
}

引用键约定

默认引用主键。自定义引用键:

type User struct {
    ID    uint
    UUID  string `gorm:"unique"`
    Profile Profile `gorm:"foreignKey:UserUUID;references:UUID"`
}

type Profile struct {
    ID       uint
    UserUUID string
    Bio      string
}

创建关联

创建用户时同时创建资料:

user := User{
    Name: "张三",
    Profile: Profile{
        Bio:    "这是个人简介",
        Avatar: "avatar.jpg",
    },
}
db.Create(&user)

两条记录都会创建,外键自动设置。

查询关联

var user User
db.Preload("Profile").First(&user, 1)
fmt.Println(user.Profile.Bio)

用 Joins 预加载:

db.Joins("Profile").First(&user, 1)

更新关联

var user User
db.First(&user, 1)
user.Profile.Bio = "更新后的简介"
db.Save(&user)

或者直接更新关联:

db.Model(&user).Association("Profile").Update(&Profile{Bio: "新简介"})

删除关联

删除用户时,关联的资料怎么处理?

默认不处理,资料保留。可以通过外键约束设置级联:

type User struct {
    ID      uint
    Name    string
    Profile Profile `gorm:"constraint:OnDelete:CASCADE;"`
}

删除用户时自动删除资料。

Belongs To 关系

反向的一对一,资料属于用户:

type Profile struct {
    ID     uint
    UserID uint
    User   User
    Bio    string
}

Profile 通过 UserIDUser 关联,表示资料属于某个用户。

自定义外键:

type Profile struct {
    ID      uint
    User    User `gorm:"foreignKey:UserRef"`
    UserRef uint
    Bio     string
}

Has One vs Belongs To

语义上的区别:

  • Has One:用户有一个资料(用户是主体)
  • Belongs To:资料属于用户(资料是主体)

数据库层面,两种方式的外键都在"多"的一方。一对一关系里,外键在哪张表,那张表就是 Belongs To。

实际案例

用户与身份证

type User struct {
    ID        uint
    Name      string
    IDCard    IDCard
}

type IDCard struct {
    ID        uint
    UserID    uint
    Number    string
    IssueDate time.Time
}

订单与支付

type Order struct {
    ID        uint
    Amount    float64
    Payment   Payment
}

type Payment struct {
    ID        uint
    OrderID   uint
    Method    string
    PaidAt    time.Time
}

文章与封面

type Article struct {
    ID        uint
    Title     string
    Cover     Image
}

type Image struct {
    ID        uint
    ArticleID uint
    URL       string
}

常见问题

外键为空

查询时关联为空:

var user User
db.First(&user, 1)
fmt.Println(user.Profile)

没有预加载,Profile 是空的。必须预加载:

db.Preload("Profile").First(&user, 1)

关联不存在

如果用户没有资料,Profile 是零值结构体。检查:

if user.Profile.ID == 0 {
    // 资料不存在
}

循环引用

type User struct {
    ID      uint
    Profile Profile
}

type Profile struct {
    ID     uint
    UserID uint
    User   User
}

会导致无限递归。解决方案:一方不存反向引用,或者用指针。

小结

一对一关联简单常用。理解 Has One 和 Belongs To 的语义区别,注意外键和引用键的配置。