子兮子兮 子兮子兮

No can, but will.

目录
Go 语言使用 GORM 对象关系映射框架兼容多种数据库
/        

Go 语言使用 GORM 对象关系映射框架兼容多种数据库

书接上文,本文主要对使用 GORM 操作数据库时如何同时兼容多种数据库进行说明。

一、GORM 数据模型映射定义多数据库兼容说明

在定义 GORM 数据模型时,需要确保定义的数据模型能够在不同数据库系统之间正确地映射和转换,包括数据类型、表名和字段名等方面。

完整模型结构体示例
package model

// ExampleStandardModel GORM 标准模型示例
//
// 定义字段类型兼容多种数据库参考规则
type ExampleStandardModel struct {
	// ID 字段在 GORM 中默认为主键,两个字母全部大写,建议显式指定 primaryKey 标签
	//
	//  - Go 整数类型字段 GORM 的 autoIncrement:false 标签,表示非自增字段,建议显式指定,否则在 PostgreSQL 中会默认自增
	//  - Go 整数类型字段 GORM 的 autoIncrement:true 标签,表示自增字段
	//  - GORM 的 not null 标签,表示数据库字段的非空约束,按需指定
	//  - GORM 的 comment:xxx 标签,表示字段注释,建议显式指定
	ID            int64  `gorm:"column:id;size:64;not null;primaryKey;autoIncrement:true;comment:主键,自增 ID" json:"id"`
	IDNoIncrement int64  `gorm:"column:id_no_increment;size:64;not null;primaryKey;autoIncrement:false;comment:主键,非自增整数 ID" json:"idNoIncrement"`
	IDString      string `gorm:"column:id_string;type:varchar(64);not null;primaryKey;comment:主键,字符串 ID" json:"idString"`

	// Go 整数类型字段 GORM 标签建议指定 size:64(字段大小),GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型
	//
	//  - PostgreSQL: smallint、integer、bigint、smallserial、serial、bigserial
	//  - SQL Server: tinyint、smallint、int、bigint
	//  - MySQL:      tinyint、smallint、mediumint、int、bigint
	//  - Oracle:     SMALLINT、INTEGER
	//  - 达梦:        TINYINT、SMALLINT、INT、BIGINT
	//  - SQLite:     integer
	TinyintField  int   `gorm:"column:tinyint_size_field;size:8;comment:微小整数字段;" json:"tinyintField"`
	SmallintField int   `gorm:"column:smallint_field;size:16;comment:小型整数字段;" json:"smallintField"`
	IntegerField  int   `gorm:"column:integer_field;size:32;comment:普通整数字段;" json:"integerField"`
	BigintField   int64 `gorm:"column:bigint_field;size:64;comment:大型整数字段;" json:"bigintField"`

	// Go 浮点类型字段 GORM 标签建议指定 precision:18(精度)和 scale:4(小数位数),GORM 会根据 precision 和 scale 自动转换为对应的数据库类型
	//
	//  - PostgreSQL: decimal、numeric(precision)、numeric(precision, scale)
	//  - SQL Server: float、decimal(precision)、decimal(precision, scale)
	//  - MySQL:      double、float、decimal(precision, scale)
	//  - Oracle:     FLOAT
	//  - 达梦:        DOUBLE、DECIMAL(precision, scale)
	//  - SQLite:     real
	FloatField float64 `gorm:"column:float_field;precision:18;scale:4;comment:浮点型小数字段,可通过 precision 和 scale 指定小数精度;" json:"floatField"`

	// Go 字符串类型字段 GORM 标签建议指定 size:1000(字段大小),GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型
	//
	//  - PostgreSQL: text、varchar(size)
	//  - SQL Server: nvarchar(MAX)、nvarchar(size)
	//  - MySQL:      longtext、mediumtext、varchar(size)
	//  - Oracle:     CLOB、VARCHAR2(size)、VARCHAR2(size CHAR)
	//  - 达梦:        CLOB、VARCHAR、VARCHAR(8188 CHAR)、VARCHAR(size)、VARCHAR(size CHAR)
	//  - SQLite:     text
	NvarcharField string `gorm:"column:nvarchar_field;size:1000;comment:(SQL Server)双字节可变长度字符串类型;" json:"nvarcharField"`
	VarcharField  string `gorm:"column:varchar_field;type:varchar(1000);comment:(SQL Server)单字节可变长度字符串类型;" json:"varcharField"`

	// Go 布尔类型字段 GORM 标签不需要指定 type:bit 或 type:boolean,GORM 会根据 Go 类型自动转换为对应的数据库类型
	//
	//  - PostgreSQL: boolean
	//  - SQL Server: bit
	//  - MySQL:      boolean
	//  - Oracle:     NUMBER(1)
	//  - 达梦:        BIT
	//  - SQLite:     numeric
	BoolField bool `gorm:"column:bool_field;comment:布尔类型字段" json:"boolField"`

	// Go 字节切片类型字段 GORM 标签建议指定 size:-1(字段大小),GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型
	//
	//  - PostgreSQL: bytea
	//  - SQL Server: varbinary(MAX)
	//  - MySQL:      longblob、mediumblob、varbinary(size)
	//  - Oracle:     BLOB
	//  - 达梦:        BLOB、VARBINARY(size)
	//  - SQLite:     blob
	BytesField []byte `gorm:"column:bytes_field;size:-1;comment:二进制数据类型字段,可用于存储文件内容" json:"bytesField"`
}

