check24-profis/fregata-symfony-bundle

数据库迁移框架,允许在不同数据库管理系统或数据库结构之间进行数据迁移。为Symfony提供捆绑。

v3.0.1 2022-12-16 15:57 UTC

README

Latest Stable Version License

Fregata 是一个数据迁移框架。您可以使用它迁移任何类型的数据,但它具有帮助您在不同数据库管理系统或数据库结构之间迁移的功能。

文档:

  1. 简介
  2. 设置
    1. 安装
    2. 配置
      1. YAML 配置
  3. 组件
    1. 迁移注册表
    2. 迁移
      1. 选项
      2. 父迁移
    3. 任务
    4. 迁移器
      1. 拉取器
      2. 推送器
      3. 执行器
  4. 工具
    1. 迁移上下文
  5. 功能
    1. 依赖迁移器
    2. 批量拉取
    3. 外键迁移
  6. CLI 使用
    1. 列出迁移
    2. 获取迁移的详细信息
    3. 执行迁移
  7. 贡献

简介

Fregata 是一个数据迁移框架。它可以与 ETL(提取 - 转换 - 加载) 工具进行比较。

您可以使用它迁移文件、数据库或任何您想要的东西,它在这一点上是完全无关的(它的测试迁移数据在PHP数组之间)。但请注意,它最初的目标是数据库,提供了一种在不同数据库管理系统之间迁移数据的方法,即使结构不同。一些内置功能专门为数据库构建。

为什么要创建一个数据迁移框架?

虽然数据库迁移可能不是您的日常任务,但我曾在不同的项目上多次遇到它。这就是我创建 Fregata 的原因,以便我有一个可以重复使用的迁移工作流程。

有哪些用例?

以下是一些用例示例(来自经验)

  • 当您想要从一种数据库管理系统迁移到另一种时
  • 当您想要将您的预发布数据库与生产数据库同步(对基于CMS的项目很有用)时

设置

安装

使用 Composer 安装

composer require check24-profis/fregata-symfony-bundle

配置

YAML 配置

要配置 Fregata 本身,您需要在您的配置目录中有一个 fregata.yaml 文件。

示例配置文件

fregata:
    migrations:
        # define any name for your migration
        main_migration:
            # define custom options for your migrations
            options:
                custom_opt: 'opt_value'
                special_cfg:
                    foo: bar
            # load migrators from a directory
            # use the %fregata.root_dir% parameter to define a relative path from the project root
            migrators_directory: '%fregata.root_dir%/src/MainMigration'
            # load individual migrators
            # can be combined with migrators_directory
            migrators:
                - App\MainMigration\FirstMigrator
            # load tasks to execute before or after the migrators
            tasks:
                before:
                    - App\MainMigration\BeforeTask
                after:
                    - App\MainMigration\AfterTask
            
        other_migration:
            # extend an other migration to inherit its options, tasks and migrators
            parent: main_migration
            # overwrite a part of the options
            options:
                custom_opt: 'another_value'
            # load additional migrators or tasks
            migrators:
                - App\OtherMigration\Migrator

组件

迁移注册表

迁移注册表 包含所有定义的迁移。您不需要与之交互。

迁移

迁移 项目包含迁移的步骤。例如,从您的生产数据库到预发布数据库的数据迁移。每个 迁移 都根据您的配置创建并保存在注册表中。您不需要自己实例化迁移对象。

迁移包含 任务迁移器。当运行迁移时,组件按以下顺序执行

  • 在任务之前
  • 迁移器
  • 在任务之后

选项

您可能需要为您的迁移项目设置特定的配置,这些配置可用于 任务迁移器。使用 options 键,您可以定义特定的迁移配置,它们将从 迁移上下文 访问。

父迁移

当有多个针对不同环境的 迁移 时,您可能想要避免重复整个配置。您可以使用 parent 键扩展迁移。“子” 迁移将继承父的 options任务迁移器。您仍然可以添加更多任务和迁移器,并覆盖选项。

任务

任务 可以在 迁移器 之前或之后执行。它们在启动迁移(在任务之前)或清理临时数据(在任务之后)时很有用。

use Fregata\Migration\TaskInterface;

class MyTask implements TaskInterface
{
    public function execute() : ?string
    {
        // perform some verifications, delete temporary data, ...
        return 'Optional result message';
    }
}

