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.