-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
eorm: Migrate 功能的支持 #214
Comments
这个功能的实现要分成两个部分:
在这两个的基础上,再考虑执行对应的 DDL。 举个例子来说:
而执行 DDL 本身如果要做成命令行工具,我的建议是拆出去作为一个单独仓库。 此外,还有一个很关键的问题,就是用户怎么指定我 GO 某个类型对应到什么的数据库类型? 再进一步考虑,用户能不能在测试环境的时候使用 sqlite,生成一种 DDL?在生产环境是 mysql,又是另外一种 DDL? |
方案选型按gorm 还是 goalng-migrate 呢?如果要做成命令行要单独拆出去或者考虑封装 goalng-migrate ??如果按照 gorm,那就设计一个 migrate 的接口,去封装 DDL 的生成就可以了!! |
成年人做啥选择,我都要!意思就是,用户可以自己在代码里面调用一下,也可以用命令行执行一下。暂时都放在 eorm 吧 |
可以的,非常赞!可以搞起来! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
场景分析
虽然在一些大厂中,往往修改一个业务表的字段都要走复杂的流程,但是在绝大多数中小公司是没有如此复杂的流程了,所以为了开发者能绕开复杂的建表语句与修改表的语句,方便直接构建 object 和 数据库 table 的映射,大多数 orm 框架 提供给了使用者 Migrate 的功能。
数据库 Migrate 一直是数据库运维人员最为头痛的问题,如果仅仅是一张表增删字段还比较容易,那如果涉及到外键等复杂的关联关系,数据库的 Migrate 就会变得非常困难。
需求分析
实现一个 Migrator 通常涉及以下步骤:
功能需求
开源实例
GORM
GORM 提供了 Migrator 接口,该接口为每个数据库提供了统一的 API 接口,可用来为您的数据库构建独立迁移,
例如:
SQLite 不支持 ALTER COLUMN、DROP COLUMN,当你试图修改表结构,GORM 将创建一个新表、复制所有数据、删除旧表、重命名新表。
一些版本的 MySQL 不支持 rename 列,索引。GORM 将基于您使用 MySQL 的版本执行不同 SQL。
使用例子
执行迁移操作:
回滚迁移操作:
这些示例展示了使用Gorm进行数据库迁移的一般模式。可以根据自己的需求定义更多的迁移文件和操作。记得在执行迁移操作之前,需要确保正确配置数据库连接,引入Gorm和相关数据库驱动,并按照示例中的方式调用相应的迁移函数和方法。
版本控制
需要注意的是,GORM 虽然提供了不错的数据库迁移功能,但是距离理想的 “版本控制” 仍有距离。不支持:版本记录、版本回退、版本选择。这些都需要开发者自行封装。
Beego ORM
Beego 的 migrate 的接口设计和gorm差不多,但核心逻辑主要是基于命令行工具和代码生成实现的。
版本控制
Beego ORM,版本控制是基于 Migration 结构体模型表,该表在初始化时会创建在数据库中,用来记录数据库迁移的版本的变化 ,包括创建、更新、滚等操作。
使用样例
生成数据库迁移文件
数据库迁移
golang-migrate
**golang-migrate **支持非常多的数据库类型,包括: cockroachdb, firebird, postgresql, redshift, clickhouse, postgres, cockroach, firebirdsql, mysql, crdb-postgres, mongodb, mongodb+srv, neo4j, pgx, spanner, sqlserver, stub, cassandra
这是一个简单的工具,可基于文件进行迁移。它带有 Go 库和 CLI 工具,可创建 SQL 迁移文件并管理架构版本。
设计
方案一:
提供一个 Migrate API,支持根据 Go Struct 结构自动生成对应的表结构 。
方案二:
通过 CLI 工具 辅助生成 sql 迁移文件,用户需要手动将迁移sql 添加至迁移文件中、支持版本管理, 类似于 golang-migrate 。
本框架先选择方案一(gorm等框架的的方式),通过注册结构体来实现对数据库的模型映射。
这个功能的实现要分成两个部分:
DDL 语句生成
DDL(Data Definition Language):数据定义语言,定义语言就是定义关系模式、删除关系、修改关系模式以及创建数据库中的各种对象,比如表、聚簇、索引、视图、函数、存储过程和触发器等等。
数据定义语言是由SQL语言集中负责数据结构定义与数据库对象定义的语言,并且由CREATE、ALTER、DROP和TRUNCATE四个语法组成。比如:
Creater
MySQL
先看简化后的语法解析
用于创建临时表,CREATE TEMPORARY TABLE table_name;这个临时表在当前会话结束或者连接断开后将自动消失。
实际上就是在建表前加上一个判断,只有该表目前尚不存在时才执行CREATE TABLE操作。用此选项可以避免出现表已经存在无法再新建的错误。
需要创建的表名。该表名必须符合标识符规则,通常的做法是在表名中仅使用字母、数字及下划线。
所创建的表中各列的相关属性定义。
语法如下:
如上所示,列的相关属性定义语法内容相当丰富。
1)column_name:
表中列的名字。必须符合标识符规则,而且在表中要唯一。
2)type:
列的数据类型。有的数据类型需要指明长度n,并用括号括起。
3)NOT NULL | NULL:
指定该列是否允许为空。如果既不指定NULL也不指定NOT NULL,列被认为指定了NULL。
4)DEFAULT default_value:
为列指定默认值。如果没有为列指定默认值,MySQL自动地分配一个。
如果列可以取NULL作为值,缺省值是NULL。
如果列被声明为NOT NULL,缺省值取决于列类型:
5)AUTO_INCREMENT:
设置该列有自增属性,只有整型列才能设置此属性。
当你插入NULL值或0到一个AUTO_INCREMENT列中时,列被设置为value+1,在这里value是此前表中该列的最大值。AUTO_INCREMENT顺序从1开始。每个表只能有一个AUTO_INCREMENT列,并且它必须被索引。
设置表的一些属性定义:
1)引擎设置
例如:ENGINE|TYPE = MYISAM
2)字符集设置
例如:CHARACTER SET gbk
3)排序规则设置
例如:COLLATE gbk_chinese_ci
COLLATE是用来做什么的?
是用来排序的规则。对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个COLLATE类型来告知mysql如何对该列进行排序和比较。简而言之,COLLATE会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCT、GROUP BY、HAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和COLLATE有关。
//如下直接通过select查询出指定数据存入新建的表中
CREATE TABLE ... SELECT ...
通常,MySQL的 create 语句如下:
SQLite
“CREATE TABLE”命令用于在 SQLite 数据库中创建一个新表。CREATE TABLE 命令指定新表的以下属性:
create table 的基本语法如下:
通用实例如下:
SQLite 同样也包括类型约束,索引约束、外键约束等语法。约束是在表的数据列上强制执行的规则。这些是用来限制可以插入到表中的数据类型。这确保了数据库中数据的准确性和可靠性。
约束可以是列级或表级。列级约束仅适用于列,表级约束被应用到整个表。
以下是在 SQLite 中常用的约束。
NOT NULL 约束
默认情况下,列可以保存 NULL 值。如果您不想某列有 NULL 值,那么需要在该列上定义此约束,指定在该列上不允许 NULL 值。NULL 与没有数据是不一样的,它代表着未知的数据。
DEFAULT 约束
DEFAULT 约束在 INSERT INTO 语句没有提供一个特定的值时,为列提供一个默认值。
UNIQUE 约束
UNIQUE 约束防止在一个特定的列存在两个记录具有相同的值。在 COMPANY 表中,例如,您可能要防止两个或两个以上的人具有相同的年龄。
INDEX 约束
普通索引
CREATE TABLE 语句中,您可以使用 INDEX 子句为表的某个列创建索引。以下是一个示例,演示了如何在创建表时为某个列创建索引:
联合索引
当在 MySQL 中创建表时,您可以使用 CREATE TABLE 语句,并通过 INDEX 子句定义联合索引。以下是一个更详细的示例,展示了如何创建一个表并添加联合索引:
在上面的示例中,需要将 your_table_name 替换为表的实际名称。然后,根据需要定义表的列,并将 column1、column2、column3 替换为实际的列名,以及 datatype 替换为相应的数据类型。
接下来,在 INDEX 子句中定义联合索引。您可以为联合索引指定一个名称,将 index_name 替换为索引的实际名称。然后,列出要包含在联合索引中的列名,按照所需的顺序列出。在示例中,我们使用 column1、column2 和 column3 列作为联合索引的组成部分。
需要注意的是,可以在一个 CREATE TABLE 语句中定义多个索引,包括单列索引和联合索引。每个索引都可以使用不同的列组合。如果要定义多个索引,请确保为每个索引指定唯一的名称。
通过使用适当的列和数据类型,以及定义所需的联合索引,可以根据您的表结构和查询需求来创建具有联合索引的表。
PRIMARY KEY 约束
PRIMARY KEY 约束唯一标识数据库表中的每个记录。在一个表中可以有多个 UNIQUE 列,但只能有一个主键。在设计数据库表时,主键是很重要的。主键是唯一的 ID。
使用主键来引用表中的行。可通过把主键设置为其他表的外键,来创建表之间的关系。由于"长期存在编码监督",在 SQLite 中,主键可以是 NULL,这是与其他数据库不同的地方。
主键是表中的一个字段,唯一标识数据库表中的各行/记录。主键必须包含唯一值。主键列不能有 NULL 值。
一个表只能有一个主键,它可以由一个或多个字段组成。当多个字段作为主键,它们被称为复合键。
如果一个表在任何字段上定义了一个主键,那么在这些字段上不能有两个记录具有相同的值。
CHECK 约束
CHECK 约束启用输入一条记录要检查值的条件。如果条件值为 false,则记录违反了约束,且不能输入到表。
删除约束
SQLite 支持 ALTER TABLE 的有限子集。在 SQLite 中,ALTER TABLE 命令允许用户重命名表,或向现有表添加一个新的列。重命名列,删除一列,或从一个表中添加或删除约束都是不可能的。
外键约束
在 SQLite 中启用外键功能需要进行一些设置。默认情况下,SQLite 中的外键功能是禁用的,我们需要手动开启它。在使用外键之前,我们需要确认 SQLite 版本是否支持外键功能。执行以下命令可以获取当前版本的 SQLite 支持情况:
如果返回的版本号中包含“foreign_key”,则说明 SQLite 支持外键功能。
下面是一个示例,演示了如何在 SQLite 中创建表并添加外键约束:
设计分析
CREATE 操作需要考虑表的字段属性,字段长度、是否为Null、是否有默认值、是否是主键、是否自增、用什么存储引擎、是否外键约束等等。
用框架已有的功能,那就是通过在模型结构体的字段中使用 tag 标识,通过解析结构体时检测。
所以必须在表元数据和列元数据的结构体中加上,表结构定义时候的 Tag 属性。
定义如下常见的eorm模型标签:
以下是示例结构体模型:
索引示例
联合索引示例
指定类型示例
默认值
字段注释
元数据模型,需要引入索引、约束、字段类型、默认值等复杂的结构体字段,来给上层的 build 语句构建语法
索引的解析,注意:索引的解析也是通过模型结构体的tag来解析的。
改造注册模型下的解析字段方法 parseFields 。
增加 FieldType 的目的是用于保存该字段再数据库中的实际类型,例如 “varchar”,在构建语句时根据DataType转换得来,默认DataType是根据结构体字段的类型,也支持使用者根据tag定义的类型。
当模型结构体中的tag出解析出类似与 “type:string”这部分描述时,DataType用来保存 string,之后的构建中将会转换成数据库中的实际类型,并保存在FieldType中。
定义一个 ColumnTypeInterface, 同时用来支持使用者直接自己定义数据库的字段的类型。
示例:
对于类型的转换,不同的数据库语法中的类型描述符是不一样的,所以需要改造方言模块,做成统一接口抽象,用于支持类型转换的1方法,也方便后续的方言相关功能扩展。
mysql 类型
sqlite 类型示例也是如此
最上层的 builder 实现
Drop
MySQL
删除表语句
DROP TABLE 删除一个或多个表。您必须具有 DROP 每个表的权限。
对于每个表,它将删除表定义和所有表数据。如果表已分区,则该语句将删除表定义、其所有分区、存储在这些分区中的所有数据以及与已删除的表关联的所有分区定义。删除表也会删除表的所有触发器。
DROP TABLE 导致隐式提交,除非与 TEMPORARY 关键字一起使用。
删除索引语句
DROP INDEX 删除表中命名 index_name 的索引 tbl_name 。此语句映射到要删除索引的 ALTER TABLE 语句。
要删除主键,索引名始终 PRIMARY 为 ,必须将其指定为带引号的标识符,因为 PRIMARY 它是一个保留字:
SQLite
SQLite 的 DROP TABLE 语句用来删除表定义及其所有相关数据、索引、触发器、约束和该表的权限规范。
删除表
删除索引
具体代码
Alter
MySQL
更改表的结构。例如,可以添加或删除列、创建或销毁索引、更改现有列的类型或者重命名列或表本身。还可以更改特征,例如用于表或表注释的存储引擎。
添加和删除列
用于ADD向表中添加新列以及 DROP删除现有列。是标准 SQL 的 MySQL 扩展。 DROP col_name
要在表行中的特定位置添加列,请使用 FIRST或。默认情况下最后添加列。 AFTER col_name
如果表仅包含一列,则无法删除该列。
如果从表中删除列,则这些列也会从它们所属的任何索引中删除。如果构成索引的所有列都被删除,则索引也会被删除。如果使用 CHANGE或MODIFY来缩短列上存在索引的列,并且结果列长度小于索引长度,MySQL 会自动缩短索引。
对于ALTER TABLE ... ADD,如果列具有使用不确定性函数的表达式默认值,则该语句可能会产生警告或错误。
重命名、重新定义
要更改列以更改其名称和定义,请使用 CHANGE,指定旧名称、新名称以及新定义。例如,要将INT NOT NULL列从a重 命名为b并将其定义更改为使用 BIGINT数据类型,同时保留 NOT NULL属性,请执行以下操作:
要更改列定义但不更改其名称,请使用 CHANGE或MODIFY。对于 CHANGE,语法需要两个列名称,因此您必须指定相同的名称两次才能保持名称不变。例如,要更改 column 的定义 b,请执行以下操作:
MODIFY更改定义而不更改名称更方便,因为它只需要一次列名:
要更改列名称但不更改其定义,请使用 CHANGE或RENAME COLUMN。对于CHANGE,语法需要列定义,因此要保持定义不变,您必须重新指定列当前具有的定义。例如,要将INT NOT NULL列从 重 命名b为a,请执行以下操作:
RENAME COLUMN更改名称而不更改定义更方便,因为它只需要旧名称和新名称:
通常,您不能将列重命名为表中已存在的名称。然而,有时情况并非如此,例如当您交换名称或在循环中移动它们时。如果表具有名为a、b和 的 列c,则这些是有效的操作:
CHANGE对于使用或 进行的列定义更改MODIFY,定义必须包括数据类型以及应应用于新列的所有属性(索引属性(例如PRIMARY KEY或 ) 除外UNIQUE)。原始定义中存在但未为新定义指定的属性不会被保留。假设列col1定义为,INT UNSIGNED DEFAULT 1 COMMENT 'my column'并且您按如下方式修改该列,打算仅更改INT为 BIGINT:
该语句将数据类型从 更改为INT ,BIGINT但也删除了 UNSIGNED、DEFAULT和 COMMENT属性。要保留它们,语句必须明确包含它们:
主键和索引
创建主键:
创建普通索引:
创建唯一索引:
删除索引:
外键和其他约束
FOREIGN KEY 子句由和存储引擎REFERENCES支持 ,“外键约束”。对于其他存储引擎,这些子句会被解析但被忽略。 InnoDB NDB ADD [CONSTRAINT [symbol]] FOREIGN KEY [index_name] (...) REFERENCES ... (...)
与 不同的是ALTER TABLE,对于 CREATE TABLE,如果给定则ADD FOREIGN KEY忽略_index_name_并使用自动生成的外键名称。作为解决方法,请包含CONSTRAINT用于指定外键名称的子句:
添加外键:
添加唯一约束:
MySQL 默默地忽略内联REFERENCES 规范,其中引用被定义为列规范的一部分。MySQL 只接受 REFERENCES作为单独FOREIGN KEY规范的一部分定义的子句。
分区InnoDB表不支持外键。此限制不适用于 NDB表,包括那些由[LINEAR] KEY. 有关更多信息,请参阅 第 24.6.2 节 “与存储引擎相关的分区限制”。
MySQL Server 和 NDB Cluster 都支持使用 ALTER TABLE删除外键:
ALTER TABLE支持 在同一语句中添加和删除外键, ALTER TABLE ... ALGORITHM=INPLACE但不支持 ALTER TABLE ... ALGORITHM=COPY。
服务器禁止对可能导致引用完整性丢失的外键列进行更改。ALTER TABLE ... DROP FOREIGN KEY解决方法是在更改列定义之前和之后使用ALTER TABLE ... ADD FOREIGN KEY。禁止更改的示例包括:
SQLlite
SQLite 支持 ALTER TABLE 的有限子集。SQLite 中的 ALTER TABLE 命令允许对现有表进行这些更改:它可以重命名;可以重命名列;可以向其中添加一列;或者可以从中删除一列。
在 SQLite 中,除了重命名表和在已有的表中添加列,ALTER TABLE 命令不支持其他操作。
重命名
用来重命名已有的表的 ALTER TABLE 的基本语法如下:
新增列
用来在已有的表中添加一个新的列的 ALTER TABLE 的基本语法如下:
测试发现 sqlite3 并不支持直接添加带有 unique 约束的列:
可以先直接添加一列数据,然后再添加 unique 索引。其实在建表的时候如果列有 unique 约束,通过查询系统表 SQLITE_MASTER 可以看到,会自动创建相应的索引。
SQLITE_TEMP_MASTER 跟 SQLITE_MASTER 是 sqlite 的系统表,SQLITE_TEMP_MASTER 表存储临时表有关的所有内容。
删除列
DROP COLUMN 语法用于从表中删除现有列。DROP COLUMN 命令从表中删除命名列,并重写其内容以清除与该列关联的数据。DROP COLUMN 命令仅在该列未被模式的任何其他部分引用并且不是 PRIMARY KEY 且没有 UNIQUE 约束时才有效
重新定义、外键和其他约束
请注意,不是所有的ALTER操作都在SQLite中都是支持的,例如修改列的约束等操作。SQLite的ALTER语句的功能相对有限。如果需要更复杂的表结构修改,可能需要使用其他方法,例如创建新表并将数据迁移至新表。
具体代码
对 DDL 封装
提供一个 Migrator 的接口, 该接口组装了 Creater、Drop、Alter 等DDL接口,除此之外对于表迁移的接口还需要将上述接口的的颗粒度细分为:修改表名、得到表类型、 修改列类型、修改列名、修改索引等接口。
为了满足ORM迁移工具类的需求,一个Migrator接口通常需要具备以下方法:
一般情况参数table应该是一个结构体类型,而不是一个接口。这是因为我们在创建表时需要明确指定表的结构和字段信息,而结构体能够提供这些信息的定义和描述。
通过传递一个代表表结构的结构体作为参数,Migrator接口可以根据结构体的定义,在数据库中创建相应的表。结构体可以灵活地定义表的字段、类型、约束和其他元数据信息,这样Migrator实现可以更准确地生成相应的SQL语句来创建表。
而接口通常用于定义一组方法,而不会提供具体的结构信息。因此,在创建表时,使用结构体作为参数更能满足需求。例如Migrator接口的CreateTable方法,应该将一个代表表结构的结构体作为参数传递进去。对于具体的ORM库和数据库类型。
通过使用接口,可以实现更高层次的抽象和灵活性。可以定义一个通用的表结构接口,然后针对不同的数据库和ORM库,实现具体的表结构结构体。
综上所述,使用接口来定义表结构是一种常见且有效的做法,可以提供更大的灵活性和可扩展性。
这些方法可以提供基本的表和列操作,从而满足 ORM 迁移工具类的需求。具体的实现方式可能因具体的ORM 库和数据库类型而有所差异。
在实现 Migrator** **接口时,需要根据具体的需求和使用的 ORM 库进行相应的调整和扩展。
Migrator 可用来为 ORM Schemas 实现自定义的迁移逻辑。Migrator 还为不同类型的数据库提供了统一的 API 抽象,例如:SQLite 不支持 ALTER COLUMN、DROP COLUMN 等 SQL 子句,所以当我们调用 Migrator API 试图修改表结构时,会自定为在 SQLite 创建一张新表、并复制所有数据,然后删除旧表、重命名新表。
那么 Migrate 的实现这里就主要参考gorm的实现。
创建一个 AutoMigrate 结构体,该结构体实现Migrator接口,接收一个数据库连接和要迁移的模型对象作为参数。
Run 方法
Run 方法为 Migrator 最核心的方法,也就是负责模型类迁移启动的方法
HasTable 方法
HasTable 判断表是否存在,返回布尔值。实现的核心是通过sql去查询目标表是否存在于当前连接的数据库中。
获得当前连接的数据库名称;
进行raw sql 拼接,查询目标表是否存在;
CreateTable 方法
基于 builder 创建数据库表;
ColumnTypes 方法
获取表结构的字段信息;
在目标表原先就已经建立的情况下,查找目前元数据模型的列, 将迁移动作分为了两个分支, :
以上两个分支需要当前表的表结构字段信息,为了和最新解析的元数据模型的信息做对比。
新增 ColumnType ,为字段信息的抽象;
实现 ColumnType,用于接收, sql/database 的表结构字段的信息。
实现 ColumnTypes 方法:
AddColumn 方法
为当前表添加列
MigrateColumn 方法
将目标列迁移到数据库中;
FullColumnTypeOf :获取模型元数据中的全部字段类型;
DatabaseTypeName :获取当前表该字段在数据库中的类型;
然后经过逐一对比,字段的属性,来判断字段是否需要被修改。
HasIndex 方法
判断在索引是否存在。
CreateIndex 方法
添加索引。
版本控制
为了先实现简单的DDL操作,和迁移封装,暂不支持版本控制。
单元测试
集成测试
The text was updated successfully, but these errors were encountered: