aymdev / fregata
数据库迁移框架,允许在不同DBMS或数据库结构之间进行数据迁移。
Requires
- php: >=7.4
- doctrine/dbal: ^2.10
- hanneskod/classtools: ^1.2
- marcj/topsort: ^2.0
- symfony/config: ^4.4||^5.0||^6.0
- symfony/console: ^4.4||^5.0||^6.0
- symfony/dependency-injection: ^4.4||^5.0||^6.0
- symfony/string: ^5.0||^6.0
- symfony/yaml: ^4.4||^5.0||^6.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^0.7.1
- mikey179/vfsstream: ^1.6
- phpcompatibility/php-compatibility: ^9.3
- phpstan/phpstan: ^1.2
- phpstan/phpstan-phpunit: ^1.0
- phpstan/phpstan-strict-rules: ^1.1
- phpunit/phpunit: ^9.1
- sebastian/phpcpd: ^6.0
- squizlabs/php_codesniffer: ^3.6
README
Fregata是一个数据迁移框架。您可以使用它来迁移任何类型的数据,但它具有帮助您在不同DBMS或数据库结构之间迁移的功能。
文档:
简介
Fregata是一个数据迁移框架。它可能与ETL(提取-转换-加载)工具相似。
您可以使用它来迁移文件、数据库或任何您想要的数据,在这方面它是完全无差别的(其中一些测试迁移数据在PHP数组之间)。但请注意,它最初是针对数据库的,提供了一种在不同DBMS之间迁移数据的方法,即使它们的结构不同。一些包含的功能是专门为数据库构建的。
为什么要创建数据迁移框架?
虽然数据库迁移可能不是您日常的任务,但我多次在不同的项目上遇到了它。这就是为什么我创建了Fregata,以便我可以重用迁移工作流程。
有哪些用例?
以下是一些用例示例(来自经验)
- 当您想从一种DBMS切换到另一种DBMS时
- 当您想将预发布数据库与生产数据库同步时(适用于基于CMS的项目)
设置
安装
使用Composer安装
composer require aymdev/fregata
配置
Fregata默认期望您在项目根目录中有一个config
和一个cache
目录。
内核和服务容器
如果您需要使用与默认结构不同的目录结构,您可以扩展Fregata\Configuration\AbstractFregataKernel
类。然后您将需要实现指定您的配置和缓存目录的方法。
重要:您的内核完全限定类名必须为
App\FregataKernel
。
内核包含一个服务容器,由Symfony的DependencyInjection组件构建。这意味着您可以在配置目录中的services.yaml
文件中像在Symfony应用程序中那样定义自己的服务。
以下是一个推荐的最小services.yaml
以启动您的项目
services: _defaults: autowire: true App\: resource: '../src/'
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、tasks和migrators。您仍然可以添加更多任务和迁移器,并覆盖选项。
任务
任务可以在执行迁移器之前或之后执行。它们在启动迁移(在任务之前)或清理临时数据(在任务之后)时非常有用。
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
从getter方法返回其组件。迁移器代表从源到目标的迁移。例如,将数据从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使用
飞燕 提供了一个简单的程序来运行迁移,您可以通过以下方式启动它:
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 配置,提供 MySQL 5.7 服务。
如果您想测试框架的实现(使用 Composer 路径仓库),在项目的根目录下安装到 _implementation
目录,默认情况下 Git 会忽略它,并确保您正在使用自己的实现自动加载器。