phpsymfonydoctrinedoctrine-migrations

How to handle migrations with multiple entity managers in DoctrineMigrationsBundle 3


On doctrine/doctrine-migrations-bundle 2.* this was relatively simple - use the --em option (and use ContainerAwareInterface to skip any migrations from a different em/connection).

Now (on doctrine/doctrine-migrations-bundle 3.2.2), it seems the --em option is ignored, and the default em/connection is always specified, meaning the migrations for the default em are applied to every database. Edit: As pointed out in comments - --em is not ignored, it's passed through directly, it's rather our ContainerAwareInterface approach that's no longer valid.

There is a lot of conflicting information on how to set this up, some suggesting it should "just work" (Symfony Docs) and other describing workarounds (Issue):

https://symfony.com/doc/current/doctrine/multiple_entity_managers.html https://github.com/doctrine/DoctrineMigrationsBundle/issues/38

How does one configure this new version (3) of doctrine/doctrine-migrations-bundle to apply migrations only to their matching entity/db?

Edit: I've included below our config previous to upgrading, which along with the ContainerAwareInterface connection filtering approach, allowed filtering migrations to run only against the appropriate entity manager.

Our existing "doctrine/doctrine-bundle": "1.12.8" config (shortened, but shows multiple entity managers):

doctrine:
    dbal:
        connections:
            default:
                charset: utf8mb4
                default_table_options:
                    charset: utf8mb4
                    collate: utf8mb4_unicode_ci
                driver: '%database_driver%'
                server_version: mariadb-10.4.11
                host: '%database_host%'
                port: '%database_port%'
                dbname: autotempest
                user: '%database_user%'
                password: '%database_password%'
                mapping_types:
                    enum: string
            model:
                charset: utf8mb4
                default_table_options:
                    charset: utf8mb4
                    collate: utf8mb4_unicode_ci
                driver: '%database_driver%'
                server_version: mariadb-10.4.11
                host: '%database_host%'
                port: '%database_port%'
                dbname: autotempest_models
                user: '%database_user%'
                password: '%database_password%'
                mapping_types:
                    enum: string
                wrapper_class: App\Doctrine\ConnectionWrapper\ConnectionModel
            persistent: true

    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        entity_managers:
            default:
                connection: default
                mappings:
                    App:
                        type: 'annotation'
                        dir: '%kernel.project_dir%/src/Entity/Main'
                        prefix: 'App\Entity\Main'
            model:
                connection: model
                mappings:
                    TempestModelBundle:
                        type: 'annotation'
                        dir: 'Entity'
                        prefix: 'Tempest\Bundle\ModelBundle\Entity'

Our "doctrine/doctrine-migrations-bundle": "2.1.2" config:

doctrine_migrations:
    dir_name: '%kernel.project_dir%/src/Migrations'
    namespace: Application\Migrations

Solution

  • Also mentioned in my question, there is an open issue on the DoctrineMigrationsBundle from 2012 describing the problem of dealing with migrations when using multiple entity managers: https://github.com/doctrine/DoctrineMigrationsBundle/issues/38. It seems there are several options for workarounds to this issue as described there, we just needed to dig and try each of them to find the best one for our situation.

    Container Aware Migrations

    On Symfony 3, we were using the ContainerAwareInterface approach. Described in the above issue:

    Currently this can be achieved by using Container Aware Migrations. If one can have the service container injected, he can obtain an instance of some entity manager and its connection.

    This is no longer really a valid solution when moving to Symfony 4 however, due to ContainerAware classes being deprecated in favor of dependency injection.

    Pass configuration directly

    Another approach mentioned in the github issue above. The idea here is to have a separate configuration file for each entity manager like the following:

    # config/packages/migrations/base.yaml
    em: default
    transactional: false
    migrations_paths:
        Hyra\Migrations\Base: src/Migrations/Base
    table_storage:
        table_name: migration_versions
    

    This is passed directly to the command, along with the entity manager like this: bin/console doctrine:migrations:migrate --em default --configuration config/packages/migrations/base.yaml. These separate config files replace the single config/packages/doctrine_migrations.yaml configuration file.

    This was also not viable for us, as we still needed to inject services into our migrations using the services configuration option of DoctrineMigrationsBundle, and --configuration only passes configuration options directly through to doctrine/migrations, which doesn't support the services configuration option.

    Initially on DoctrineMigrationsBundle 3.0, this approach was complicated by the fact that the --em and --conn options were dropped completely, so it was also necessary to create a wrapper on top of the DoctrineMigrationsBundle commands to re-implement these options (described in more detail here). This is no longer necessary on DoctrineMigrationsBundle 3.1+ (which restored these options).

    Use DoctrineMigrationsMultipleDatabaseBundle

    Also mentioned in the github issue thread, this bundle implements what we needed exactly (and ended up using) - per-entity configuration for DoctrineMigrationsBundle, so we can also include our services config for migration dependency injection. Initially I misconfigured this - it's important that the base doctrine_migrations.yaml config only includes config for the default entity manager. Sample working config provided by the package author (version 0.3.3):

    # doctrine.yaml
    doctrine:
        dbal:
            default_connection: default
            connections:
                default:
                    url: '%env(resolve:DATABASE_URL)%'
                    server_version: mariadb-10.1.26
                    charset: utf8mb4
                    default_table_options:
                        charset: utf8mb4
                        collate: utf8mb4_unicode_ci
                    mapping_types:
                        enum: string
                geonames:
                    url: '%env(resolve:GEONAMES_DATABASE_URL)%'
                    server_version: mariadb-10.1.26
                    charset: utf8mb4
                    default_table_options:
                        charset: utf8mb4
                        collate: utf8mb4_unicode_ci
                    mapping_types:
                        enum: string
    
            # IMPORTANT: You MUST configure your server version,
            # either here or in the DATABASE_URL env var (see .env file)
            #server_version: '5.7'
        orm:
            auto_generate_proxy_classes: true
            default_entity_manager: default
            entity_managers:
                default:
                    connection: default
                    naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                    mappings:
                        Main:
                            is_bundle: false
                            type: annotation
                            dir: '%kernel.project_dir%/src/App/Entity/Main'
                            prefix: 'App\Entity\Main'
                            alias: Main
                geonames:
                    naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                    connection: geonames
                    mappings:
                        Geonames:
                            is_bundle: false
                            type: annotation
                            dir: '%kernel.project_dir%/src/App/Entity/Geonames'
                            prefix: 'App\Entity\Geonames'
                            alias: Geonames
    
    # doctrine_migrations.yaml
    doctrine_migrations:
        em: default
        migrations_paths:
            DoctrineMigrations: '%kernel.project_dir%/migrations/Main'
    
    # doctrine_migrations_multiple_database.yaml
    doctrine_migrations_multiple_database:
        entity_managers:
            default:
                migrations_paths:
                    DoctrineMigrations\Main: '%kernel.project_dir%/migrations/Main'
            geonames:
                migrations_paths:
                    DoctrineMigrations\Geonames: '%kernel.project_dir%/migrations/Geonames'