前言

func (*gorm.DB).AutoMigrate(dst ...interface{}) error

首先, 介绍这个api的原因是, 我们使用gorm框架永久了, 对于sql的使用难免会摆脱依赖, 也就是说, 更改表字段时, 可能更倾向与直接通过修改该表对应的结构体标签名(gorm:"column:unix")或是直接在结构体中修改字段名称, 然后通过AutoMigrate()来传递到表中, 作出对应修改。

不过, 即使使用了AutoMigrate这个API, 也还是需要我们手动的去操作数据表。因为AutoMigrate仅能够帮助我们完成如下操作:

在 GORM 中,AutoMigrate 函数会在以下情况下修改数据库表结构:

  1. 创建表:如果数据库中不存在与结构体对应的表,AutoMigrate 会创建一个新的表。

  2. 添加列:如果数据库表中缺少与结构体字段对应的列,AutoMigrate 会添加一个新的列。

  3. 添加索引:如果数据库表中缺少与结构体标签定义的索引对应的索引,AutoMigrate 会添加一个新的索引。

然而,为了保护你的数据,AutoMigrate 函数不会删除未使用的列,也不会更改现有列的类型或名称

也就是说, 我做对于某个字段标签名或字段名的修改, 只会使得数据库表发生如下变更, 以适应接下来的新业务, 而无法自动迁移旧的数据。

显然AutoMigrate还不够强大, 不过也确实可以帮助我们减轻一部分的工作量(但减轻的这部分确实是微乎其微, 无关痛痒的)。 并不能真正的自动实现重命名。

db.Migrator().RenameColumn(&User{}, "name", "new_name")

RenameColumn()函数可以在重命名字段时保留旧数据。这是因为GORM在重命名字段时,实际上是在数据库中更改了字段的名称,而不是删除旧字段并创建一个新字段。因此,该字段中的所有数据都将被保留。

但是,请注意,在某些数据库(如SQLite)中,不支持ALTER COLUMN或DROP COLUMN操作。在这种情况下,GORM将创建一个新表、复制所有数据、删除旧表、重命名新表。因此,在重命名字段之前,请确保你了解了数据库的限制和GORM如何处理这些限制。如果你有任何疑问,建议你在重命名字段之前备份你的数据。这样,即使出现问题,你也可以恢复到之前的状态。

确实,我们想实现重命名, Gorm也提供了像这种API, 不过这种只用一次的操作, 写在代码里执行后删除, 还不如我直接在sql命令行中, 甚至直接在ui界面中执行呢。而且我们仍需要面临手动变更数据库表所对应的结构体字段的标签名 或 字段名 的额外工作。 并且若是改的是字段名, 还会间接的引起更多地方的修改。

在 GORM 中,AutoMigrate 函数不会修改现有的数据库字段。但是,GORM 提供了 Migrator 接口,其中包含一些可以用来修改字段的方法。例如:

  • AddColumn(dst interface{}, field string) error:添加一个新的列。
  • DropColumn(dst interface{}, field string) error:删除一个列。
  • AlterColumn(dst interface{}, field string) error:修改一个列。
  • RenameColumn(dst interface{}, oldName, field string) error:重命名一个列。

这些方法可以用来手动修改数据库表的结构。但是,请注意,在使用这些方法时,你需要确保已经对数据库做了备份,并在开发或测试环境中先行验证。

注意:

在 GORM 中,AlterColumnRenameColumn 是两个不同的操作。

  • AlterColumn:这个方法用于修改一个列的属性。例如,你可能想要改变一个列的数据类型,或者添加或删除一个约束。这个方法会生成一个 ALTER TABLE SQL 语句,用于修改列的定义。

  • RenameColumn:这个方法用于改变一个列的名称。例如,你可能想要将一个名为 “old_name” 的列重命名为 “new_name”。这个方法会生成一个 ALTER TABLE SQL 语句,用于重命名列。

总的来说,AlterColumnRenameColumn 都是用于修改数据库表结构的方法,但它们关注的方面不同:一个关注于改变列的属性,另一个关注于改变列的名称。

毫不夸张的说, 不论何种方式去更改现有表结构, 所引起的额外工作量, 将是巨量的代码变更, 而且对于这类工作量可以说是毫无意义,浪费生命却又不得不做的

于是 gen 出现了。并且它告诉你, 所有的这些, 完全是可以自动来完成的。代码中一切相关字段的更改, 都是可以自动完成的, 并且gen还提供了自有的一套动态接口, 来对应不同的数据表, 甚至是关联查询也给出了相适配的解决方案。也就是说, 借助gen我们可以做的几乎完全的脱离(sql), 而不是像传统orm的使用那样, 半orm半sql的做法

gen

我们可以直接去官网查看gen的使用: https://gorm.io/gen/
或是一些第三方的用户使用文档来了解gen的具体使用:

我就在文章中简单介绍下吧!

Gen中的api类型

涉及的概念很多, 我们先从, 它自动生成的 两种api 类型开始介绍:

  • DAO , 我们一般通过ApplyBasic(mode.stractName{})(只做基础查询的情况优先推荐)或ApplyInterface(func(InterfaceName) {},mode.stractName1{},mode.stractName2{}), 来生成默认的API。
  • Dynamic SQL

其中ApplyInterface()主要适用于生成 Dynamic SQL, 不过, 它也是会生成参数内对应表的DAOapi的。

个人的建议是, 如果您涉及到复杂查询,需要Dynamic SQL时, 再使用ApplyInterface()。毕竟个人认为它相比于只能用来生成DAOapi的ApplyBasic()来说功能更强大, 与之而来的代价是开销也会相对较大。

因此在默认自动生成的DAO可以满足您需求的情况下, 优先推荐ApplyBasic()

虽然两种api都是自动生成的, 但是对于Dynamic SQLapi来说, 我们还需要做一些额外的工作, 才能支持Gen对它的自动生成。

  • 我们需要先定义相关的接口类型
  • 然后安装Gen所给的规范, 定义自身需要的接口
  • 然后在此接口上方编写注释, 内容为对应Gen要求规范的sql语句
  • 最后, 执行Configuration.go生成Dynamic SQLapi。

也就是说, 我们是需要在注释中通过sql注明此接口具体要进行的工作的, 以便Gen为我们准确的生成我们需要的真实的更符合自身阅读习惯的api。(不过, DAO本身提供的, 已经满足我们的几乎大部分需求了, 不过调用链过长时, 或是一些特殊需要时, 还是需要通过Dynamic SQL来实现的。)

自此, 程序员们可以摆脱crud的无聊工作内容了。

DAO

具体使用还存在很多细节, 可参考官方文档的对应篇幅https://gorm.io/gen/dao.html

Dynamic SQL

具体使用还存在很多细节, 可参考官方文档的对应篇幅https://gorm.io/gen/dynamic_sql.html

部分配置介绍

  • 下面提到的这四个选项都是 GORM 的代码生成工具 gen 的配置选项,它们的作用如下:

    • WithContext:这个选项会生成使用 context.Context 的代码。这对于需要在查询中使用 context 的场景非常有用,例如设置查询超时或在中间件中传递数据库连接。

    • WithDefaultQuery:这个选项会生成默认的查询结构体,这个结构体可以作为全局变量使用。这样,你就可以在你的代码中直接使用这个结构体,而不需要每次都创建一个新的实例。

    • WithoutContext:这个选项会生成没有 context 调用限制的代码。这意味着你可以在没有 context 的情况下调用这些生成的方法。这对于一些简单的查询或者在不需要 context 的场景下非常有用。

    • WithQueryInterface:这个选项会生成 interface 形式的查询代码,并且这些接口是可以导出的。这样,你就可以在你的代码中使用这些接口,而不是直接使用具体的实现。这对于解耦和测试非常有帮助。

    你可以在 GORM 的 gen 工具中同时使用这些选项。这将使你的代码生成更加灵活和强大。但是,请注意,WithContext(默认) 和 WithoutContext 是互斥的,你只能选择其中一个。如果你同时使用了这两个选项,那么 WithContext 将会覆盖 WithoutContext 的设置。其他的选项可以同时使用。

  • 在使用 gen 进行初始化时,FieldNullable: true 这个配置的作用是设置生成的模型中可空字段的类型为指针类型。

    例如,如果你有一个数据库表中有一个可空的整数字段 age,那么在生成的模型中,这个字段将会被表示为一个 *int 类型,而不是 int 类型。这样做的好处是,当 age 字段为空时,它的值将会是 nil,而不是 int 类型的零值 0。这可以更准确地反映字段的实际状态,并避免在处理这些字段时出现误解。

总的来说,这些选项提供了很大的灵活性,使得你可以根据你的需求来生成适合你的代码。

Database To Structs

