go语言中的数据库迁移

759 阅读5分钟

这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战

本文为译文,原文链接:www.calhoun.io/database-mi…

当从另一种严重依赖框架的语言过渡到Go时,很快就会觉得Go缺乏。就像它没有你所习惯的力量或工具一样。最终的结果是语言会让人感到困惑和笨拙。

其中一个例子就是数据库迁移工具。

如果你来自Rails、Django、Flask、Laravel或者任何一个web框架,你很可能会对我所说的有所了解。您已经习惯了一些工具,这些工具可以轻松地创建新表、修改现有表,甚至在列发生变化时修改数据库中的数据。

例如,在Ruby on Rails中,你可以修改一个用户表,添加一个新的receive_newsletter字段,代码如下:

class AddReceiveNewsletterToUsers < ActiveRecord::Migration
  def up
    change_table :users do |t|
      t.boolean :receive_newsletter, :default => false
    end
    User.update_all ["receive_newsletter = ?", true]
  end

  def down
    remove_column :users, :receive_newsletter
  end
end

在创建迁移之后,您还希望能够运行迁移并在必要时回滚它。

在Go中,这些工具都不存在。

公平地说,Ruby中也没有现成的工具。这个框架——Ruby on Rails——增加了这个功能。如果你真的想在Go中使用一个框架,很多都提供了迁移工具(例如Buffalo提供了迁移工具)。

如果您在这里,我将假设您没有使用Go中的框架,那么如果他们的工具不能提供解决方案,那么每个人如何处理迁移呢?

选择1:使用第三方库

第一个选项可能是您最常听到的——只需使用第三方库来处理这个问题。

其中的几个例子包括:

在几乎所有这些情况下,您都用SQL或Go编写迁移文件,然后使用工具安装的二进制文件来运行迁移。例如,goose支持SQL和Go迁移(尽管Go迁移可能需要一些额外的设置),你可以用如下代码运行迁移:

# run the migrations
goose up

# Output:
OK    001_basics.sql
OK    002_next.sql
OK    003_and_again.go

还有一些选项可以用于运行迁移到特定的迁移,回滚迁移等等。二进制文件中甚至经常有用于生成迁移文件的命令。

几乎所有这些工具都非常容易上手,而且从技术上讲,如果您只是编写SQL迁移,甚至不需要使用用Go编写的工具。

唯一的缺点是你投资的时间到另一个库,长期可能会或可能不会满足你的需求,此时你要么需要解决二进制库的局限性,或者你所有的时间投入到这个工具将会被浪费。

我的经验表明,这些迁移工具足够灵活,能够满足大多数需求,但仍然值得考虑。

选择2:自己写依赖库

第二种选择可能并不适合每个人,但我确实喜欢讨论它,因为它清楚地表明,我们经常如何把相对小的问题小题大做。

乍一看,迁移听起来很复杂,但我们大多数人需要的其实并不复杂。我的需求通常可以总结为:

  1. 我需要一种方法来运行迁移,跳过任何已经运行的迁移。
  2. 我需要一种方法来运行回滚,跳过任何从未运行过的迁移。
  3. 我需要一种方法来对迁移和回滚进行排序。

大多数迁移工具解决这三个问题的方法是将当前日期预先添加到迁移文件中,然后按字典顺序对迁移文件进行排序并按顺序运行它们。根据每个文件名创建标识符,以便更容易地跟踪哪些迁移已经运行,哪些没有运行。接下来就是创建一个migrations表来跟踪这些标识符。

知道了这就是我们真正需要的迁移工具,那么编写一个只完成我们需要的最低限度的东西有多难呢?

我很好奇,所以我写了一个我称之为migrate的工具,它可以进行基本的数据库迁移。

要使用这个库,首先要创建一个SqlxMigration数组的Sqlx实例。我没有对它们进行排序,而是使用数组中提供的顺序来确定顺序。这让最终用户决定是否订货。

SqlxMigration可以通过sql文件创建,也可以通过手工构造,为迁移和回滚定义func(tx *sqlx.Tx) error函数。这使得做额外的工作非常容易,比如在添加新列后转换数据。

sqlMigration := migrate.Sqlx{
  Migrations: []migrate.SqlxMigration{
    migrate.SqlxFileMigration("001_init", "migrations/001_init.sql", "migrations/001_init.undo.sql"),
    migrate.SqlxMigration{
      ID: "002_add_currency",
      Migrate: func(tx *sqlx.Tx) error {
        // add currency field, then fill existing entries with "USD"
        return nil
      },
      Rollback: func(tx *sqlx.Tx) error {
        // drop the currency field
        return nil
      },
    },
  },
}

因为我的迁移只是需要sqlx.Tx的函数运行时,Migrate函数是非常直接的。它首先确保有一个migrations表,然后对于每个迁移,它检查表中是否有包含每个迁移ID的条目。如果存在该条目,则跳过迁移。否则,将运行它并将ID插入表中。

回滚的工作原理非常类似,但逻辑相反。如果存在表项,则执行回滚并删除表项。

虽然我还没有天真到相信这段代码涵盖了我可能遇到的所有情况,但我发现,在意识到迁移并不是一项巨大的任务后,我对迁移的关注大大减少了,特别是当我只关注我需要开始的几个特性时。

下次当缺少工具阻碍您前进时,我强烈建议您花一点时间来看看为自己编写一个简化版本是否可行。有时候你会让自己大吃一惊😀