G

[Golang]Gorm框架 关系操作

RoLingG 2023-11-12

一对多关系

在gorm中,官方文档是把一对多关系分为了两类,

Belongs To 属于谁

Has Many 我拥有的

以用户和文章为例

一个用户可以发布多篇文章,一篇文章属于一个用户

type User struct {
  ID       uint      `gorm:"size:4"`
  Name     string    `gorm:"size:8"`
  Articles []Article // 用户拥有的文章列表
}

type Article struct {
  ID     uint   `gorm:"size:4"`
  Title  string `gorm:"size:16"`
  UserID uint   // 属于   这里的类型要和引用的外键类型一致,包括大小
  User   User   // 属于
}

关于外键命名,外键名称就是关联表名+ID,类型是uint

重写外键关联

package main

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "gorm.io/gorm/schema"
)

// 用户表
type User struct {
   ID       uint      `gorm:"size:4"`
   Name     string    `gorm:"size:8"`
   Articles []Article // 用户拥有的文章列表
}

// 文章表 一篇文章属于一个用户,一个用户能有多篇文章
type Article struct {
   ID     uint   `gorm:"size:4"`
   Title  string `gorm:"size:16"`
   UserID uint   // 属于   这里的类型要和引用的外键类型一致,包括大小    外键名称就是`关联表名+ID`
   User   User   // 属于
}

//type User struct {
// ID       uint      `gorm:"size:4"`
// Name     string    `gorm:"size:8"`
// Articles []Article `gorm:"foreignKey:UID"` // 用户拥有的文章列表
//}
//
//type Article struct {
// ID    uint   `gorm:"size:4"`
// Title string `gorm:"size:16"`
// UID   uint   // 属于 改了Article 的外键,将UID作为了外键,那么User这个外键关系就要指向UID 与此同时,User所拥有的Articles也得更改外键,改为UID
// User  User   `gorm:"foreignKey:UID"` // 属于
//}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
      //跳过默认事故,它默认是false,在初始化时禁用它,这样可以获得60%的性能提升
      //SkipDefaultTransaction: true,

      //如果不想自动以蛇形复数那些默认形式生成表,就写在这里面
      NamingStrategy: schema.NamingStrategy{
         SingularTable: false, // 是否单数表名
         NoLowerCase:   true,  // 是否关闭小写转换
      },
   })
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   DB.AutoMigrate(&User{}, &Article{})

}

一对多添加

package main

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "gorm.io/gorm/schema"
)

//// 用户表
//type User struct {
// ID       uint      `gorm:"size:4"`
// Name     string    `gorm:"size:8"`
// Articles []Article // 用户拥有的文章列表
//}
//
//// 文章表   一篇文章属于一个用户,一个用户能有多篇文章
//type Article struct {
// ID     uint   `gorm:"size:4"`
// Title  string `gorm:"size:16"`
// UserID uint   // 属于   这里的类型要和引用的外键类型一致,包括大小    外键名称就是`关联表名+ID`
// User   User   // 属于
//}

//重写外键关联
//type User struct {
// ID       uint      `gorm:"size:4"`
// Name     string    `gorm:"size:8"`
// Articles []Article `gorm:"foreignKey:UID"` // 用户拥有的文章列表
//}
//
//type Article struct {
// ID    uint   `gorm:"size:4"`
// Title string `gorm:"size:16"`
// UID   uint   // 属于 改了Article 的外键,将UID作为了外键,那么User这个外键关系就要指向UID 与此同时,User所拥有的Articles也得更改外键,改为UID
// User  User   `gorm:"foreignKey:UID"` // 属于
//}

// 重写外键引用  (但这样不适合实战应用)
type User struct {
   ID       uint      `gorm:"size:4"`
   Name     string    `gorm:"size:8"`
   Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用户拥有的文章列表   外键是UserName,主键是Name
}