当然, Gen还提供了根据现有数据库表生成符合Gorm规范的结构体的。有两种方式:

  • 利用Gen Tool工具来指定表名, 在项目中生成对应的结构体(生成的文件会默认放入项目中的model目录–也可自定义路径), 具体用法请前往这里查看。
  • 其次, 就是给了两个帮助函数, 以表名为参数传入后, 即可在项目目录中为我们生成:
    • GenerateModel("table_name") , 此方法用于直接生成表名对应的结构体。
    • GenerateModelAs("table_name1", "tablename2"), 此方法用于, 基于现有表结构生成另一个名字的表对应的结构体。
    • 对于以上两种函数的方式, 返回值都可直接用于ApplyBasic()等函数的参数, 还支持自定义一些忽略的字段或新建字段或是指定字段类型…一些相关的操作, 甚至是我们可以直接利用此工具来为我们生成结构体以及对应的表(还不如直接自己写结构体哈哈, 具体场景还是基于现有表结构的操作), 具体用法请前往这里查看。

      相关操作有

      • 生成结构体绑定自定义方法
      • 指定生成的查询结构体字段类型
    • 当然, 图方便的话, 还可以直接使用GenerateAllTable()来直接生成所链接数据库中的所有表格对应的结构体, 注意它的返回值是一个数组, 直接用在ApplyBasic()中的话, 记得使用...解构。
  • 当然, 我们还通过”指定你期望的数据映射关系,如自定义数据库字段类型和 Go 类型的映射关系”映射类型。(几乎用不到)
    • WithDataTypeMap(dataMap) 通过这个函数来实现。

不过, 个人使用下来, 还是有不少小问题的。

首先,由表名反推过来的结构体名, 是有有很小的机率出现小误差的, 往往需要手动修改, 还有就是指定一些其中具体对应字段的类型也很繁琐(不过多半是由于你没有完全安照gorm的标准来指定罢了)。

毕竟直接生成的结构体可能出现类型范围与原有表结构中所使用的大小不一致的情况。

比如我在models表中的INT使用的是8字节长度的, 但反推出的结构体中改字段使用的是int32(即4字节)。因此,若是不在函数参数中作一些相关配置, 生成的结构体是没有办法拿来直接用的

  • 此种情况出现的原有是因为数据库的特性, 比如此问题一般只会出现在sqlite中, 而不会出现在mysql中

    如果你想要使用 int64 类型,你可能需要确保你的数据库列类型是一个 64 位整数类型,例如:

    • MySQL 中的 BIGINT 类型1。然后,GORM 的 gen 工具应该会将其映射为 Go 中的 int64 类型。
    • 在 SQLite 中,所有的整数类型都被存储为变长的整数。这意味着 SQLite 会根据整数的值选择最合适的存储方式。例如,如果一个整数可以用 1 字节表示,那么 SQLite 就会用 1 字节来存储它。如果一个整数需要 8 字节来表示,那么 SQLite 就会用 8 字节来存储它。

      因此,在 SQLite 中,你不需要指定整数的位数(如 INT32 或 INT64)。你只需要使用 INTEGER 类型,SQLite 就会自动选择最合适的存储方式。

      然而,在 GORM 的 gen 工具中,生成的 Go 结构体字段类型是根据数据库列类型转换而来的。对于 SQLite 的 INTEGER 类型,GORM 可能会将其映射为 Go 中的 int32 或 int64 类型,具体取决于整数的大小。

      所以, 如果你想要在 GORM 的 gen 工具生成的 Go 结构体中使用 int64 类型,你的models中的初始结构体书写, 也要尽量符合gorm的规范

      你得在models的初始生成中, 就按照gorm的规范, 在你的模型中为相应的字段添加 gorm:”type:bigint” 标签。例如:

      type User struct {
      ID int64 `gorm:"type:bigint"`
      // ...
      }
      
      • 那这个操作会不会影响我的 sqlite 数据库, 使得其变长存储的特性被破坏呢?
        答:
        • 在 SQLite 中,即使你在 GORM 模型中为某个字段指定了 gorm:"type:bigint" 标签,SQLite 仍然会使用变长整数存储这个字段的值。这是因为在 SQLite 中,所有的整数类型都被存储为变长的整数。SQLite 会根据整数的值选择最合适的存储方式。

          所以,添加 gorm:"type:bigint" 标签不会破坏 SQLite 的变长存储特性。这个标签只是告诉 GORM,你希望在 Go 结构体中使用 int64 类型来表示这个字段。

          总的来说,你可以放心地在 GORM 模型中使用 gorm:"type:bigint" 标签,这不会影响 SQLite 的存储优化。

      当然, 直接做到这一点(指严格按照gorm规范)对新手来说是很难的, 因此你可以借助gen, 不断对比gen生成的model你自己的models之间的差异, 来渐进式地规范你对gorm的使用, 也就是说有了gen, 新手也可以稳如老手。

      比如, 很多人的英语水平并不高, 那么在其使用Gorm设计数据库时, 即使知道 结构体用单数 , 表名转换为复数 的规范, 也依旧会不免犯错。

      在 GORM 的 gen 工具中,生成的结构体名称是根据表名转换而来的。默认情况下,GORM 会将表名从 snake_case(蛇形命名法)转换为 CamelCase(驼峰命名法)。例如,对于结构体名称 TestData,其默认的表名会是 test_data, 这很正常, 但是这只是你英语不好选手的直觉。

      DatumData 这个词的单数形式。因为 GORM 的 gen 工具在处理英语复数形式时的一个特性, 会自动将表名test_data转换为结构体TestDatum(在英语中,datadatum 的复数形式)。当 GORM 的 gen 工具看到 data 这个词时,会认为它是复数形式,然后将其转换为单数形式 datum。 也就是说, 你会发现, 你的models中的结构体命名可能存在一些偏差, 对照model后便可以发现这些未注意到的细节。

      英语不好的长见识了吧!!!哈哈!并不是所有的复数形式都是简单在其末尾加s

      因此, 使用gen后, 通过对比model和models, 你就能够发现更多原本不符合规范的小细节, 从而使自己的代码更符合规范。

本人目前对此反向应用的的实际用途是, 使用了model和models两个文件夹, models中是自己的实际定义数据库的地方, 这样做的好处有:

  • 减少实际的初始化Gen代码的工作量, 以及后续增加表时, 不用去Configuration.go文件中做对应的添加。

    • 我在实际使用中是以model为准的, 但在gorm的初始化中, 我是用的是models。 这样一来, 我帮大家分析下我的工作流:
      • 在models中做增加结构体或字段的操作。(不涉及修改字段和删除字段, 这两种建议手动操作)

        在 GORM 中,AutoMigrate 函数不会修改现有的数据库字段1。但是,GORM 提供了 Migrator 接口,其中包含一些可以用来修改字段的方法1。例如:

        • AddColumn(dst interface{}, field string) error:添加一个新的列1。
        • DropColumn(dst interface{}, field string) error:删除一个列1。
        • AlterColumn(dst interface{}, field string) error:修改一个列1。
        • RenameColumn(dst interface{}, oldName, field string) error:重命名一个列1。

        在 GORM 中,AlterColumnRenameColumn 是两个不同的操作。

        • AlterColumn:这个方法用于修改一个列的属性。例如,你可能想要改变一个列的数据类型,或者添加或删除一个约束。这个方法会生成一个 ALTER TABLE SQL 语句,用于修改列的定义。

        • RenameColumn:这个方法用于改变一个列的名称。例如,你可能想要将一个名为 “old_name” 的列重命名为 “new_name”。这个方法会生成一个 ALTER TABLE SQL 语句,用于重命名列。

        总的来说,AlterColumnRenameColumn 都是用于修改数据库表结构的方法,但它们关注的方面不同:一个关注于改变列的属性,另一个关注于改变列的名称。

      • gorm初始化函数根据AutoMigrate修改对应表结构, 或是增加表字段等。
      • 或者是以上这两步, 无聊手动自动, 都合并看作成手动修改或删除表字段以及对应的modols结构体。(注意一定要确保 models 和 真实表结构 的对应修改与一致性。)
      • 那么总之, 表结构改变, 带来的就是我们Gen所需要处理的model中的结构改变, 以及model中结构改变引起的DAOapi的全局自动改变。(注意,我们实际使用的业务逻辑中, 要以model文件夹为准)
  • 对于一些需要更改tag标签来修改字段名, 或是需要修改表名的时候, 直接去model中复制过来然后做更改, 免得自己写了。

  • 我会建立一个dal目录来定义 我的 configuration.go 文件, 用于执行后自动生成 model及其对应的query。 (注意, 这里我的configuration.go, 会以 package main 为包名, 虽然这样做是不合适的<相同的包名的.go文件, 在物理文件夹上, 也应该保持一致, 否则编译器仍旧会视其为不同的包, 而且会带来设计问题等一系列问题>, 但是我的生成需求本就不是实际项目的一部分, 就应该脱离实际的 package main 来单独手动执行<在我的需求中, 它本就是独立的>, 所以我才这样使用。)

我希望我分享的这些自身经验信息能够帮助你理解 GORM gen 的强大之处。

使用注意

注意两个函数的用途, 一个是query.SetDefault(), 另一个是query.Use()

  • query.SetDefault(), 用于设置query的默认值(即全局Q变量或query模块直接被调用时使用的数据库链接)。
  • query.Use(), 用于对指定的数据库链接, 进行操作。

query.Use(), 常用于后续有不同数据库链接需求, 通过query.Use()获取一个非默认的Query变量进行查询(或是不使用局部变量,直接内联此计算值进行查询)。

比如你又一个测试用的数据库, 还有一个业务用的数据库, 那么我们只需要在初始化时使用 query.SetDefault() 来指定默认数据库为业务数据库, 后续直接通过全局变量Q写业务即可。 而遇到的相关测试, 则使用query.Use()指定另外用于测试的数据库。

总结

总之, Gen还在发展中, 未来对于准确度的问题, 也一定会得到改善的。 而且, 现如今, 我已经将其用到了生产代码上写项目了。