Mehrere Datenbanken unter Symfony

In einem Projekt bestand die Notwendigkeit, die applikationsspezifische Datenbank (MariaDB) und eine externen Datenbank (MS-SQL) gleichzeitig zu verwenden. An der Datenbankstruktur war nichts zu drehen, da die MS-SQL Datenbank in anderen Projekten ebenfalls im Produktivbetrieb verwendet wurde. Somit bestand das Problem, zwei Datenbankverbindungen parallel offen zu halten und je nach Entity-Klasse die richtige Datenbank zu verwenden.

Wie man sich unter Linux mit einer MS-SQL Datenbank verbindet, habe ich bereits im Artikel MS-SQL mit Symfony unter Linux im Detail beschrieben. In diesem Artikel geht es vornehmlich um den gleichzeitigen Betrieb zweier Datenbank und die automatische Auswahl der korrekten Datenbankverbindung.

Zwei Datenbanken parallel betreiben

Hierfür habe ich zur einfacheren Übersicht in den Verzeichnissen src/Entity und src/Repository ein Unterverzeichnis MsSql angelegt, in welche die Klassen der externen Datenbank gelegt werden. Dieser Schritt erhöht lediglich die Übersicht für menschliche Betrachter; Symfony/Doctrine ist das egal.

Somit ergeben sich die Verzeichnisse

Verzeichnis Verwendung
src/Entity Entity-Klassen der MariaDB-Datenbank
src/Entity/MsSql Entity-Klassen der MS-SQL-Datenbank
src/Repository Repository-Klassen der MariaDB-Datenbank
src/Repository/MsSql Repository-Klassen der MS-SQL-Datenbank

Zudem habe ich in der Datei doctrine.yaml zwei Verbindungen und zwei Entity-Manager definiert:

doctrine:
    dbal:
        default_connection:       default
        connections:
            default:
                driver: pdo_mysql
                dbname:           "%env(resolve:DATABASE_DB)%"
                user:             "%env(resolve:DATABASE_USER)%"
                password:         "%env(resolve:DATABASE_PASS)%"
                host:             "%env(resolve:DATABASE_HOST)%"
                port: 3306
                schema_filter: ~^(?!tt[a-z]{4}[0-9]{6})~  # Ignore Mssql tables
            mssql:
                # IMPORTANT: this configuration requires FreeTDS installed!
                # See doc/OdbcMssql.md for details!
                driver_class: App\Lib\OdbcMssql\Driver
                driver: pdo_mssql
                dbname:           "%env(resolve:MSSQL_DB)%"
                user:             "%env(resolve:MSSQL_USER)%"
                password:         "%env(resolve:MSSQL_PASS)%"
                host:             "%env(resolve:MSSQL_HOST)%"
                port: 1433
                # Eigentlich könnte FreeTDS hier schon eine automatische Konvertierung in UTF-8 vornehmen. Leider
                # speichert unser MSSQL aber Strings als b"Dies ist ein String", also als byte[] und nicht als String.
                # Daher schlägt die Konvertierung fehl und muss in den Entities manuell per iconv erfolgen.
                options: 
                  'client_charset': 'WINDOWS-1252'
                server_version: '2008'
        # IMPORTANT: You MUST configure your server version,
        # either here or in the DATABASE_URL env var (see .env file)
        #server_version: '13'
    orm:
        auto_generate_proxy_classes: true
        default_entity_manager: default
        entity_managers:
            default:
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                auto_mapping: true
                mappings:
                  App:
                      is_bundle: false
                      type: annotation
                      dir: '%kernel.project_dir%/src/Entity'
                      prefix: 'App\Entity'
                      alias: App
            mssql:
                connection: mssql
                naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
                mappings:
                  Mssql:
                      is_bundle: false
                      type: annotation
                      dir: '%kernel.project_dir%/src/Entity/Mssql'
                      prefix: 'App\Entity\Mssql'
                      alias: Mssql

Die Verwendung von den Einzelnen Feldern Host, User, Pass und DB erfolgt hier absichtlich, da DATABASE_URL nicht sauber mit MS-SQL funktioniert.

Da die doctrine.yml auf verschiedene .env Variablen verweist, werden diese noch ergänzt (und mit sinnvollen Werten belegt).

DATABASE_HOST=www.example.com
DATABASE_USER=user
DATABASE_PASS=define-in-.env.local
DATABASE_DB=database1

MSSQL_HOST=www.anotherexample.com
MSSQL_USER=anotheruser
MSSQL_PASS=define-in-.env.local
MSSQL_DB=database2

Die Klasse App\Lib\OdbcMssql\Driver findet sich bereits in dem vorgehenden Tutorial MS-SQL mit Symfony unter Linux.

An diesem Punkt weiss Symfony, dass wir zwei Datenbanken haben und wir können die Datenbanken manuell auswählen. Das ist aber umständlich, fehleranfällig und verhindert, dass wir Entitäten aus der MS-SQL Instanz über Injection holen.

Automatische Auswahl der korrekten Datenbank

Damit die Magie volle Fahrt aufnimmt, müssen wir Symfony die Möglichkeit geben, automagisch die richtige Verbindung zu verwenden. Die Idee ist relativ einfach. Wir modifizieren die Repository-Klassen derart, dass der EntityManagerDecorator für die korrekte Datenbank verwendet wird. Dafür vergewaltigen das Autowiring.

Zuerst definieren wir uns eine Repository-Klasse, die später nicht mehr die Default-Verbindung verwenden soll:

<?php
namespace App\Repository\Infor;

use App\Entity\Mssql\MssqlArticle;
use App\Lib\OdbcMssql\OdbcEntityManager;

class MssqlArticleRepository extends EntityRepository
{
    public function __construct(OdbcEntityManager $em)
    {
        parent::__construct($em, $em->getClassMetadata(MssqlArticle::class));
    }
}

Während ein normaler Konstruktor einer Repository-Klasse ein Objekt von Typ ManagerRegistry haben möcht, will unser Repository ein OdbcEntityManager haben. Das Autowiring wird nun alles tun, um unserer Bitte nachzukommen. Die Klasse src/Lib/OdbcMssql/OdbcEntityManager.php sieht dabei herzlich unspektakulär aus; wir brauchen sie nur, damit das Autowiring das richtige für uns tut:

<?php

namespace App\Lib\OdbcMssql;

use Doctrine\ORM\Decorator\EntityManagerDecorator;

class OdbcEntityManager extends EntityManagerDecorator
{
}

Doch woher kommt nun die richtige Datenbankverbindung? Hierfür müssen wir noch die config/services.yml anpassen und ergänzen:

services:
    App\Lib\OdbcMssql\OdbcEntityManager:
      arguments:
          $wrapped: '@doctrine.orm.mssql_entity_manager'

Damit injezieren wir die richtige Datenbankverbindung in diese Klasse und Doctrine kommt damit klar.

Warnungen

Prinzipiell können wir nun mit den MS-SQL Klassen so arbeiten, als müssten wir die Datenbankverbindung nicht wechseln. Was in dieser Konstellation jedoch nicht geht sind Joins über Datenbankgrenzen hinweg!. Je nach Beziehungen zwischen verschiedensten Tabellen kann ein 1:n Verknüpfung damit zur Datenbank-Katastrophe werden….vor allem, wenn man 10.000 Datensätze hat.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert