Erlang-pure-migrations 在 IMBoy 项目中的应用实践

发布于:2024-06-21 ⋅ 阅读:(151) ⋅ 点赞:(0)

在软件开发中,数据库的版本控制和迁移是一个复杂而关键的任务。在本文中,我们将详细介绍在IMBoy项目中如何应用erlang-pure-migrations这个工具来构建一个健壯的数据库迁移功能。

什么是 erlang-pure-migrations?

erlang-pure-migrations 是一个Erlang语言的强大而灵活的数据库迁移工具,它帮助开发者以纯函数的方式管理PostgreSQL或MySQL数据库的迁移;

它遵循Unix哲学,即“一切皆文件”,将数据库迁移脚本视为文件系统中的普通文件,它允许开发者使用纯SQL语句来编写迁移脚本,而无需编写任何Erlang代码;这不仅简化了迁移过程,而且提高了代码的可读性和可维护性。

https://github.com/bearmug/erlang-pure-migrations

IMBoy 项目中应用 erlang-pure-migrations

环境准备

首先,我们将erlang-pure-migrations作为依赖项添加到IMBoy项目的Makefile文件中,并确保所有开发和生产环境都包含了这个库。

DEPS += epgsql pooler pure_migrations

IMBoy使用的是PostgreSQL15+,链接数据库依赖了 epgsql;

为了安全,特别为 erlang-pure-migrations 新增了一个操作数据库的 super_account 账号,配置参考 ./config/sys.config 100行

        , {super_account, #{
            host => "localhost"
            , username => "imboy_user"
            , password => "123456"
            , database => "imboy_v1"
            , port => 5432
            , ssl => false
            , timeout => 4000
        }}

通过配置 scripts_path 目录来指定脚本的存放目录

        , {scripts_path, "./doc/postgresql/migrations"}

迁移脚本编写

迁移脚本以数字为前缀版本号.sql为文件后缀版本号和其他部分用下划线 _ 分割,并且按照严格的顺序编号。

"<VersionNum>_<FileNameDescription>.sql"

例如:

00000000_config.sql
00000001_app_version.sql
00000002_app_ddl.sql
...

每个脚本文件中包含了数据库迁移所需的所有SQL语句。

  • 以8位数字为前缀版本号(这是IMBoy项目的约定)
  • .sql为文件后缀
(imboy@127.0.0.1)2> string:to_integer("00000001_app_version.sql").
{1,"_app_version.sql"}

版本号前缀通过上面的代码转为int数据存在 int4(postgresql15是这样的)中,(int4数据大小范围为-2,147,483,648 to 2,147,483,647),所以版本号的最大值是 2,147,483,647

自动从0递增,相信不用考虑版本号不够的问题。

迁移流程

  1. 配置迁移路径:指定迁移脚本所在的文件夹路径。
  2. 编写迁移脚本:根据数据库变更编写SQL脚本。
  3. 执行迁移:使用pure_migrations:migrate/3函数执行迁移。这个函数接受迁移路径、事务处理器和数据库查询执行处理器作为参数。

在IMBoy项目中,运行 imboy_db:migrate(). 即可()

IMBOYENV=local gmake run
(imboy@127.0.0.1)1> imboy_db:migrate().
ok

以后打算添加一个 ./imboy migrate:run 的命令

事务和查询处理器

erlang-pure-migrations允许我们自定义事务和查询处理器。这意味着我们可以根据不同的数据库客户端库(如epgsql、pgsql等)来实现这些处理器,以确保迁移过程中的事务性和查询的正确执行。

IMBoy 用法如下 imboy_db:migrate():