// TableName 显式指定数据库对应的表名
func (ExampleStandardModel) TableName() string {
	return "example_standard"
}

1. 主键字段映射

通常有以下三种类型的主键:

  1. 整数、自增
  2. 整数、非自增
  3. 字符串

如下三个字段所示:

type ExampleStandardModel struct {
	ID            int64  `gorm:"column:id;size:64;not null;primaryKey;autoIncrement:true;comment:主键,自增 ID" json:"id"`
	IDNoIncrement int64  `gorm:"column:id_no_increment;size:64;not null;primaryKey;autoIncrement:false;comment:主键,非自增整数 ID" json:"idNoIncrement"`
	IDString      string `gorm:"column:id_string;type:varchar(64);not null;primaryKey;comment:主键,字符串 ID" json:"idString"`
}

注意事项:

  • 整数类型主键字段的 GORM 数据类型使用 size:n 标签映射;
  • 字符串类型主键字段的 GORM 数据类型使用 type:varchar(n) 标签映射;
  • ID 字段在 GORM 中默认为主键,两个字母全部大写,建议显式指定 primaryKey 标签;
  • Go 整数类型字段 GORM 的 autoIncrement:false 标签,表示非自增字段,建议显式指定,否则在 PostgreSQL 中会默认自增;
  • Go 整数类型字段 GORM 的 autoIncrement:true 标签,表示自增字段;
  • GORM 的 not null 标签,表示数据库字段的非空约束,按需指定;
  • GORM 的 comment:xxx 标签,表示字段注释,建议显式指定。

[!Tip]

参考链接:https://gorm.io/zh_CN/docs/models.html

2. 整数类型字段映射

Go 整数类型字段 GORM 数据类型使用 size:nn 为字段大小,如 size:64)标签映射,
GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型,通过 size 标签映射数据库中的整数类型对照关系如下所示:

序号 size 标签 SQL Server PostgreSQL MySQL Oracle 达梦 DM8 SQLite
1. size:8 tinyint smallint/smallserial tinyint SMALLINT TINYINT integer
2. size:16 smallint smallint/smallserial smallint INTEGER SMALLINT integer
3. size:24 int integer/serial mediumint INTEGER INT integer
4. size:32 int integer/serial int INTEGER INT integer
5. size:64 bigint bigint/bigserial bigint INTEGER BIGINT integer
参考链接

[!Tip]