type Article struct {
   ID       uint   `gorm:"size:4"`
   Title    string `gorm:"size:16"`
   UserName string
   User     User `gorm:"references:Name"` // 属于
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
      //跳过默认事故,它默认是false,在初始化时禁用它,这样可以获得60%的性能提升
      //SkipDefaultTransaction: true,

      //如果不想自动以蛇形复数那些默认形式生成表,就写在这里面
      NamingStrategy: schema.NamingStrategy{
         SingularTable: false, // 是否单数表名
         NoLowerCase:   true,  // 是否关闭小写转换
      },
   })
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   //日志显示
   DB = DB.Session(&gorm.Session{
      Logger: mysqlLogger,
   })

   //DB.AutoMigrate(&User{}, &Article{})
   //创建用户,带上文章
   //DB.Create(&User{
   // Name: "RoLingG",
   // Articles: []Article{
   //    {
   //       Title: "Python",
   //    },
   //    {
   //       Title: "Golang",
   //    },
   // },
   //})

   //创建文章,关联已有用户
   //DB.Create(&Article{
   // Title:    "C++狗屎",
   // 一般来说主键是ID,我这主键是Name,就当关联ID进行关联操作就好了
   // UserName: "RoLingG",
   //})

   //DB.Create(&Article{
   // Title: "Golang天下第一",
   // User: User{
   //    Name: "Clouwer",
   // },
   //})

   //以现有对象进行添加操作
   //var user User
   //DB.Take(&user, 2)
   //DB.Create(&Article{
   // Title: "Spring也挺好",
   // User:  user,
   //})
}

外键添加

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"
)

// 重写外键引用    (但这样不适合实战应用)
type User struct {
    ID       uint      `gorm:"size:4"`
    Name     string    `gorm:"size:8"`
    Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用户拥有的文章列表    外键是UserName,主键是Name
}

type Article struct {
    ID       uint   `gorm:"size:4"`
    Title    string `gorm:"size:16"`
    UserName string
    User     User `gorm:"references:Name"` // 属于
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
    username := "root1"    //账号
    password := "rootroot" //密码
    host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
    port := 3306           //数据库端口
    DBname := "GoORM"      //数据库名
    timeout := "10s"       //连接超时,10秒

    mysqlLogger = logger.Default.LogMode(logger.Info)

    // root:root@tcp(127.0.0.1:3306)/gorm?
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
    //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        //跳过默认事故,它默认是false,在初始化时禁用它,这样可以获得60%的性能提升
        //SkipDefaultTransaction: true,

        //如果不想自动以蛇形复数那些默认形式生成表,就写在这里面
        NamingStrategy: schema.NamingStrategy{
            SingularTable: false, // 是否单数表名
            NoLowerCase:   true,  // 是否关闭小写转换
        },
    })
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }
    DB = db
    // 连接成功
}

func main() {
    //日志显示
    DB = DB.Session(&gorm.Session{
        Logger: mysqlLogger,
    })

//给已有用户绑定文章
   //方法一:Save方式
   //将原本为Clouwer的文章变成了RoLingG的
   //var user User
   //DB.Take(&user, 1)
   //
   //var article Article
   //DB.Take(&article, 5)
   //
   //user.Articles = []Article{article}
   //DB.Save(&user)

   //方法二:Append方式
   //var user User
   //DB.Take(&user, 2)
   //var article Article
   //DB.Take(&article, 5)
   //Articles是指type User里面的
   //DB.Model(&user).Association("Articles").Append(&article)

   //给文章绑定用户
    //同上:方法一
    //var article Article
    //DB.Take(&article, 5)
    //article.UserName = "克劳德"
    //DB.Save(&article)

    //方法二:
    var user User
    DB.Take(&user, 1)
    var article Article
    DB.Take(&article, 5)
    //User是指type Articles里面的
    DB.Model(&article).Association("User").Append(&user)
}

查询

查询用户,显示用户的文章列表

所以这里就要用到预加载

预加载

var user User
DB.Preload("Articles").Take(&user, 1)
fmt.Println(user)

预加载的名字就是外键关联的属性名

查询文章,显示文章用户的信息

同样的,使用预加载

var article Article
DB.Preload("User").Take(&article, 1)
fmt.Println(article)

嵌套预加载

查询文章,显示用户,并且显示用户关联的所有文章,这就得用到嵌套预加载了

//嵌套预加载:即查用户又查用户的文章列表
var article Article
DB.Preload("User.Articles").Take(&article, 1)
//甚至可以套娃
//DB.Preload("User.Articles.User.Articles.User.Articles.User.Articles").Take(&article, 1)
fmt.Println(article)

带条件的预加载

//带条件的预加载
var user User
DB.Preload("Articles", "id = ?", 1).Take(&user, 1)
fmt.Println(user)

自定义预加载

//自定义预加载
var user User
DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
    return db.Where("id > ?", 2)
}).Take(&user)
fmt.Println(user)

删除

清除外键关系

删除用户,与将与用户关联的文章,外键设置为null

//删除
var user User
//为了Articles预加载
DB.Preload("Articles").Take(&user, 2)
//fmt.Println(user.Articles)
//删除文章上的所属用户
//DB.Model(&user).Association("Articles").Delete(user.Articles)
//删除对应用户
DB.Delete(&user)

级联删除

删除用户,与用户关联的文章也会删除,外键不是设置为null

//级联删除
//连人带所属文章一起删完
var user User
DB.Take(&user, 2)
DB.Select("Articles").Delete(&user)

一对一关系

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"
)

type User struct {
    ID       uint
    Name     string
    Age      int
    Gender   bool
    UserInfo UserInfo // 通过UserInfo可以拿到用户详情信息
}

//type UserInfo struct {
//    UserID uint // 外键
//    ID     uint
//    Addr   string
//    Like   string
//}

type UserInfo struct {
    User   *User //记得加指针,放置嵌套
    UserID uint  // 外键
    ID     uint
    Addr   string
    Like   string
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
    username := "root1"    //账号
    password := "rootroot" //密码
    host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
    port := 3306           //数据库端口
    DBname := "GoORM"      //数据库名
    timeout := "10s"       //连接超时,10秒

    mysqlLogger = logger.Default.LogMode(logger.Info)

    // root:root@tcp(127.0.0.1:3306)/gorm?
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
    //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        //跳过默认事故,它默认是false,在初始化时禁用它,这样可以获得60%的性能提升
        //SkipDefaultTransaction: true,

        //如果不想自动以蛇形复数那些默认形式生成表,就写在这里面
        NamingStrategy: schema.NamingStrategy{
            SingularTable: false, // 是否单数表名
            NoLowerCase:   true,  // 是否关闭小写转换
        },
    })
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }
    DB = db
    // 连接成功
}

func main() {
    //日志显示
    DB = DB.Session(&gorm.Session{
        Logger: mysqlLogger,
    })
    ////创建表
    //DB.AutoMigrate(&User{}, &UserInfo{})
    //
    ////创建数据
    //DB.Create(&User{
    //    Name:   "RoLingG",
    //    Age:    21,
    //    Gender: true,
    //    UserInfo: UserInfo{
    //        Addr: "广东",
    //        Like: "写代码",
    //    },
    //})
    //DB.Create(&User{
    //    Name:   "Clouwer",
    //    Age:    23,
    //    Gender: true,
    //    UserInfo: UserInfo{
    //        Addr: "广东",
    //        Like: "玩游戏",
    //    },
    //})

    //根据用户查用户详情
    //var user User
    //DB.Preload("UserInfo").Take(&user)
    //fmt.Println(&user)

    ////根据用户详情查用户
    ////要给UserInfo加上User *User
    //var userinfo UserInfo
    ////能查到了,但是是以进制码的形式,给User的UserInfo 加上指针*就能看了
    //DB.Preload("User").Take(&userinfo)
    //fmt.Println(userinfo)
    ////硬要不加也可以看,直接查某个属性即可,但这样UserInfo的内容就看不到了
    //fmt.Println(userinfo.User)

    ////删除    和一对多一样
    //var user User
    ////能删,但只能删一个user表中的数据
    //DB.Select("UserInfo").Delete(&user, 3)
    ////DB.Preload("UserInfo").Take(&user)
}

多对多关系

多对多的时候要在表属性那里添加tag:many2many

例如:

type Tag struct {
   ID       uint
   Name     string
   Articles []Article `gorm:"many2many:article_tags;"` // 用于反向引用
}

type Article struct {
   ID    uint
   Title string
   Tags  []Tag `gorm:"many2many:article_tags;"`    //many2many对应第三个表的名字,也就会用第三个表去存储前两个表之间的关系
}

添加

//添加文章,并创建标签
//DB.Create(&Article{
// Title: "GOLANG对对对",
// Tags: []Tag{
//    { Name: "GOLANG"},
//    { Name: "后端"},
// },
//})

//使用上面已有tag给予新建文章
var tags []Tag
//take也可以,但是take貌似只能最多拿一个数据,有点忘了
DB.Find(&tags, "name = ?", "后端")
DB.Create(&Article{
   Title: "Python",
   Tags:  tags,
})

查询

//查询文章,显示标签列表
var article Article
DB.Preload("Tags").Take(&article)
fmt.Println(&article)

//查询标签,显示文章列表
var tag Tag
DB.Preload("Articles").Take(&tag, 2)
fmt.Println(tag)

更新

//更新
//方法一:删除原有的tags,再添加新的
//移除文章的标签
var article Article
DB.Preload("Tags").Take(&article, 1)                        //找文章
DB.Model(&article).Association("Tags").Delete(article.Tags) //关联Tags,删除文章对应Tags
fmt.Println(article)                                        //先没tag
//添加新的tag
var tag Tag
DB.Take(&tag, 1)
DB.Model(&article).Association("Tags").Append(tag)
fmt.Println(article) //再有新的tag
//方法二:Gorm原生方法
var article Article
var tags []Tag
DB.Find(&tags, []int{2, 6, 7})

DB.Preload("Tags").Take(&article, 2)                 //找到文章2的tag
DB.Model(&article).Association("Tags").Replace(tags) //用replace进行替换
fmt.Println(article)

自定义连接表(重要)

包括了添加,更改

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"
    "time"
)

type Article struct {
    ID    uint
    Title string
    Tags  []Tag `gorm:"many2many:article_tags"`
}

type Tag struct {
    ID   uint
    Name string
    //Article []Article `gorm:"many2many:article_tags"` //反向引用,但对自定义链接表有影响
}

type ArticleTag struct {
    ArticleID uint `gorm:"primaryKey"`
    TagID     uint `gorm:"primaryKey"`
    CreatedAt time.Time
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
    username := "root1"    //账号
    password := "rootroot" //密码
    host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
    port := 3306           //数据库端口
    DBname := "GoORM"      //数据库名
    timeout := "10s"       //连接超时,10秒

    mysqlLogger = logger.Default.LogMode(logger.Info)

    // root:root@tcp(127.0.0.1:3306)/gorm?
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
    //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        //跳过默认事故,它默认是false,在初始化时禁用它,这样可以获得60%的性能提升
        //SkipDefaultTransaction: true,

        //如果不想自动以蛇形复数那些默认形式生成表,就写在这里面
        NamingStrategy: schema.NamingStrategy{
            SingularTable: false, // 是否单数表名
            NoLowerCase:   true,  // 是否关闭小写转换
        },
    })
    if err != nil {
        panic("连接数据库失败, error=" + err.Error())
    }
    DB = db
    // 连接成功
}

