GORM数据库编程

Keywords: #技术 #Golang #GORM
Release Date: 2025-03-04
Table of Contents

GORM 官方文档基础入门学习笔记

什么是 ORM

ORM 全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

传统通过 SQL 语句来操作数据库,需要我们有足够的 SQL 语句基础,并且不同数据库有不同的 SQL 语言,有一定的门槛。然而 ORM 可以帮我们隔离 SQL 语言,操作数据库时不需要编写对应的 SQL 语言,只需要面向对象编程即可。

针对 Java、Python 等面向对象的编程语言来说,ORM 就是将一张表映射成一个类,表中的列映射成类中的一个子类(类中套类)。但是对于 Go 语言而言,表不是映射为一个类而是一个结构体(struct),表中的列可以映射成 struct 中不同的类型,同时可以使用 struct 中的 tag 来描述数据库中列的各种属性。

Go 语言中常用的 ORM 框架有以下这些:

我们不用太去纠结应该选择哪一个 ORM 框架,熟悉了其中一个,其他的 orm 迁移成本很低,我们就选择一个 star 数量最高的,不会有出错的框架就行了,它们之间整体差异不会很大。我们更应该关注 SQL 语言本身,它远比 ORM 框架要重要的多。

ORM 有何优缺点?

优点:

  • 提高了开发效率。
  • 屏蔽 SQL 细节。可以自动对实体 Entity 对象与数据库中的 Table 进行字段与属性的映射;不用直接 SQL 编码,屏蔽各种数据库之间的差异。

缺点:

  • ORM 会牺牲程序的执行效率和会固定思维模式
  • 太过依赖 ORM 会导致 SQL 理解不够
  • 对于固定的 ORM 依赖过重,导致切换到其他的 ORM 代价高

我们需要以 SQL 为主,ORM 为辅。ORM 主要目的是为了增加代码可维护性和开发效率。对于 SQL 我们一定要学好:

  • group by
  • 子查询
  • having 子句

连接数据库

GORM 官方文档,其官方文档写的十分详细和规范,可以方便我们自行学习。本学习笔记将使用 MySQL 数据库进行演示。

示例代码:

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

注意:想要正确的处理 time.Time ,您需要带上 parseTime 参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4 查看 此文章 获取详情。

MySQL 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:

db, err := gorm.Open(mysql.New(mysql.Config{
    // DSN data source name
    DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", 
    // string 类型字段的默认长度
    DefaultStringSize: 256, 
    // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
    DisableDatetimePrecision: true, 
    // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
    DontSupportRenameIndex: true, 
    // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
    DontSupportRenameColumn: true, 
    // 根据当前 MySQL 版本自动配置
    SkipInitializeWithVersion: false, 
}), &gorm.Config{})

创建数据表

根据 GORM 文档中的说明,GORM 有一个默认的 logger 实现,我们将设置一个全局的 logger,用于将我们后续执行操作的 SQL 语言都打印出来,边学习 GORM 的同时,学习其 SQL 语句的写法。搞清楚 GORM 各个 API 背后真正执行的 SQL 语句是什么。

配置 logger,并在连接时通过配置参数传入。

newLogger := logger.New(
    // io writer
    log.New(os.Stdout, "\r\n", log.LstdFlags), 
    logger.Config{
        SlowThreshold: time.Second, // Slow SQL threshold
        LogLevel: logger.Info, // Log level
        // Ignore ErrRecordNotFound error for logger
        IgnoreRecordNotFoundError: true, 
        ParameterizedQueries: true, // Don't include params in the SQL log
        Colorful: true, // Enable color
  },
)
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: newLogger,
})

接着我们就可以快速入门 GORM 的写法,首先定义一个表结构,将表结构直接生成对应的 MySQL 表,这也是 migrations 功能。

定义表结构体:

type Product struct {
    gorm.Model
    Code sql.NullString
    Price uint
}

gorm.Model 是 GORM 内置的一个结构体,存放着数据库表中常用的字段,如 id、创建时间、删除时间、更新时间等,我们可以直接拿来使用,作为表结构体的一部分。

type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

在使用之前不要忘了现在数据库中创建一个 gorm_test 的数据库,选择 utf8mb4 字符集,排序规则选择 utf8mb4_general_ci

接着,我们使用 _ = db.AutoMigrate(&Product{}) 语句就可以自动在数据库中创建一个空表。由于这段代码会执行多条 SQL 语句。如果设置 logger 正确,会输出执行了什么 SQL 语句。

整体代码如下:

type Product struct {
    gorm.Model
    Code  sql.NullString
    Price uint
}

func main() {
    dsn := "root:root@tcp(192.168.0.104:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), 
        logger.Config{
            SlowThreshold: time.Second,   
            LogLevel: logger.Info,
            Colorful: true, 
        },
    )

    // 全局模式
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        panic(err)
    }
    
    _ = db.AutoMigrate(&Product{}) //此处会有sql语句
}

输出的 SQL 语句:

SELECT DATABASE()

SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gorm_test%' ORDER BY SCHEMA_NAME='gorm_test' DESC,SCHEMA_NAME limit 1

SELECT count(*) FROM information_schema.tables WHERE table_schema = 'gorm_test' AND table_name = 'products' AND table_type = 'BASE TABLE'

CREATE TABLE `products` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`code` longtext,`price` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_products_deleted_at` (`deleted_at`))

db.AutoMigrate() 可以同时传入两个结构体,同时生成两个数据表,包括关联表。

数据的增删改查

接着我们可以尝试编写增删改查的语句。

// 新增
db.Create(&Product{Code: sql.NullString{"D42", true}, Price: 100})

// 读取
var product Product
db.First(&product, 1) // 根据整形主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

// 更新 - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)

// 更新 - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code:sql.NullString{"", true}}) // 仅更新非零值字段
// 如果我们去更新一个product 只设置了price:200
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

// Delete - 删除 product
db.Delete(&product, 1) // 并没有执行delete语句 执行了update语句 逻辑删除 添加删除时间

以上 Go 代码转换成的 SQL 语句如下,我们可以发现 GORM 自动为我们加上了很多需求和限制,比如自动更改了数据更新时间,自动加上限制 1 条,使用逻辑删除(所有删除语句都不是真的删除数据而是使用逻辑删除,设置删除标志)等等。

INSERT INTO `products` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES ('2025-03-03 19:52:46.757','2025-03-03 19:52:46.757',NULL,'D42',100)

SELECT * FROM `products` WHERE `products`.`id` = 1 AND `products`.`deleted_at` IS NULL ORDER BY `products`.`id` LIMIT 1

SELECT * FROM `products` WHERE code = 'D42' AND `products`.`deleted_at` IS NULL AND `products`.`id` = 1 ORDER BY `products`.`id` 
LIMIT 1

UPDATE `products` SET `price`=200,`updated_at`='2025-03-03 19:52:46.78' WHERE `products`.`deleted_at` IS NULL AND `id` = 1       

UPDATE `products` SET `updated_at`='2025-03-03 19:52:46.784',`code`='F42',`price`=200 WHERE `products`.`deleted_at` IS NULL AND `id` = 1

UPDATE `products` SET `code`='F42',`price`=200,`updated_at`='2025-03-03 19:52:46.787' WHERE `products`.`deleted_at` IS NULL AND `id` = 1

UPDATE `products` SET `deleted_at`='2025-03-03 19:52:46.791' WHERE `products`.`id` = 1 AND `products`.`id` = 1 AND `products`.`deleted_at` IS NULL

零值更新问题

在上面的更新语句中,我们了解到 updates 语句只能更新非零值的字段, 而 update 语句则不受影响。为什么需要这样?因为 updates 需要传入 struct 或者 map[string]interface{} 参数,如果使用 struct 更新时,比如在 product 结构体中只传入了 Price 的值,那么其他值就会是默认值(Golang 语法决定的),这样在后续数据库操作过程中可能就会存在问题。

因此,在 ORM 框架中,使用 updates 方法通过传入 struct 的方式进行更新操作时,只能更新非零值字段,其他默认零值的字段传入,将会被忽略,不会出现在最后的 SQL 语句中。具体介绍见官方文档:更新 | GORM

我们将上述语句改成以下的代码,那么 price 和 code 都不会被更新,因为它们都是各自属性的零值 (uint 类型零值为 0,string 类型零值为空字符串)。

db.Model(&product).Updates(Product{Price: 0, Code:""})

那么如果我们需要将零值更新进数据库怎么办?就可以像上一节中的代码一样,将可能传入零值的字段定义为 sql.NullString 这个类型。它通过 Valid 这个变量来指定这个字符串是否为空。

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

如果将 Valid 设置为 true,就可以将零值(如 code 的空字符串 "")通过 SQL 语句更新到数据库中。

db.Model(&product).Updates(Product{Price: 200, Code:sql.NullString{"", true}})

除了 NullString 这个类型外,还有 NullBoolNullFloat64NullInt32NullInt64NullTime 这些类型,因为这些类型都有零值。使用它们时需要引入 database/sql 包。

还有一种方式可以使零值也能更新,就是将类型设置为指针类型,传入参数时需要传入结构体的指针。比如以下这种方式,在最终的 SQL 语句中也会设置 email 为零值。

type user struct {
    email *string
}

empty := ""
db.Model(&User{ID:1}).Updates(User{Email: &empty})

总结一下,如果想要在数据库中写入零值,那么对应的字段类型就需要使用以上这几种 NUll~ 类型,或者使用该类型的指针类型。

  1. sql.NullString
  2. *string

表结构定义的细节

模型定义 | GORM

在使用 GORM 时,会有一些默认的约定:

  1. 主键:GORM 使用一个名为 ID 的字段作为每个模型的默认主键。
  2. 表名:默认情况下,GORM 将结构体名称转换为 snake_case (蛇形命名)并为表名加上复数形式。例如,一个 Product 结构体在数据库中的表名变为 products 。
  3. 列名:GORM 自动将结构体字段名称转换为 snake_case (蛇形命令)作为数据库中的列名。例如创建时间 CreatedAt 在数据库的列中变为 created_at
  4. 时间戳字段:GORM 使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。

字段标签

通过设置结构体的方式定义一个数据库表时,我们可以通过结构体各个字段的 tag 来对各个字段进行约束。这叫做字段标签,多个字段标签用 ; 隔开,比如以下例子:

type User struct {
    UserID        uint `gorm:"primarykey"`
    Name string `gorm:"column:user_name;type:varchar(50);index:idx_user_name;unique;default:'bobby'"`
}

我们将 UserID 设置为主键;将 Name 在数据表中的列名设置为 user_name,将其类型设置为数据库中的 varchar(50) 定长字符,创建索引 idx_user_name,并定义成唯一键,最后将默认值设为 bobby

这些都是数据库中的知识点,一定要学好数据库,否则难以理解。

创建记录

创建 | GORM

user := User{Name: "liuchao", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

db.Create() 这条语句执行之前,User 这个结构体中是没有 ID 这个字段的,执行 SQL 语句后,会设置一个 ID 值(作为主键),并将其写入 User 中,这也是为什么需要传入 User 的地址,因为要对这个对象进行改动。如果在 User 中已经通过 tag 来设置主键后,就不会自动添加 ID 字段了。

我们还可以使用 Create() 创建多项记录,但是这样使用并不常见,最常用的方式是下方的批量插入的方式。

users := []*User{  
    {Name: "liuchao", Age: 18, Birthday: time.Now()},  
    {Name: "Jackson", Age: 19, Birthday: time.Now()},  
}  
  
result := db.Create(users) // pass a slice to insert multiple row  
  
result.Error        // returns error  
result.RowsAffected // returns inserted records count

如果我们有大量的数据需要插入,那么我们就需要用到批量插入了。

要高效地插入大量记录,需要切片传递给 Create 方法。 GORM 将生成一条 SQL 来插入所有数据(因此性能较好),以返回所有主键值,并触发 Hook 方法。当这些记录可以被分割成多个批次时,GORM 会开启一个事务 </0> 来处理它们。

var users = []User{{Name: "bobby1"}, {Name: "bobby2"}, {Name: "bobby3"}}
db.Create(&users)

for _, user := range users {
  user.ID // 1,2,3
}

我们可以通过 db.CreateInBatches 方法来指定批量插入的批次大小。比如有 10000 条数据需要插入,批次大小设置为 100,那么 GORM 就会生成 100 条 SQL 语句,分别通过 100 次将这 1000 条数据插入数据库中。

var users = []User{{Name: "bobby_1"}, ...., {Name: "bobby_10000"}}

// batch size 100
db.CreateInBatches(users, 100)

为什么要分批次呢?因为 SQL 语句有长度限制,太长的 SQL 语句不能被执行,分批次插入的方式在大数据量的情况下更常见。

还有一种更简便的插入方式是通过 map 类型来插入,不创建结构体实例,直接传入一个 map 即可,比如下方这个例子。

// 用 `[]map[string]interface{}` 的方式单行插入
db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// 用 `[]map[string]interface{}{}` 的方式多行插入
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

map 方式创建更灵活,struct 方式创建更好维护。各有优缺点。

最后还有一种关联创建,一条创建语句同时可以创建两个表记录。

type CreditCard struct {
  gorm.Model
  Number   string
  UserID   uint
}

type User struct {
  gorm.Model
  Name       string
  CreditCard CreditCard
}

db.Create(&User{
  Name: "liuchao",
  CreditCard: CreditCard{Number: "123456"},
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...

同时 GORM 会为 CreditCard 中的 UserID 自动创建一个外键,关联到 User 中的 ID 字段。

这是因为 UserID 是一个 uint 类型的字段,它的命名符合 GORM 的外键命名规则(即 <struct name in singular form>_id)。GORM 会自动识别这个字段为外键,并将其与 User 表的主键 id 关联起来。

其 SQL 语句为:

INSERT INTO `credit_cards` (`created_at`,`updated_at`,`deleted_at`,`number`,`user_id`) VALUES ('2025-03-03 21:03:34.475','2025-03-03 21:03:34.475',NULL,'123456',1) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2025-03-03 21:03:34.472','2025-03-03 21:03:34.472',NULL,'liuchao')

查询记录

查询 | GORM

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误。

// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

errors.Is() 方法可以用于准确判断两者是否是同一种错误类型。

如果想避免 ErrRecordNotFound 错误,你可以使用 Find,比如 db.Limit(1).Find(&user)Find 方法可以接受 struct 和 slice的数据。

也可以通过主键查询。主键查询时不用指定字段名,默认就为主键查询

如果主键是数字类型,您可以使用 内联条件 来检索对象。当使用字符串时,需要额外的注意来避免 SQL 注入;查看 Security 部分来了解详情。

db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

如果主键是字符串 (例如像 uuid),查询将被写成如下:

db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

这种方式实际上也能够代替下方的 Where() 语句,但是不太常用,First Find Last 都可以。

当目标对象有一个主键值时,将使用主键构建查询条件,例如:

var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

如果需要查询全部对象,则需要使用 Find() 方法。

// Get all records
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

在实际项目中,最常用的还是使用条件查询 Where()

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

我们通过传入结构体 &users 来指定需要查询的表名,同时需要注意 GORM 中的字段名是大小写不敏感的,也就是说 db.Where("Name = ?", "jinzhu").First(&user) 也是可以正确执行的。

请注意!db.Where("") 引号中一点要填入数据库表中的字段名,而不是代码结构体中的字段名。比如,有以下结构体

type User struct {
    MyName string `gorm:"column:name`
}

则需要使用以下方式,才能被正确执行。

db.Where("name = ?", "jinzhu").First(&user)

如果不想使用数据表字段的名称,而是使用结构体中的名称,这时就需要使用到 struct 和 map 作为查询条件了,可以屏蔽具体的数据库细节。比如以下这几种方式,我们更建议使用这种方式,当这种情况不能满足需求时,再使用上面的方式,更加灵活,更接近于直接写 SQL 语句

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

使用 struct 时,GORM 是不会处理零值的(忽略零值),也就是如果字段为零值,那么将不会在最终的 SQL 语句中被执行,而 map 方式则不会受到影响。比如以下示例:

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

除此之外,还有许多的查询语句,具体可以见官方文档,同时一定要学好数据库,其中 group byhaving 和子查询等十分重要。

总结一下,常用的查询方法:

  1. string,灵活但不常用,接近 SQL 语句
  2. struct,推荐使用
  3. map,解决零值问题

Find() 可能会返回多条记录,因此需要使用 []User 类型来接收,而 First()Last() 都只会返回单条记录,因此可以使用 User 类型来接收。

更新记录

更新 | GORM

在上方我们已经学习过零值更新问题,实际上还有一个 Sava() 方法会保存所有的字段,即使字段是零值。

db.First(&user)

user.Name = "jinzhu 2"
user.Age = 100
db.Save (&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

Save () 是一个组合函数。如果保存值不包含主键,它将执行 Create(创建新记录),否则它将执行 Update (包含所有字段,其它字段自动设为零值)。所以如果不想将所有字段都修改,那么不推荐使用 Save ()

db.Save (&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu", 100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save (&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

需要注意,不要将 Save () 和 Model () 一同使用, 这是未定义的行为。因为 Save () 已经可以通过传入的结构体名称来指定是哪个数据表,比如 Save (&user) 就是修改 user 表,这跟上方的查询语句 Find ()First () 等语句一样。

但是对于接下来的 Update ()Updates () 方法,它们没有可以传递结构体名的地方,因此需要用到 Model () 来指定更新哪个数据表的记录。

更新单个列。

当使用 Update 更新单列时,需要有一些条件,否则将会引起ErrMissingWhereClause 错误,查看 阻止全局更新 了解详情。当使用 Model 方法,并且它有主键值时,主键将会被用于构建条件,例如:

// 根据条件更新
db.Model (&User{}). Where ("active = ?", true). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User 的 ID 是 `111`
db.Model (&user). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据条件和 model 的值进行更新
db.Model (&user). Where ("active = ?", true). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

如果传入的 user 结构体中有主键信息,则会自动作为 SQL 语句的条件,如果没有任何数据信息,则会通过 Where () 中的条件进行查找更新。

更新多个列。

Updates 方法支持 struct 和 map[string]interface{} 参数。当使用 struct 更新时,默认情况下 GORM 只会更新非零值的字段

// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model (&user). Updates (User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 根据 `map` 更新属性
db.Model (&user). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

再次注意,使用 struct 更新时, GORM 将只更新非零值字段。解决方式是用 map 来更新属性,或者使用 Select 声明字段来更新

如果您想要在更新时选择、忽略某些字段,您可以使用 SelectOmit

// 选择 Map 的字段
// User 的 ID 是 `111`:
db.Model (&user). Select ("name"). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model (&user). Omit ("name"). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 选择 Struct 的字段(会选中零值的字段)
db.Model (&user). Select ("Name", "Age"). Updates (User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// 选择所有字段(选择包括零值字段的所有字段)
db.Model (&user). Select ("*"). Updates (User{Name: "jinzhu", Role: "admin", Age: 0})

// 选择除 Role 外的所有字段(包括零值字段的所有字段)
db.Model (&user). Select ("*"). Omit ("Role"). Updates (User{Name: "jinzhu", Role: "admin", Age: 0})

删除记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:

// Email 的 ID 是 `10`
db.Delete (&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where ("name = ?", "jinzhu"). Delete (&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,这与查询-内联条件(Query Inline Conditions) 是类似的。

db.Delete (&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete (&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete (&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);

更为重要的是软删除功能,即逻辑删除(不删除表数据,只是通过删除时间来标记该条记录已被删除)。

如果你的模型包含了 gorm. DeletedAt字段(该字段也被包含在gorm. Model中),那么该模型将会自动获得软删除的能力!

当调用 Delete 时,GORM 并不会从数据库中删除该记录,而是将该记录的 DeleteAt 设置为当前时间,而后的一般查询方法也将无法查找到此条记录。

// user's ID is `111`
db.Delete (&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// Batch Delete
db.Where ("age = ?", 20). Delete (&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// Soft deleted records will be ignored when querying
db.Where ("age = 20"). Find (&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

如果你并不想嵌套gorm. Model,你也可以像下方例子那样开启软删除特性:

type User struct {
  ID      int
  Deleted gorm. DeletedAt
  Name    string
}

如果不想使用软删除,而是使用硬删除,即永久删除。可以使用 Unscoped 来永久删除匹配的记录。

db.Unscoped (). Delete (&order)
// DELETE FROM orders WHERE id=10;

无论更新、删除都可以先通过查询记录的方式拿到 user(单个对象)或者 users(对象列表),再将其传入 Model () 或者 Delete () 中来确定需要更新或者删除的数据范围。

Belong To

Belongs To | GORM

创建记录中的关联创建中,我们已经了解过如何将两个表通过外键联系。除此之外,还有很多种方式进行表的关联。

belongs to 会与另一个模型建立了一对一的连接。这种模型的每一个实例都“属于”另一个模型的一个实例。

例如,您的应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。下面的类型就表示这种关系。注意,在 User 对象中,有一个和 Company 一样的 CompanyID。默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系,因此必须包含在 User 结构体中才能填充 Company 内部结构体。

CompanyID intCompany Company 才可以共同表示一个外键,Company Company 没有实际作用,只是指明了外键连接的是哪张表。

在 User 表中创建记录时,如果不传入 company 字段则会报错(因为有外键),因此可以使用关联创建。GORM 会自动找到两个表对应的记录,并关联起来。

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm. Model
  Name      string
  CompanyID int
  Company   Company
}

type Company struct {
  ID   int
  Name string
} 

要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字

例如,定义一个 User 实体属于 Company 实体,那么外键的名字一般使用 CompanyID。

GORM 同时提供自定义外键名字的方式,如下例所示。

type User struct {
  gorm. Model
  Name         string
  CompanyRefer int
  Company      Company `gorm: "foreignKey: CompanyRefer"`
  // 使用 CompanyRefer 作为外键
}

type Company struct {
  ID   int
  Name string
}

对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。如果不想默认使用主键作为外键,也可以使用标签 references 来更改它,例如:

type User struct {
  gorm. Model
  Name      string
  CompanyID string
  Company   Company `gorm: "references: Code"` // 使用 Code 作为引用
}

type Company struct {
  ID   int
  Code string
  Name string
}

如何解决关联查询的问题,如何同时查询两个表?

如果对一个关联的表使用之前的查询语句去查询两个表的内容,比如以下情况:

var user User
db.First (&user)
fmt.Println (user. Name, user. Company. Name)

则只会输出 user. Name 的值,而 user. Company. Name 的值为零值。这是因为 user. Company. Name 的数据属于另外一个表 company,使用以上方式查询记录,在 SQL 语句中指定了数据表为 usersSELECT * FROM users WHERE ……,所以查询不到数据。

如果想查询出两个表的数据,需要使用 PreloadJoins 预加载 belongs to 关联的记录,查看 预加载 获取详情。

创建多条记录。

db.Create (&[]User{
    {Name: "liuchao 1", Company: Company{Name: "company 1"}},
    {Name: "liuchao 2", Company: Company{Name: "company 2"}},
    {Name: "liuchao 3", Company: Company{Name: "company 3"}},
})

使用 Preload 方法查询。

var user User
db.Preload ("Company"). First (&user)
fmt.Println (user. Name, user. Company. Name)

使用 Joins 方法查询。

var user User
db.Joins ("Company"). First (&user)
fmt.Println (user. Name, user. Company. Name)

两者的区别在于 Preload 方法会执行多条 SQL 语句,而 Joins 方法只会执行一条 SQL 语句(使用 JOIN 操作 )。

Has Many

Has Many | GORM

has many 与另一个模型建立了一对多的连接。不同于 has one,拥有者可以有零或多个关联模型。

例如,您的应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card。

// User 有多张 CreditCard,UserID 是外键
type User struct {
  gorm. Model
  Name string
  CreditCards []CreditCard
}

type CreditCard struct {
  gorm. Model
  Number string
  UserID uint
}

与 Belong To 类型不同,此时的外键必须设置在 CreditCard 中,这是因为只有一张信用卡才能唯一对应一个用户,反之则不行。

要定义 has many 关系,同样必须存在外键。默认的外键名是拥有者的类型名加上其主键字段名

例如,要定义一个属于 User 的模型,则其外键应该是 UserID

此外,想要使用另一个字段作为外键,您可以使用 foreignKey 标签自定义它:

type User struct {
  gorm. Model
  CreditCards []CreditCard `gorm: "foreignKey: UserRefer"`
}

type CreditCard struct {
  gorm. Model
  Number    string
  UserRefer uint
}

GORM 通常使用拥有者的主键作为外键的值。对于上面的例子,它是 User 的 ID 字段。

为 user 添加 credit card 时,GORM 会将 user 的 ID 字段保存到 credit card 的 UserID 字段。

同样的,您也可以使用标签 references 来更改它,例如:

type User struct {
  gorm. Model
  MemberNumber string
  CreditCards  []CreditCard `gorm: "foreignKey: UserNumber; references: MemberNumber"`
}

type CreditCard struct {
  gorm. Model
  Number     string
  UserNumber string
}

在大型的系统、高并发的系统中一般不使用外键约束,因为外键约束对数据库性能有很大的影响,所以一般是在业务层面保证数据的一致性。同时外键约束也有很大的优点:,可以保持数据的完整性,即使是业务代码考虑不严谨。

创建测试记录。

db.Create (&[]User{
    {Name: "liuchao 1", CreditCart: []CreditCart {{Number: "123456"}, {Number: "654321"}} },
    {Name: "liuchao 2", CreditCart: []CreditCart {{Number: "abcdef"}, {Number: "fedcba"}} },
    {Name: "liuchao 3", CreditCart: []CreditCart {{Number: "qwerty"}, {Number: "ytrewq"}} },
})

查询两个表的数据,同样可以使用 Preload 方法,但不能使用 Joins 方法(会报错)。

var user User
db.Preload ("CreditCards"). First (&user)
for _, card := range user. CreditCards{
    fmt.Println (card. Number)
}

为什么不能使用 Joins 方法?

在 Belongs To 关系中,主表(如 User)通过外键(如 CompanyID)指向从表(如 Company)。这种关系的特点是:

  • 主表包含外键:User 表中有一个外键 CompanyID,指向 Company 表的主键。
  • 一对一关系:每个 User 只能关联一个 Company

但是在 Has Many 关系中,主表(如 User)可以关联多个从表记录(如 CreditCart)。这种关系的特点是:

  • 从表包含外键:CreditCart 表中有一个外键 UserID,指向 User 表的主键。
  • 一对多关系:一个 User 可以关联多个 CreditCart

Joins ("CreditCard")会生成类似这样的 SQL:

SELECT * FROM users 
JOIN credit_cards ON users. id = credit_cards. user_id

对于有多个 CreditCard 的用户,会产生多条记录(每个 CreditCard 对应一行)。而 GORM 的 First () 方法期望只处理单条记录,无法将多行 JOIN 结果正确映射到切片字段,导致反射错误。

Joins ("Company")生成的 SQL:

SELECT * FROM users 
JOIN companies ON users. company_id = companies. id

由于是一对一关系,JOIN 结果始终只有单行,可以完美映射到结构体的单个嵌套对象。

Preload ("CreditCards")会执行两条 SQL:

SELECT * FROM users; -- 先查询主表
SELECT * FROM credit_cards WHERE user_id IN (...); -- 再批量查询关联表

这种方式将关联数据以切片形式正确填充,避免了 JOIN 带来的行数膨胀问题。

当尝试用Joins ()加载 Has Many 关联时,GORM 的反射机制试图将多行 JOIN 结果映射到切片字段,但由于First ()只取第一条记录,导致:

  • 反射试图操作切片值时出现类型不匹配
  • 最终抛出 reflect: call of reflect. Value. Field on slice Value

所以

  • 对于Has Many关系,坚持使用 Preload ()
  • 对于Belongs To/Has One关系,可以安全使用 Preload ()Joins ()
  • 对于Many To Many关系,需要使用 Association ()

Many To Many

Many To Many | GORM

Many To Many 多对多的关系会在很多实际开发的场景被用到。

Many to Many 会在两个 model 中添加一张连接表。

例如,您的应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language。

// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm. Model
  Name string
  Languages []Language `gorm: "many 2 many: user_languages;"`
}

type Language struct {
  gorm. Model
  Name string
}
// 连接表:user_languages  
//   foreign key: user_id, reference: users. id  
//   foreign key: language_id, reference: languages. id

当使用 GORM 的 AutoMigrate 为 User 创建表时,GORM 会自动创建连接表。

对于 many 2 many 关系,连接表会同时拥有两个模型的外键,若要重写它们,可以使用标签 foreignKeyreferencesjoinforeignKeyjoinReferences。当然,您不需要使用全部的标签,你可以仅使用其中的一个重写部分的外键、引用。

type User struct {
    gorm. Model
    Profiles []Profile `gorm: "many 2 many: user_profiles; foreignKey: Refer; joinForeignKey: UserReferID; References: UserRefer; joinReferences: ProfileRefer"`
    Refer    uint      `gorm: "index:, unique"`
}

type Profile struct {
    gorm. Model
    Name      string
    UserRefer uint `gorm: "index:, unique"`
}

// 连接表:user_profiles
//   foreign key: user_refer_id, reference: users. refer
//   foreign key: profile_refer, reference: profiles. user_refer

创建测试记录。

db.Create (&[]User{
    {Name: "liuchao 1", Languages: []Language {{Name: "chinese"}, {Name: "english"}} },
    {Name: "liuchao 2", Languages: []Language {{Name: "chinese"}, {Name: "japanese"}} },
    {Name: "liuchao 3", Languages: []Language {{Name: "chinese"}, {Name: "korean"}} },
    {Name: "liuchao 4", Languages: []Language {{Name: "english"}, {Name: "french"}} },
})

如果直接使用以上方式创建记录,那么 language 表中就会出现如下图所示的重复数据,这是因为每次创建用户时都直接新建了 Language 实例,而没有检测数据库中是否已存在同名语言记录(GORM 不会自动检测)。

image.png

第一种解决方式是在插入记录之前,先使用 FirstOrCreate 检查是否有同名的 language,如果有就不会创建新数据,直接使用数据表中的已有数据。

type User struct {
    gorm.Model
    Name      string
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Name string `gorm:"index:,unique"` // 关键点:唯一约束
}

func main() {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    db.AutoMigrate(&User{}, &Language{})

    // 批量创建用户
    users := []User{
        {Name: "liuchao1", Languages: getOrCreateLanguages(db, "chinese", "english")},
        {Name: "liuchao2", Languages: getOrCreateLanguages(db, "chinese", "japanese")},
        {Name: "liuchao3", Languages: getOrCreateLanguages(db, "chinese", "korean")},
        {Name: "liuchao4", Languages: getOrCreateLanguages(db, "english", "french")},
    }
    db.Create(&users)
}

func getOrCreateLanguages(db *gorm.DB, names ...string) []Language {
    var languages []Language
    for _, name := range names {
        var lang Language
        db.FirstOrCreate(&lang, Language{Name: name}) // 关键点:查询或创建
        languages = append(languages, lang)
    }
    return languages
}

第二种解决方式就是在第一种方法的基础上使用 BeforeCreate 钩子,Before Create

type User struct {
    gorm.Model
    Name      string
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Name string `gorm:"index:,unique"` // 仍然建议加唯一约束
}

// BeforeCreate 钩子:自动处理语言记录的唯一性
func (u *User) BeforeCreate(tx *gorm.DB) error {
    for i := range u.Languages {
        lang := &u.Languages[i]
        if err := tx.Where(Language{Name: lang.Name}).FirstOrCreate(lang).Error; err != nil {
            return err
        }
    }
    return nil
}

func main() {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    db.AutoMigrate(&User{}, &Language{})

    // 直接创建用户,无需手动处理语言
    db.Create(&[]User{
        {Name: "liuchao1", Languages: []Language{{Name: "chinese"}, {Name: "english"}}},
        {Name: "liuchao2", Languages: []Language{{Name: "chinese"}, {Name: "japanese"}}},
        {Name: "liuchao3", Languages: []Language{{Name: "chinese"}, {Name: "korean"}}},
        {Name: "liuchao4", Languages: []Language{{Name: "english"}, {Name: "french"}}},
    })
}

两种方式都是相同的原理,都能够实现下图的效果。

image.png

查询两个表中的数据,同样只能使用 Preload ,而不能使用 Joins 方法。原理与 Has Many 相同。

var user User
// 查找第一个user的languages
db.Preload("Languages").First(&user)
for _, language := range user.Languages{
    fmt.Println(language.Name)
}

如果我已经取出一个 user,但是这个 user 我们之前没有使用 Preload 来加载对应的 Languages,即只有 usergorm.Model 字段(如使用 db.First(&user) 单独对 user 表查询 ),那么可以使用 Association,比如:

var user User
db.First(&user) // 只有user的数据,没有language的数据
var laguages []Language
_ = db.Model(&user).Association("Languages").Find(&languages)
for _, language := range laguages{
    fmt.Println(language.Name)
}

自定义表名

前面我们都是使用 GORM 自动根据结构体创建的蛇形表名,实际上表名也可以自定义。自定义表名通常有两种情况:

  1. 我们自己定义表名是什么
  2. 统一的给所有的表名加上一个前缀

在 GORM 中可以通过给某一个 struct 添加 TableName 方法来自定义表名:

func (User) TableName() string{
    return "my_user"
}

如果想统一给所有表名都加上前缀,则可以在打开数据库时传入 NamingStrategy 参数:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    NamingStrategy:schema.NamingStrategy{
        TablePrefix: "mxshop_",
    },
    Logger: newLogger,
})

需要注意,TableNameNamingStrategy 不能同时生效,TableName 优先级更大。

Before Create

Hook | GORM

有一种需求,比如在下方的表结构中,我们希望每个记录创建的时候自动加上当前时间,即加入到 AddTime 中。

type User struct {
    gorm.Model
    Name string
    AddTime sql.NullTime
}

AddTime 使用 sql.NullTime 而不使用 time.time 的原因同样在于防止未传值导致零值报错问题。零值更新问题

这时我们可以使用 BeforeCreate 这个钩子,除此之外还有 AfterCreate,同理保存、更新、删除都有类似的钩子方法。

func (u *User) BeforeCreate(tx *gorm.DB) (err error){
    u.AddTime = time.Now()
    return
}

完整示例代码

package main

import (
    "fmt"
    "log"
    "os"
    "time"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

// 增删改查
// type User struct {
//     gorm.Model
//     Name string
//     Age  uint
// }

// Belongs TO
// type User struct {
//     gorm.Model
//     Name      string
//     CompanyID int
//     Company   Company
// }

// type Company struct {
//     ID   int
//     Name string
// }

// Has Many
// type User struct {
//     gorm.Model
//     Name       string
//     CreditCart []CreditCart
// }

// type CreditCart struct {
//     gorm.Model
//     Number string
//     UserID uint
// }

// Many To Many
type User struct {
    gorm.Model
    Name      string
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Name string `gorm:"index:,unique"`
}

func main() {
    dsn := "root:123456@tcp(192.168.101.134:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
    newlogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
        logger.Config{
            SlowThreshold: time.Second, // Slow SQL threshold
            LogLevel:      logger.Info, // Log level
            Colorful:      true,        // Disable color
        },
    )
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newlogger,
    })
    if err != nil {
        panic(err)
    }

    // 创建记录
    // db.AutoMigrate(&User{})
    // users := []User{
    //     {Name: "liuchao1", Age: 19},
    //     {Name: "liuchao2", Age: 20},
    //     {Name: "liuchao3", Age: 21},
    // }
    // db.Create(&users)

    // 查询记录
    // var user User
    // var users []User
    // db.First(&user, "age = ?", "21") // 查询id为1的user
    // fmt.Println(user.ID, user.Name, user.Age)
    // db.Where("age = ?", "21").First(&user)
    // fmt.Println(user.ID, user.Name, user.Age)
    // db.Where(&User{Name: "liuchao3"}).First(&users)
    // fmt.Println(user.ID, user.Name, user.Age)
    // db.Where(map[string]interface{}{"name": "liuchao2"}).Find(&users)
    // for _, user := range users {
    //     fmt.Println(user.ID, user.Name, user.Age)
    // }

    // 更新记录
    // db.Model(&User{}).Where("name = ?", "liuchao1").Update("age", 100)
    // db.Model(&User{}).Where("id = ?", "1").Updates(User{Name: "liuchao5", Age: 200})

    // 删除记录
    // db.Delete(&User{}, []int{4, 5, 6, 7})
    // db.Where("age < ?", 100).Delete(&User{})

    // Belongs TO
    // db.AutoMigrate(&User{}, &Company{})
    // db.Create(&[]User{
    //     {Name: "liuchao1", Company: Company{Name: "company1"}},
    //     {Name: "liuchao2", Company: Company{Name: "company2"}},
    //     {Name: "liuchao3", Company: Company{Name: "company3"}},
    // })
    // var user User
    // db.Preload("Company").First(&user, 1)
    // fmt.Println(user.Name, user.Company.Name)
    // db.Joins("Company").First(&user, 2)
    // fmt.Println(user.Name, user.Company.Name)

    // Has Many
    // db.AutoMigrate(&User{}, &CreditCart{})
    // db.Create(&[]User{
    //     {Name: "liuchao1", CreditCart: []CreditCart{{Number: "123456"}, {Number: "654321"}}},
    //     {Name: "liuchao2", CreditCart: []CreditCart{{Number: "abcdef"}, {Number: "fedcba"}}},
    //     {Name: "liuchao3", CreditCart: []CreditCart{{Number: "qwerty"}, {Number: "ytrewq"}}},
    // })
    // var user User
    // db.Preload("CreditCart").First(&user)
    // for _, cart := range user.CreditCart {
    //     fmt.Println(user.Name, cart.Number, cart.UserID)
    // }

    // Many To Many
    // db.AutoMigrate(&User{}, &Language{})
    // db.Create(&[]User{
    //     {Name: "liuchao1", Languages: []Language{{Name: "chinese"}, {Name: "english"}}},
    //     {Name: "liuchao2", Languages: []Language{{Name: "chinese"}, {Name: "japanese"}}},
    //     {Name: "liuchao3", Languages: []Language{{Name: "chinese"}, {Name: "korean"}}},
    //     {Name: "liuchao4", Languages: []Language{{Name: "english"}, {Name: "french"}}},
    // })
    var user User
    // db.Preload("Languages").First(&user)
    // for _, language := range user.Languages{
    //     fmt.Println(language.Name)
    // }
    db.First(&user)
    var languages []Language
    _ = db.Model(&user).Association("Languages").Find(&languages)
    for _, language := range languages {
        fmt.Println(user.Name, language.Name)
    }
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    for i := range u.Languages {
        lang := &u.Languages[i]
        if err := tx.Where(Language{Name: lang.Name}).FirstOrCreate(lang).Error; err != nil {
            return err
        }
    }
    return nil
}