type ExampleStandardModel struct {
	TinyintField  int   `gorm:"column:tinyint_size_field;size:8;comment:微小整数字段;" json:"tinyintField"`
	SmallintField int   `gorm:"column:smallint_field;size:16;comment:小型整数字段;" json:"smallintField"`
	IntegerField  int   `gorm:"column:integer_field;size:32;comment:普通整数字段;" json:"integerField"`
	BigintField   int64 `gorm:"column:bigint_field;size:64;comment:大型整数字段;" json:"bigintField"`
}

3. 浮点类型字段映射

Go 浮点类型字段 GORM 数据类型使用 precision:nn 为精度,如 precision:18)和 scale:nn 为小数位数,如 scale:4
)标签映射,
GORM 会根据 Go 类型以及 precisionscale 自动转换为对应的数据库类型。通过 precisionscale
标签映射数据库中的小数类型对照关系如下所示:

序号 precisionscale 标签 SQL Server PostgreSQL MySQL Oracle 达梦 DM8 SQLite
1. precision:0;scale:0 float decimal float FLOAT DOUBLE real
2. precision:18;scale:0 decimal(18) numeric(18) decimal(18, 0) FLOAT DECIMAL(18, 0) real
3. precision:18;scale:4 decimal(18, 4) numeric(18, 4) decimal(18, 4) FLOAT DECIMAL(18, 4) real
参考链接

[!Tip]

type ExampleStandardModel struct {
	FloatField float64 `gorm:"column:float_field;precision:18;scale:4;comment:浮点型小数字段,可通过 precision 和 scale 指定小数精度;" json:"floatField"`
}

4. 字符串类型字段映射

Go 字符串类型字段 GORM 数据类型建议使用 size:nn 为字段大小,如 size:1000)标签映射,
GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型。通过 size 标签映射数据库中的字符串类型对照关系如下所示:

序号 size 标签 SQL Server PostgreSQL MySQL Oracle 达梦 DM8 SQLite
1. size:4000 nvarchar(4000) varchar(4000) varchar(4000) VARCHAR2(4000) VARCHAR(4000) text
2. size:10485760 nvarchar(MAX) varchar(10485760) mediumtext CLOB CLOB text
3. size:-1 nvarchar(MAX) text longtext CLOB CLOB text
参考链接

[!Tip]

type ExampleStandardModel struct {
	NvarcharField string `gorm:"column:nvarchar_field;size:1000;comment:(SQL Server)双字节可变长度字符串类型;" json:"nvarcharField"`
	VarcharField  string `gorm:"column:varchar_field;type:varchar(1000);comment:(SQL Server)单字节可变长度字符串类型;" json:"varcharField"`
}

由于 nvarchar 类型仅在 SQL Server 数据库中支持,所以 nvarchar 类型的字段不能使用 GORM 的 type:nvarchar(n) 标签映射,
需要使用 size:n 标签。

varchar 类型在所有数据库中都支持,所以 varchar 类型的字段可以使用 GORM 的 type:varchar(n) 标签映射。
但是不同数据库对 varchar 类型字段的长度支持有所不同,所以即使是 varchar 类型,也建议使用 size 标签进行映射。

另外,text/clob 类型的字段请使用 size:-1 标签进行映射。

5. 布尔类型字段映射

Go 布尔类型字段 GORM 数据类型不要使用 type:bittype:boolean 标签进行映射,
GORM 会直接根据 Go 类型 bool 自动转换为对应的数据库类型,指定 type 标签后会降低数据库兼容性。
Go 布尔类型映射数据库中的布尔类型对照关系如下所示:

序号 数据库类型 对应布尔数据类型 存储的值
1. SQL Server bit 0 / 1
2. PostgreSQL boolean false / true
3. MySQL tinyint(1) 0 / 1
4. Oracle NUMBER(1) 0 / 1
5. 达梦 DM8 BIT 0 / 1
6. SQLite numeric 0 / 1
参考链接

[!Tip]

type ExampleStandardModel struct {
	BoolField bool `gorm:"column:bool_field;comment:布尔类型字段" json:"boolField"`
}

6. 二进制数据类型字段映射

Go 字节切片类型字段 GORM 数据类型建议使用 size:-1 标签映射,GORM 会根据 Go 类型和 size 自动转换为对应的数据库类型。
Go 字节切片类型映射数据库中的二进制数据类型对照关系如下所示:

序号 数据库类型 对应二进制数据类型
1. SQL Server varbinary(MAX)
2. PostgreSQL bytea
3. MySQL longblobmediumblobvarbinary(size)
4. Oracle BLOB
5. 达梦 DM8 BLOBVARBINARY(size)
6. SQLite blob
参考链接

[!Tip]

type ExampleStandardModel struct {
	BytesField []byte `gorm:"column:bytes_field;size:-1;comment:二进制数据类型字段,可用于存储文件内容" json:"bytesField"`
}

7. 表名映射

GORM 自动迁移表结构时默认会通过模型结构体的名称自动转换为数据库中的表名,为了在不同的数据库中具有一样的表名,
模型结构体应该实现 GORM 中 Tabler 接口的 TableName 方法,
来指定固定表名:

// TableName 显式指定数据库对应的表名
func (ExampleStandardModel) TableName() string {
	return "example_standard"
}

另外,为了防止没有实现 TableName() 方法的模型在不同的数据库中标识符出现命名截断,在初始化 GORM 时应该配置
命名策略 中的 IdentifierMaxLength
标识符最大长度选项为 30,以保证在不同数据库中保持一致的标识符长度。

推荐配置:

gorm.Config{
	// 自定义命名策略
	NamingStrategy: schema.NamingStrategy{
		IdentifierMaxLength: 30, // Oracle: 30, PostgreSQL:63, MySQL: 64, SQL Server、DM: 128
	},
	// ...
}

对于实现了 TableName 方法的模型返回的表名和 column 标签指定的字段名尽量不要超过 30

二、GORM 操作数据库兼容说明

GORM 提供了一致的 API 接口,用于在不同数据库系统之间执行 CRUD 操作。
但在实际应用中,仍然需要针对不同数据库系统的特性和要求进行适配和调整。

在多数据库环境下使用 GORM 操作数据库时,需要特别注意以下几点:

  • 数据库连接配置:根据目标数据库的类型和参数,配置合适的数据库连接信息。
  • 数据库迁移:在不同数据库系统之间进行迁移时,需要注意数据模型的兼容性和迁移过程中可能存在的差异。
  • SQL 语句生成:在使用 GORM 进行查询和操作时,需要注意生成的 SQL 语句是否符合目标数据库的语法和规范,避免出现语法错误或性能问题。

使用 GORM 自动迁移表结构时,GORM 会自动使用双引号 "" 或反引号 `` 包裹标识符用于明确指定标识符的大小写
所以为了提高多数据库的兼容性,在显式指定表名、字段名时需要注意标识符的大小写

本节内容将通过以下 联合查询语句 进行举例说明:

SELECT A.column_1 AS C1, B.column_1 AS C2
FROM table_a A
LEFT JOIN table_b B ON A.column_2=B.column_2
WHERE A.column_3 = 'true'
ORDER BY A.column_1 DESC

对于以上 SQL 语句,使用 GORM 最基本的实现方式可能是这样的:

db.Select("A.column_1 AS C1, B.column_1 AS C2").
    Table("table_a A").
    Joins("table_b B ON A.column_2=B.column_2").
    Where("A.column_3 = ?", true).
    Order("A.column_1 DESC")
    Find(...)

但这样生成的 SQL 跟原语句一样并没有明确指定标识符的大小写,不能兼容某些标识符区分大小写数据库。

为了明确指定标识符的大小写以兼容多种数据库,我们最终需要的 SQL 应该是下面这样用双引号 "" 或反引号 ``
MySQLSQLite)包裹了标识符的:

SELECT A."column_1" AS C1, B."column_1" AS C2      -- 列名和别名之间可以使用 AS 关键字
FROM "table_a" A                                   -- 表名和别名之间不要使用 AS 关键字
LEFT JOIN "table_b" B ON A."column_2"=B."column_2" -- 表名和字段名要包裹起来明确指定大小写
WHERE A."column_3" = 'true'                        -- 别名不要包裹起来,且尽量全部大写
ORDER BY A."column_1" DESC

下面将对如何通过 GORM 得到类似上述明确指定标识符的大小写的 SQL 语句进行详细说明。

1. 通过 GORM 子句处理标识符

GORM 框架内部提供了 Table
Column 子句表达式,用于生成带引号的表名和字段名。
可以在 GORM 的方法中通过问号 ? 占位符来使用 GORM 子句表达式。

column1 := clause.Column{Name: "column_1"}
column2 := clause.Column{Name: "column_2"}
column3 := clause.Column{Name: "column_3"}

tableA := clause.Table{Name: "table_a"}
tableB := clause.Table{Name: "table_b"}

db.Select("A.? AS C1, B.? AS C2", column1, column1).
    Table("? A", tableA).
    Joins("? B ON A.?=B.?", tableB, column2, column2).
    Where("A.? = ?", column3, true).
    Order("A.? DESC", column1)
    Find(...)

2. 通过模型结构体指定表名和字段名

在使用 GORM 操作数据库时通常都会定义对应表结构的模型结构体,在 GORM 的方法中应该尽可能的去使用模型结构体和结构体中的字段。
上述示例中的 SQL 应该对应以下两个模型结构体:

type TableA struct {
	Column1 string `gorm:"column:column_1"`
	Column2 string `gorm:"column:column_2"`
	Column3 bool   `gorm:"column:column_3"`
}

func (*TableA) TableName() string {
	return "table_a"
}

type TableB struct {
	Column1 string `gorm:"column:column_1"`
	Column2 string `gorm:"column:column_2"`
}

func (*TableB) TableName() string {
	return "table_b"
}

然后我们可以在 GORM 的方法中使用模型结构体及其字段:

column1 := clause.Column{Name: "column_1"}
column2 := clause.Column{Name: "column_2"}

db.Select("A.? AS C1, B.? AS C2", column1, column1).
    Table("(?) A", db.Model(&TableA{}).Where(&TableA{Column3: true})).
    Joins("(?) B ON A.?=B.?", db.Model(&TableB{}), column2, column2).
    Order("A.? DESC", column1)
    Find(...)

在 GORM 中指定表名时,可以通过 db.Table("table_name") 使用字符串指定表名,也可以通过 db.Model(&TableModel{})
使用模型结构体指定表名。

通过合理使用模型结构体,减少了 GORM 子句表达式的使用,使代码更加符合 GORM 的规范,还增加了代码的可读性。

另外可以看到,上述代码中的 Where 子句被放到了 Table 方法中,将 table_a 过滤查询后的整理作为 A 表,减少了别名的使用。

3. 通过 map[string]interface{} 指定查询条件

在上一步我们将 Where 查询条件改为了使用模型结构体及其字段作为参数值进行过滤查询,
GORM 的条件方法还支持传入 map[string]interface{} 类型的参数,GORM 会自动使用引号对 mapkey 进行包裹后作为字段名,
mapvalue 作为字段值。

column1 := clause.Column{Name: "column_1"}
column2 := clause.Column{Name: "column_2"}

db.Select("A.? AS C1, B.? AS C2", column1, column1).
    Table("(?) A", db.Model(&TableA{}).Where(map[string]interface{}{"column_3": true})).
    Joins("(?) B ON A.?=B.?", db.Model(&TableB{}), column2, column2).
    Order("A.? DESC", column1)
    Find(...)

通过以上操作,可以确保在使用 GORM 操作数据库时能够兼容多种数据库,提高应用程序的灵活性和可移植性。

在处理更复杂 SQL 的多数据库兼容性时,以上 3 种方式可以相互结合使用。

未完待续...