func main() {
    //日志显示
    DB = DB.Session(&gorm.Session{Logger: mysqlLogger})

    //自定义设置链接表
    DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) //不能注释掉,要一直放开
    // 如果tag要反向应用Article,那么也得加上(但是这种情况实战少用)
    //DB.SetupJoinTable(&Tag{}, "Articles", &ArticleTag{})
    //err := DB.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})
    //fmt.Println(err)

    //添加外键关系,直接操作第三张表,效率最高
    //DB.Create(&ArticleTag{
    //    ArticleID: 3,
    //    TagID:     3,
    //    CreatedAt: time.Time{},
    //})

    //之前的方式
    //var article Article
    //DB.Preload("Tags").Take(&article, 2)
    //fmt.Println(article)

    //添加文章并添加标签,并自动关联
    //DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) // 要设置这个,才能走到我们自定义的连接表
    //DB.Create(&Article{
    //    Title: "flask零基础入门",
    //    Tags: []Tag{
    //        {Name: "前端"},
    //        {Name: "后端"},
    //        {Name: "web"},
    //    },
    //})
    // CreatedAt time.Time 由于我们设置的是CreatedAt,gorm会自动填充当前时间,
    // 如果是其他的字段,需要使用到ArticleTag 的添加钩子 BeforeCreate

    //添加文章,关联已有tag
    //var tags []Tag
    //DB.Find(&tags, []int{4, 5})
    //DB.Create(&Article{
    //    Title: "普通基础",
    //    Tags:  tags,
    //})

    //给以有文章添加tag
    //以防没有文章先创建文章
    //article := Article{
    //    Title: "django基础",
    //}
    //DB.Create(&article)
    ////开始添加
    //var at Article
    //var tags []Tag
    //DB.Find(&tags, "name in ?", []string{"python", "web"})
    //DB.Take(&at, article.ID).Association("Tags").Append(tags)

    //给以有文章替换标签
    var articleToReplace Article
    DB.Preload("Tags").Take(&articleToReplace, "title = ?", "django基础")
    //DB.Preload("Tags").Take(&articleToReplace, 7)    //多种展示
    var tags []Tag
    DB.Find(&tags, []int{1, 7})
    //DB.Find(&tags, "name in ?", []string{"python", "web"})    //多种展示
    DB.Model(&articleToReplace).Association("Tags").Replace(tags)

    ////查询文章列表,显示标签
    //var articles []Article
    //DB.Preload("Tags").Find(&articles)
    //fmt.Println(articles)
}

自定义连接表主键

这个功能还是很有用的,例如你的文章表 可能叫ArticleModel,你的标签表可能叫TagModel

那么按照gorm默认的主键名,那就分别是ArticleModelIDTagModelID,太长了,根本就不实用

主要是要修改这两项

joinForeignKey:连接的主键id

joinReferences:关联的主键id

例子:(这次没有手动改表结构,db,err := gorm.Open(mysql.Open(dsn))用的是gorm默认结构)

package main

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "time"
)

type ArticleModel struct {
   ID    uint
   Title string
   Tags  []TagModel `gorm:"many2many:article_tags;joinForeignKey:ArticleID;JoinReferences:TagID"` //joinForeignKey:ArticleID对应的是ArticleModel的主键ID,也就是ArticleTagModel里的ArticleID
}

type TagModel struct {
   ID       uint
   Name     string
   Articles []ArticleModel `gorm:"many2many:article_tags;joinForeignKey:TagID;JoinReferences:ArticleID"`
}

type ArticleTagModel struct {
   ArticleID uint `gorm:"primaryKey"` // article_id,如果不加JoinReferences:ArticleID,那么这个本身就是ArticleModelID
   TagID     uint `gorm:"primaryKey"` // tag_id   同理
   CreatedAt time.Time
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn))
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   //日志显示
   DB = DB.Session(&gorm.Session{Logger: mysqlLogger})

   DB.SetupJoinTable(&ArticleModel{}, "Tags", &ArticleTagModel{})
   DB.SetupJoinTable(&TagModel{}, "Articles", &ArticleTagModel{})
   //err := DB.AutoMigrate(&ArticleModel{}, &TagModel{}, &ArticleTagModel{})
   //fmt.Println(err)

   //添加内容
   DB.Create(&ArticleModel{
      Title: "flask零基础入门",
      Tags: []TagModel{
         {Name: "前端"},
         {Name: "后端"},
         {Name: "web"},
      },
   })
}