% imboy_db:migrate().
migrate() ->
    Conf = config_ds:env(super_account),
    Path = config_ds:env(scripts_path),
    {ok, Conn} = epgsql:connect(Conf),
    MigrationCall =
      pure_migrations:migrate(
        Path,
        fun(F) -> epgsql:with_transaction(Conn, fun(_) -> F() end) end,
        fun(Q) ->
          case epgsql:squery(Conn, Q) of
            {ok, [{column, <<"version">>, _, _, _, _, _, _, _},
                   {column, <<"filename">>, _, _, _, _, _, _, _}], []} ->
                    [];
            {ok, [{column, <<"version">>, _, _, _, _, _, _, _},
                   {column, <<"filename">>, _, _, _, _, _, _, _}], Data} ->
                [{list_to_integer(binary_to_list(BinV)), binary_to_list(BinF)} || {BinV, BinF} <- Data];
            {ok, [{column, <<"max">>, _, _, _, _, _, _, _}], [{null}]} ->
                % It has to be -1 or it will get an error during initialization
                -1;
            {ok, [{column, <<"max">>, _, _, _, _, _, _, _}], [{N}]} ->
                % The version number is stored in the int4 type and ranges from -2,147,483,648 to 2,147,483,647
              list_to_integer(binary_to_list(N));

            {ok, [
              {column, <<"version">>, _, _, _, _, _},
              {column, <<"filename">>, _, _, _, _, _}], Data} ->
                [{list_to_integer(binary_to_list(BinV)), binary_to_list(BinF)} || {BinV, BinF} <- Data];
            {ok, [{column, <<"max">>, _, _, _, _, _}], [{null}]} -> -1;
            {ok, [{column, <<"max">>, _, _, _, _, _}], [{N}]} ->
              list_to_integer(binary_to_list(N));
            {ok, _, _} -> ok;
            {ok, _} -> ok;
            Default ->
                % Match multiple SQL statements in a script
                Res = priv_is_valid(Default),
                % io:format("DefaultDefaultDefaultDefaultDefault ~p~n", [Default]),
                case Res of
                    true->
                        ok;
                    _ ->
                        Default
                end
          end
        end),
    % ...
    %% more preparation steps if needed
    % ...
    %% migration call
    Res = MigrationCall(),
    % imboy_log:debug(io:format("~p~n", [Res])),
    ok = epgsql:close(Conn),
    Res.

priv_is_valid(List) ->
    lists:all(fun(E) ->
        case E of
            {ok, _} -> true;
            {ok, _, _} -> true;
            _ -> false
        end
    end, List).

更多其他,使用案例参考 https://github.com/bearmug/erlang-pure-migrations/tree/master/test

兼容性和集成

erlang-pure-migrations与多种Erlang“数据库客户端库”兼容,我们已经在IMBoy项目中成功集成了epgsql。

并且erlang-pure-migrations完全不依赖第三方库,代码变得非常轻量,并且与特定“数据库客户端库”分离,这为我们提供了灵活的选择,可以根据项目需求和团队熟悉度来选择合适的数据库客户端。

无副作用的迁移

  • erlang-pure-migrations采用了无副作用的编程范式,将所有副作用(如文件操作、数据库事务)推迟到程序的边缘。这使得迁移代码更加安全,可以在不同的环境和条件下重复执行,而不会引起副作用。

  • 迁移逻辑是幂等的,可以使用相同的迁移脚本集针对同一个数据库执行多次。

  • 同时迁移数据库也是事务安全的。

函数式编程抽象

erlang-pure-migrations使用了函数式编程中的一些抽象概念,如函数组合、functor应用和部分函数应用。这些抽象使得迁移逻辑更加清晰和灵活。

结语

通过在IMBoy项目中应用erlang-pure-migrations,我们成功构建了一个健壯、灵活且易于维护的数据库迁移功能。它不仅提高了开发效率,而且确保了数据库迁移的安全性和一致性。

通过这篇文章,我们希望能够帮助更多Erlang开发者了解并应用erlang-pure-migrations,以实现高效、安全的数据库迁移管理。

同时欢迎关注 IMBoy开源项目 https://gitee.com/imboy-pub

许可

IMBoy项目中应用的erlang-pure-migrations库遵循MIT许可协议,具体细节请参考LICENSE文件。


网站公告

今日签到

点亮在社区的每一天
去签到