迁移器

迁移器 是框架的主要组件。单个迁移器包含 3 个组件

  • 一个 拉取器
  • 一个 推送器
  • 一个 执行器

它必须通过实现 Fregata\Migration\Migrator\MigratorInterface 来通过获取器方法返回其组件。一个 迁移器 表示数据从 目标 的迁移。例如,将数据从 MySQL 表迁移到 PostgreSQL 表。

拉取器

拉取器 是一个负责 从源拉取数据迁移器 组件。它返回数据以及可选的迁移项目数量

use Doctrine\DBAL\Connection;
use Fregata\Migration\Migrator\Component\PullerInterface;

class Puller implements PullerInterface
{
    private Connection $connection;
    
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function pull()
    {
        return $this->connection
            ->executeQuery('SELECT * FROM my_table')
            ->fetchAllAssociative();
    }
    
    public function count() : ?int
    {
        return $this->connection
            ->executeQuery('SELECT COUNT(*) FROM my_table')
            ->fetchColumn();
    }
}

推送器

推送器 获取 拉取器 获取的项,并必须将数据 推送到目标

use Doctrine\DBAL\Connection;
use Fregata\Migration\Migrator\Component\PusherInterface;

class Pusher implements PusherInterface
{
    private Connection $connection;
    
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }
    
    /**
     * @return int number of items inserted
     */
    public function push($data): int
    {
        return $this->connection->executeStatement(
            'INSERT INTO my_table VALUES (:foo, :bar, :baz)',
            [
                'foo' => $data['foo'],
                'bar' => some_function($data['bar']),
                'baz' => 'default value',
            ]
        );
    }
}

在这里,$data 是示例 拉取器 返回值中的一个单独的项目。push() 方法会被调用多次。将 拉取器推送器 分离,可以使您在不同的源之间进行迁移:从文件中拉取并推送到数据库等。

执行器

执行器 是一个将 拉取器推送器 连接起来的组件。默认提供的一个应该适用于大多数情况:Fregata\Migration\Migrator\Component\Executor。如果您需要特定的行为,可以扩展默认的 执行器

工具

迁移上下文

您可以通过在 任务迁移 中注入 Fregata\Migration\MigrationContext 服务来获取有关当前 迁移 的信息。

它提供

  • 当前 迁移 对象
  • 当前迁移 名称
  • 迁移 选项
  • 如果适用,则 迁移名称

功能

依赖迁移器

如果您的 迁移器 需要按照特定顺序执行,您可以定义依赖项,它们将被自动排序

use Fregata\Migration\Migrator\DependentMigratorInterface;

class DependentMigrator implements DependentMigratorInterface
{
    public function getDependencies() : array
    {
        return [
            DependencyMigrator::class,
        ];
    }
    
    // other migrator methods ...
}

在这里,DependencyMigrator 将在 DependentMigrator 之前执行。

批量拉取

拉取器 与非常大的数据集一起工作时,您可能希望按块拉取数据

use Doctrine\DBAL\Connection;
use Fregata\Migration\Migrator\Component\BatchPullerInterface;

class BatchPulling implements BatchPullerInterface
{
    private Connection $connection;
    private ?int $count = null;
    
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function pull(): \Generator
    {
        $limit = 50;
        $offset = 0;
        
        while ($offset < $this->count()) {
            yield $this->connection
                ->executeQuery(sprintf('SELECT * FROM my_table LIMIT %d, %d', $offset, $limit))
                ->fetchAllAssociative();
            
            $offset += $limit;
        }
    }
    
    public function count() : ?int
    {
        if (null === $this->count) {
            $this->count = $this->connection
                ->executeQuery('SELECT COUNT(*) FROM my_table')
                ->fetchColumn();
        }
        
        return $this->count;
    }
}

外键迁移

数据库迁移中最复杂的一部分是关于 外键。执行有效的外键迁移需要遵循多个步骤。这是使用 Doctrine DBAL 完成的。

您必须在迁移中添加 2 个任务

  • 前置 任务: Fregata\Adapter\Doctrine\DBAL\ForeignKey\Task\ForeignKeyBeforeTask
  • 后置 任务: Fregata\Adapter\Doctrine\DBAL\ForeignKey\Task\ForeignKeyAfterTask