操作多对多连接表

查询

如果通过一张表去操作连接表,这样会比较麻烦

比如查询某篇文章关联了哪些标签

或者是举个更通用的例子,用户和文章,某个用户在什么时候收藏了哪篇文章

无论是通过用户关联文章,还是文章关联用户都不太好查

最简单的就是直接查连接表

package main

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
   "time"
)

type UserModel struct {
   ID        uint
   Name      string
   ACollects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}

type ArticleModel struct {
   ID    uint
   Title string
   //UCollects []UserModel `gorm:"many2many:user_collect_models;joinForeignKey:ArticleID;JoinReferences:UserID"`
   // 这里也可以反向引用,根据文章查哪些用户收藏了
}

// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
   UserID    uint `gorm:"primaryKey"` // article_id
   ArticleID uint `gorm:"primaryKey"` // tag_id
   CreatedAt time.Time
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn))
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   //日志显示
   DB = DB.Session(&gorm.Session{Logger: mysqlLogger})

   DB.SetupJoinTable(&UserModel{}, "ACollects", &UserCollectModel{})
   err := DB.AutoMigrate(&UserModel{}, &ArticleModel{}, &UserCollectModel{})
   fmt.Println(err)
}

常用的操作就是根据用户查收藏的文章列表

var user UserModel
DB.Preload("ACollects").Take(&user, "name = ?", "RoLingG")
fmt.Println(user)

但是这样不太好做分页,并且也拿不到收藏文章的时间

var collects []UserCollectModel
DB.Find(&collects, "user_id = ?", 2)
fmt.Println(collects)

//结果:[{2 2 2023-11-14 15:38:01 +0800 CST}]

这样虽然可以查到用户id,文章id,收藏的时间,但是搜索只能根据用户id搜,返回也拿不到用户名,文章标题等

我们需要改一下表结构,不需要重新迁移,加一些字段

type UserModel struct {
   ID       uint
   Name     string
   Collects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}

type ArticleModel struct {
   ID    uint
   Title string
}

// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
   UserID       uint         `gorm:"primaryKey"` // article_id
   UserModel    UserModel    `gorm:"foreignKey:UserID"`
   ArticleID    uint         `gorm:"primaryKey"` // tag_id
   ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
   CreatedAt    time.Time
}
func main() {
    //日志显示
    DB = DB.Session(&gorm.Session{Logger: mysqlLogger})

    DB.SetupJoinTable(&UserModel{}, "ACollects", &UserCollectModel{})

    var collects []UserCollectModel
    var user UserModel
    DB.Take(&user, "name = ?", "RoLingG")
// 这里用map的原因是如果没查到,那就会查0值,如果是struct,则会忽略零值,全部查询
DB.Debug().Preload("UserModel").Preload("ArticleModel").Where(map[string]any{"user_id": user.ID}).Find(&collects)

    for _, collect := range collects {
        fmt.Println(collect)
    }
}

//结果:
//{1 {1 RoLingG []} 1 {1 Golang基础} 2023-11-14 15:49:10 +0800 CST}
//{1 {1 RoLingG []} 2 {2 Python基础} 2023-11-14 15:49:18 +0800 CST}

自定义数据类型

很多情况下我们存储到数据库中的数据是多变的

例如我需要存储json或者是数组

然后很多数据库并不能直接存储这些数据类型,我们就需要自定义数据类型

自定义的数据类型必须实现 Scanner Valuer 接口(因为算高级数据类型 ),以便让 GORM 知道如何将该类型接收、保存到数据库

gorm中自定义数据类型无外乎就两个方法

在数据入库的时候要转换为什么数据,已经出库的时候数据变成什么样子

