Einmal 42 und zurück > Programmierung > Mehrere Datenbanken unter Symfony
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.