effiana / migration-bundle
BAP的架构和数据迁移
Requires
- php: ^7.2 | ^7.4 | ^8.0
- doctrine/common: ^2.10 | ^3
- doctrine/data-fixtures: ^1.4
- doctrine/dbal: ^2 | ^3
- doctrine/doctrine-bundle: ^2 | ^3
- doctrine/orm: ^2 | dev-2.8.x-dev
- psr/container: ^1.0
- symfony/config: ^4.3 | ^5
- symfony/console: ^4.3 | ^5
- symfony/dependency-injection: ^4.3 | ^5
- symfony/event-dispatcher: ^4.3 | ^5
- symfony/finder: ^4.3 | ^5
- symfony/framework-bundle: ^4.2 | ^5
- symfony/http-kernel: ^4.3 | ^5
- twig/twig: ^3.0
Requires (Dev)
- phpunit/phpunit: ^8.5
- roave/security-advisories: dev-master
README
EffianaMigrationBundle 扩展了 DBAL(数据库抽象层),并提供了使用迁移和固定数据类以一致、结构化的方式管理应用程序数据库架构变化的能力。
数据库结构迁移
每个包都可以有自己的迁移文件,允许更新数据库架构。
迁移文件应位于 Migrations\Schema\version_number
文件夹中。版本号必须是 PHP 标准化的版本号字符串,但有一些限制。这个字符串不能包含 "." 和 "+" 字符作为版本部分分隔符。有关 PHP 标准化版本号字符串的更多信息,请参阅 PHP 手册。
每个迁移类必须实现 Migration 接口,并必须实现 up
方法。该方法接收当前数据库结构作为 schema
参数,以及可以用于添加附加查询的 queries
参数。
使用 schema
参数,您可以在不担心数据库引擎兼容性的情况下创建或更新数据库结构。如果您想在应用架构修改前后执行额外的 SQL 查询,可以使用 queries
参数。此参数代表一个 查询包,允许添加将在应用前(使用 addPreQuery
方法)或应用后(使用 addQuery
或 addPostQuery
方法)执行的附加查询。查询可以是字符串或实现 MigrationQuery 接口的类的实例。该接口有几种现成的实现
- SqlMigrationQuery - 表示一个或多个 SQL 查询
- ParametrizedSqlMigrationQuery - 与前面的类类似,但每个查询都可以有自己的参数。
如果您需要创建自己的 MigrationQuery 实现,可以使用 ConnectionAwareInterface。只需在需要数据库连接的迁移查询类中实现此接口即可。您还可以使用 ParametrizedMigrationQuery 类作为迁移查询的基础类。
如果您在同一版本中有多个迁移类,并且需要确保它们按指定顺序执行,可以使用 OrderedMigrationInterface 接口。
迁移文件示例
<?php namespace Acme\Bundle\TestBundle\Migrations\Schema\v1_0; use Doctrine\DBAL\Schema\Schema; use Effiana\MigrationBundle\Migration\Migration; use Effiana\MigrationBundle\Migration\QueryBag; use Effiana\MigrationBundle\Migration\Extension\RenameExtension; use Effiana\MigrationBundle\Migration\Extension\RenameExtensionAwareInterface; class AcmeTestBundle implements Migration, RenameExtensionAwareInterface { /** * @var RenameExtension */ protected $renameExtension; /** * @inheritdoc */ public function setRenameExtension(RenameExtension $renameExtension) { $this->renameExtension = $renameExtension; } /** * @inheritdoc */ public function up(Schema $schema, QueryBag $queries) { $table = $schema->createTable('test_table'); $table->addColumn('id', 'integer', ['autoincrement' => true]); $table->addColumn('created', 'datetime', []); $table->addColumn('field', 'string', ['length' => 500]); $table->addColumn('another_field', 'string', ['length' => 255]); $table->setPrimaryKey(['id']); $this->renameExtension->renameTable( $schema, $queries, 'old_table_name', 'new_table_name' ); $queries->addQuery( "ALTER TABLE another_table ADD COLUMN test_column INT NOT NULL", ); } }
每个包都可以有一个安装文件。这个迁移文件替换了运行多个迁移文件。安装迁移类必须实现 安装 接口,并必须实现 up
和 getMigrationVersion
方法。 getMigrationVersion
方法必须返回此安装文件替换的最大迁移版本号。
在安装过程中(这意味着您从头开始安装系统),如果找到安装迁移文件,它将首先加载,然后加载由 getMigrationVersion
方法返回的版本号更大的迁移文件。
例如。我们有 v1_0
、v1_1
、v1_2
、v1_3
迁移。此外,我们还有一个安装迁移类。此类返回 v1_2
作为迁移版本。因此,在安装过程中,将加载安装迁移文件,然后仅加载 v1_3
迁移文件。从 v1_0
到 v1_2
的迁移将不会加载。
安装迁移文件的示例
<?php namespace Acme\Bundle\TestBundle\Migrations\Schema; use Doctrine\DBAL\Schema\Schema; use Effiana\MigrationBundle\Migration\Installation; use Effiana\MigrationBundle\Migration\QueryBag; class AcmeTestBundleInstaller implements Installation { /** * @inheritdoc */ public function getMigrationVersion() { return 'v1_1'; } /** * @inheritdoc */ public function up(Schema $schema, QueryBag $queries) { $table = $schema->createTable('test_installation_table'); $table->addColumn('id', 'integer', ['autoincrement' => true]); $table->addColumn('field', 'string', ['length' => 500]); $table->setPrimaryKey(['id']); } }
要运行迁移,有 effiana:migration:load 命令。此命令从包中收集迁移文件,按版本号排序并应用更改。
此命令支持一些附加选项
- force - 导致由迁移生成的 SQL 语句物理执行到您的数据库;
- dry-run - 输出迁移列表而不应用它们;
- show-queries - 输出每个迁移文件的数据库查询列表;
- bundles - 要加载数据的包列表。如果未设置选项,则从所有包中获取迁移;
- exclude - 要跳过的迁移包名称列表。
此外,还有 effiana:migration:dump 命令,有助于创建安装文件。此命令将当前数据库结构输出为纯 SQL 或 Doctrine\DBAL\Schema\Schema
查询。
此命令支持一些附加选项
- plain-sql - 以纯 SQL 查询输出模式
- bundle - 将生成迁移的包名称
- migration-version - 迁移版本号。此选项将设置由生成的安装文件的
getMigrationVersion
方法返回的值。
对于包来说,有一个当前版本的安装文件和从先前版本迁移到当前版本的迁移文件是一个好的做法。
以下算法可用于您包的新版本
- 创建新的迁移
- 使用 effiana:migration:load 应用它
- 使用 effiana:migration:dump 生成新的安装文件
- 如果需要 - 向生成的安装文件中添加迁移扩展调用。
数据库结构迁移的扩展
有时您不能使用标准的 Doctrine 方法来修改数据库结构。例如,Schema::renameTable
不工作,因为它删除了现有的表然后创建了一个新的表。为了帮助您处理此类情况并向任何迁移添加一些有用的功能,设计了一种扩展机制。以下示例显示了如何使用 RenameExtension。
<?php namespace Acme\Bundle\TestBundle\Migrations\Schema\v1_0; use Doctrine\DBAL\Schema\Schema; use Effiana\MigrationBundle\Migration\Migration; use Effiana\MigrationBundle\Migration\QueryBag; use Effiana\MigrationBundle\Migration\Extension\RenameExtension; use Effiana\MigrationBundle\Migration\Extension\RenameExtensionAwareInterface; class AcmeTestBundle implements Migration, RenameExtensionAwareInterface { /** * @var RenameExtension */ protected $renameExtension; /** * @inheritdoc */ public function setRenameExtension(RenameExtension $renameExtension) { $this->renameExtension = $renameExtension; } /** * @inheritdoc */ public function up(Schema $schema, QueryBag $queries) { $this->renameExtension->renameTable( $schema, $queries, 'old_table_name', 'new_table_name' ); } }
如您所见,要使用 RenameExtension,您的迁移类应实现 RenameExtensionAwareInterface 和 setRenameExtension
方法。此外,还有一些您可以在迁移类中使用的一些其他有用的接口
ContainerAwareInterface
- 提供对 Symfony 依赖容器的访问- DatabasePlatformAwareInterface - 允许编写数据库类型无关的迁移
- NameGeneratorAwareInterface - 提供对 DbIdentifierNameGenerator 类的访问,该类可用于生成索引、外键约束等的名称。
为数据库结构迁移创建自己的扩展
要创建自己的扩展,您需要执行以下简单的步骤
- 在
YourBundle/Migration/Extension
目录中创建一个扩展类。使用YourBundle/Migration/Extension
目录不是强制的,但强烈推荐。例如
<?php namespace Acme\Bundle\TestBundle\Migration\Extension; use Doctrine\DBAL\Schema\Schema; use Effiana\MigrationBundle\Migration\QueryBag; class MyExtension { public function doSomething(Schema $schema, QueryBag $queries, /* other parameters, for example */ $tableName) { $table = $schema->getTable($tableName); // highly recommended to make sure that a table exists $query = 'SOME SQL'; /* or $query = new SqlMigrationQuery('SOME SQL'); */ $queries->addQuery($query); } }
- 在同一个命名空间中创建
*AwareInterface
。重要的是接口名称应该是{ExtensionClass}AwareInterface
,并且设置方法应该是set{ExtensionClass}({ExtensionClass} ${extensionName})
。例如
<?php namespace Acme\Bundle\TestBundle\Migration\Extension; /** * MyExtensionAwareInterface should be implemented by migrations that depends on a MyExtension. */ interface MyExtensionAwareInterface { /** * Sets the MyExtension * * @param MyExtension $myExtension */ public function setMyExtension(MyExtension $myExtension); }
- 在依赖注入容器中注册扩展。例如
parameters: acme_test.migration.extension.my.class: Acme\Bundle\TestBundle\Migration\Extension\MyExtension services: acme_test.migration.extension.my: class: %acme_test.migration.extension.my.class% tags: - { name: effiana_migration.extension, extension_name: test /*, priority: -10 - priority attribute is optional an can be helpful if you need to override existing extension */ }
如果您需要访问数据库平台或名称生成器,您应该相应地实现 DatabasePlatformAwareInterface 或 NameGeneratorAwareInterface。此外,如果您需要在您的扩展中使用其他扩展,扩展类只需实现所需的扩展的 *AwareInterface
即可。
数据固定
Symfony 允许使用数据固定加载数据。但是,这些固定每次执行 doctrine:fixtures:load
命令时都会运行。
为了避免多次加载相同的固定,创建了 effiana:migration:data:load 命令。此命令保证每个数据固定只加载一次。
此命令支持两种类型的迁移文件:main
数据固定和 demo
数据固定。在安装过程中,用户可以选择是否加载演示数据。
此命令的数据固定应放在 Migrations/Data/ORM
或 Migrations/Data/Demo/ORM
目录中。
固定顺序可以使用标准的 Doctrine 排序或依赖功能进行更改。有关固定排序的更多信息,请参阅 doctrine 数据固定手册。
版本化固定
有一些固定需要反复执行。例如,上传国家数据的固定。通常,如果您添加了新的国家列表,您需要创建新的数据固定来上传这些数据。为了避免这种情况,您可以使用版本化数据固定。
要使固定版本化,该固定必须实现 VersionedFixtureInterface 和返回固定数据版本的 getVersion
方法。
示例
<?php namespace Acme\DemoBundle\Migrations\DataFixtures\ORM; use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\Persistence\ObjectManager; use Effiana\MigrationBundle\Fixture\VersionedFixtureInterface; class LoadSomeDataFixture extends AbstractFixture implements VersionedFixtureInterface { /** * {@inheritdoc} */ public function getVersion() { return '1.0'; } /** * {@inheritdoc} */ public function load(ObjectManager $manager) { // Here we can use fixture data code which will be run time after time } }
在此示例中,如果固定尚未加载,则将加载它,并将版本 1.0 保存为该固定当前加载的版本。
为了有重新加载此固定的可能性,固定必须返回一个大于 1.0 的版本,例如 1.0.1 或 1.1。版本号必须是一个 PHP 标准化的版本号字符串。有关 PHP 标准化版本号字符串的更多信息,请参阅 PHP 手册。
如果固定需要知道最后加载的版本,则必须实现 LoadedFixtureVersionAwareInterface 和 setLoadedVersion
方法
<?php namespace Acme\DemoBundle\Migrations\DataFixtures\ORM; use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\Persistence\ObjectManager; use Effiana\MigrationBundle\Fixture\VersionedFixtureInterface; use Effiana\MigrationBundle\Fixture\RequestVersionFixtureInterface; class LoadSomeDataFixture extends AbstractFixture implements VersionedFixtureInterface, LoadedFixtureVersionAwareInterface { /** * @var $currendDBVersion string */ protected $currendDBVersion = null; /** * {@inheritdoc} */ public function setLoadedVersion($version = null) { $this->currendDBVersion = $version; } /** * {@inheritdoc} */ public function getVersion() { return '2.0'; } /** * {@inheritdoc} */ public function load(ObjectManager $manager) { // Here we can check last loaded version and load data data difference between last // uploaded version and current version } }