存储json

存储json可能是经常使用到的

我们需要定义一个结构体,在入库的时候,把它转换为[]byte类型,查询的时候把它转换为结构体

package main

import (
   "database/sql/driver"
   "encoding/json"
   "errors"
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
)

type Info struct {
   Status string `json:"status"`
   Addr   string `json:"addr"`
   Age    int    `json:"age"`
}

// Scan 从数据库中读取出来    从这往下基本是通用写法,不通用的就是结构体
func (i *Info) Scan(value interface{}) error {
   bytes, ok := value.([]byte)
   if !ok {
      return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
   }
   err := json.Unmarshal(bytes, i)
   return err
}

// Value 存入数据库
func (i Info) Value() (driver.Value, error) {
   return json.Marshal(i)
}

type AuthModel struct {
   ID   uint
   Name string
   Info Info `gorm:"type:string"`
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn))
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   DB.AutoMigrate(&AuthModel{})
}

插入数据

//添加信息
DB.Create(&AuthModel{
   Name: "RoLingG",
   Info: Info{
      Status: "success",
      Addr:   "广东惠州",
      Age:    21,
   },
})

查询信息

//查询信息
var auth AuthModel
DB.Take(&auth, "name = ?", "RoLingG")
fmt.Println(auth)

存储数组

很多时候存储数组也是很常见的

最简单的方式就是存json

(偷懒了,在上面的代码基础上做了点变化,就介样了)

package main

import (
   "database/sql/driver"
   "encoding/json"
   "errors"
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
   "gorm.io/gorm/logger"
)

type Array []string

type HostModel struct {
   ID    uint   `json:"id"`
   IP    string `json:"ip"`
   Ports Array  `gorm:"type:string" json:"ports"`
}

// Scan 从数据库中读取出来
func (arr *Array) Scan(value interface{}) error {
   bytes, ok := value.([]byte)
   if !ok {
      return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
   }
   err := json.Unmarshal(bytes, arr)
   return err
}

// Value 存入数据库
func (arr Array) Value() (driver.Value, error) {
   return json.Marshal(arr)
}

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
   username := "root1"    //账号
   password := "rootroot" //密码
   host := "127.0.0.1"    //数据库地址,可以是Ip或者域名
   port := 3306           //数据库端口
   DBname := "GoORM"      //数据库名
   timeout := "10s"       //连接超时,10秒

   mysqlLogger = logger.Default.LogMode(logger.Info)

   // root:root@tcp(127.0.0.1:3306)/gorm?
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, DBname, timeout)
   //连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
   db, err := gorm.Open(mysql.Open(dsn))
   if err != nil {
      panic("连接数据库失败, error=" + err.Error())
   }
   DB = db
   // 连接成功
}

func main() {
   //日志显示
   DB = DB.Session(&gorm.Session{Logger: mysqlLogger})

   //创建数据库
   //DB.AutoMigrate(&HostModel{})

   //添加信息
   DB.Create(&HostModel{
    IP:    "192.168.1.1",
    Ports: []string{"80", "8080"},
   })

   ////查询信息
   var host HostModel
   DB.Take(&host, 1)
   fmt.Println(host)
}

当然,也可以用字符串拼接,例如 |=(,和=分隔要注意,url里面也有这些字符,可能会有问题)

type Array []string
// Scan 从数据库中读取出来
func (arr *Array) Scan(value interface{}) error {
    data, ok := value.([]byte)
    if !ok {
        return errors.New(fmt.Sprintf("解析失败: %v %T", value, value))
    }
    *arr = strings.Split(string(data), "|")
    return nil
}
// Value 存入数据库
func (arr Array) Value() (driver.Value, error) {
    return strings.Join(arr, "|"), nil
}

注意:拼接的字符串不能是输入字符串中存在的

PREV
[Golang]Gorm框架学习日志
NEXT
[Golang]Gorm框架 枚举

评论(0)

发布评论