前置任务将在目标数据库中创建临时列以保留原始引用和引用列。它还可以更改引用列以允许 NULL(仅当您指定时)。后置任务将设置原始引用列中的真实值,然后删除临时列。

然后迁移器必须提供数据库连接和外键列表

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Fregata\Adapter\Doctrine\DBAL\ForeignKey\ForeignKey;
use Fregata\Adapter\Doctrine\DBAL\ForeignKey\Migrator\HasForeignKeysInterface;

class ReferencingMigrator implements HasForeignKeysInterface
{
    private Connection $connection;
    
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }
    
    public function getConnection() : Connection
    {
        return $this->connection;
    }
    
    /**
     * List the foreign keys constraints to keep
     * @return ForeignKey[]
     */
    public function getForeignKeys() : array
    {
        $constraints = $this->connection->getSchemaManager()->listTableForeignKeys('my_table');
        return array_map(
            function (ForeignKeyConstraint $constraint) {
                return new ForeignKey(
                    $constraint,            // DBAL constraint object
                    'target_referencing',   // name of the referencing table
                    ['fk']                  // columns to change to allow NULL (will be set back to NOT NULL in the after task)
                );
            },
            $constraints
        );
    }
    
    // other migrator methods ...
}

迁移器负责数据迁移,这意味着您需要用源数据库中的原始主/外键填充临时列。要在 推送器 中获取临时列的名称,请在您的 推送器 中请求 CopyColumnHelper 服务

use Doctrine\DBAL\Connection;
use Fregata\Adapter\Doctrine\DBAL\ForeignKey\CopyColumnHelper;
use Fregata\Migration\Migrator\Component\PusherInterface;

class ReferencingForeignKeyPusher implements PusherInterface
{
    private Connection $connection;
    private CopyColumnHelper $columnHelper;
    
    public function __construct(Connection $connection, CopyColumnHelper $columnHelper)
    {
        $this->connection = $connection;
        $this->columnHelper = $columnHelper;
    }
    
    /**
     * @return int number of items inserted
     */
    public function push($data): int
    {
        return $this->connection->executeStatement(
            sprintf(
                'INSERT INTO my_table (column, %s) VALUES (:value, :old_fk)',
                $this->columnHelper->localColumn('my_table', 'fk_column')
            ),
            [
                'value' => $data['value'],
                'old_fk' => $data['fk_column'],
            ]
        );
    }
}

此示例显示 本地(或 引用)方面,但这也需要在 外键(或 引用)方面执行,使用 CopyColumnHelper::foreignColumn()

CLI 使用

Fregata 提供了一个简单的程序来运行迁移,您可以使用以下命令启动它:

php vendor/bin/fregata

列出迁移

要列出您的安装的迁移,请运行 migration:list 命令

> php vendor/bin/fregata migration:list

Registered migrations: 2
========================

main_migration
other_migration

获取迁移的详细信息

要获取有关单个迁移的信息,请运行 migration:show 命令

> php vendor/bin/fregata migration:show main_migration

main_migration : 1 migrators
============================

 --- --------------------------------- 
  #   Migrator Name                   
 --- --------------------------------- 
  0   App\MainMigration\FirstMigrator 
 --- --------------------------------- 

执行迁移

运行迁移最重要的命令:migration:execute

> php vendor/bin/fregata migration:execute main_migration

 Confirm execution of the "main_migration" migration ? (yes/no) [no]:
 > yes

                                                                                                                        
 [OK] Starting "main_migration" migration: 1 migrators                                                                  
                                                                                                                        

Before tasks: 1
===============

 App\MainMigration\BeforeTask : OK

Migrators: 1
============

0 - Executing "App\MainMigration\FirstMigrator" [3 items] :
===========================================================

 3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


After tasks: 1
==============

 App\MainMigration\AfterTask : OK

                                                                                                                        
 [OK] Migrated successfully !                                                                                           

贡献

有一个 Docker 设置可用,运行 make start 来启动服务,运行 make shellPHP 容器中打开命令行。

如果您想测试框架的实现(使用一个 Composer 路径仓库),请将其安装在项目根目录下的 _implementation 目录中,默认情况下Git会忽略该目录,这样可以确保您使用的是自己的实现自动加载器。