# Inkrementelle Migrations-Strategie: Parallele Schema-Integration ## Konzept: Strangler Fig Pattern Diese Strategie nutzt das **Strangler Fig Pattern** - das Backend-Schema wird parallel integriert und schrittweise übernimmt die Logik, während das alte System weiterläuft. Kein Big Bang, keine Breaking Changes. ``` Phase 1: Beide Systeme parallel ┌─────────────────┐ ┌─────────────────┐ │ Public Schema │ │ Backend Schema │ │ (Aktiv) │ │ (Read-Only) │ └─────────────────┘ └─────────────────┘ Phase 2: Dual-Write Pattern ┌─────────────────┐ ┌─────────────────┐ │ Public Schema │────▶│ Backend Schema │ │ (Primary) │ │ (Secondary) │ └─────────────────┘ └─────────────────┘ Phase 3: Umstellung ┌─────────────────┐ ┌─────────────────┐ │ Public Schema │◀────│ Backend Schema │ │ (Read-Only) │ │ (Primary) │ └─────────────────┘ └─────────────────┘ Phase 4: Deprecation ┌─────────────────┐ │ Backend Schema │ │ (Einzige Quelle)│ └─────────────────┘ ``` --- ## Phase 1: Dual-Connection Setup (1-2 Tage) ### 1.1 Database-Konfiguration erweitern **config/database.php**: ```php env('DB_CONNECTION', 'pgsql'), 'connections' => [ // Bestehende Public-Schema Verbindung 'pgsql' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', 'sslmode' => 'prefer', ], // Neue Backend-Schema Verbindung 'backend' => [ 'driver' => 'pgsql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), 'database' => env('DB_DATABASE', 'forge'), // Gleiche DB 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'backend', // Unterschiedliches Schema! 'sslmode' => 'prefer', ], ], ]; ``` **.env**: ```env # Keine Änderung nötig - beide Connections nutzen gleiche Credentials DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=your_database DB_USERNAME=your_user DB_PASSWORD=your_password ``` ### 1.2 Backend Models erstellen **app/Models/Backend/Company.php**: ```php hasMany(Transaction::class); } /** * Sync zu public.companies (während Migration) */ public function syncToPublic(): \App\Models\Company { return \App\Models\Company::updateOrCreate( ['id' => $this->id], $this->only($this->fillable) ); } } ``` **app/Models/Backend/Transaction.php**: ```php 'decimal:2', // tx_date ist als TEXT gespeichert - später migrieren zu timestamp ]; public function company(): BelongsTo { return $this->belongsTo(Company::class); } /** * Konvertiere zu public.transactions Format */ public function toPublicFormat(): array { return [ 'company_id' => $this->company_id, 'reference' => $this->id, // oder generiere unique reference 'amount' => $this->tx_amount, 'currency' => $this->tx_currency, 'counterparty' => $this->corporate_counterparty, 'counterparty_country' => $this->tx_country_incoming, 'executed_at' => $this->tx_date, 'status' => $this->status, 'requires_review' => true, ]; } } ``` ### 1.3 Test der Dual-Connection ```php toBeGreaterThan(0); // Backend Schema $backendCount = BackendCompany::count(); expect($backendCount)->toBeGreaterThanOrEqual(0); }); test('connections are isolated', function () { $public = PublicCompany::first(); $backend = BackendCompany::first(); // Verschiedene Connections expect($public->getConnectionName())->toBe('pgsql'); expect($backend->getConnectionName())->toBe('backend'); }); ``` --- ## Phase 2: Abstraction Layer (2-3 Tage) ### 2.1 Repository Pattern mit Feature Flags **app/Repositories/CompanyRepository.php**: ```php useBackendSchema = config('features.use_backend_schema', false); } public function all(): Collection { return $this->useBackendSchema ? BackendCompany::all() : PublicCompany::all(); } public function find(int $id): PublicCompany|BackendCompany|null { return $this->useBackendSchema ? BackendCompany::find($id) : PublicCompany::find($id); } public function create(array $data): PublicCompany|BackendCompany { if ($this->useBackendSchema) { $company = BackendCompany::create($data); // Dual-Write: Sync zu Public während Übergangsphase if (config('features.dual_write', true)) { $company->syncToPublic(); } return $company; } return PublicCompany::create($data); } public function update(int $id, array $data): bool { $company = $this->find($id); if (!$company) { return false; } $result = $company->update($data); // Dual-Write if ($this->useBackendSchema && config('features.dual_write', true)) { $company->syncToPublic(); } return $result; } public function delete(int $id): bool { $company = $this->find($id); if (!$company) { return false; } // Dual-Delete if ($this->useBackendSchema && config('features.dual_write', true)) { PublicCompany::destroy($id); } return $company->delete(); } /** * Helper: Get model class */ public function getModelClass(): string { return $this->useBackendSchema ? BackendCompany::class : PublicCompany::class; } } ``` **app/Repositories/TransactionRepository.php**: ```php useBackendSchema = config('features.use_backend_schema', false); } public function forCompany(int $companyId): Collection { return $this->useBackendSchema ? BackendTransaction::where('company_id', $companyId)->get() : PublicTransaction::where('company_id', $companyId)->get(); } public function requiresReview(): Collection { if ($this->useBackendSchema) { // Backend hat kein requires_review Feld - nutze status return BackendTransaction::where('status', 'pending')->get(); } return PublicTransaction::where('requires_review', true)->get(); } public function highRisk(int $threshold = 70): Collection { if ($this->useBackendSchema) { // Backend hat keinen risk_score - Alternative Logik return BackendTransaction::where('status', 'flagged')->get(); } return PublicTransaction::where('risk_score', '>=', $threshold)->get(); } } ``` ### 2.2 Feature Flag Konfiguration **config/features.php**: ```php env('FEATURE_USE_BACKEND_SCHEMA', false), // Dual-Write: Schreibe in beide Schemas während Migration 'dual_write' => env('FEATURE_DUAL_WRITE', true), // Read-Verification: Vergleiche Reads aus beiden Schemas (Logging) 'verify_reads' => env('FEATURE_VERIFY_READS', false), // Schrittweise Migration pro Bereich 'backend_schema_areas' => [ 'companies' => env('FEATURE_BACKEND_COMPANIES', false), 'transactions' => env('FEATURE_BACKEND_TRANSACTIONS', false), 'reports' => env('FEATURE_BACKEND_REPORTS', false), ], ]; ``` **.env** (für schrittweise Aktivierung): ```env # Phase 1: Beide Schemas verfügbar, aber Public aktiv FEATURE_USE_BACKEND_SCHEMA=false FEATURE_DUAL_WRITE=false # Phase 2: Dual-Write aktivieren # FEATURE_USE_BACKEND_SCHEMA=false # FEATURE_DUAL_WRITE=true # Phase 3: Backend als Primary, Public als Fallback # FEATURE_USE_BACKEND_SCHEMA=true # FEATURE_DUAL_WRITE=true # Phase 4: Nur Backend # FEATURE_USE_BACKEND_SCHEMA=true # FEATURE_DUAL_WRITE=false ``` ### 2.3 Service Provider für Dependency Injection **app/Providers/RepositoryServiceProvider.php**: ```php app->singleton(CompanyRepository::class, function ($app) { return new CompanyRepository( useBackendSchema: config('features.use_backend_schema', false) ); }); $this->app->singleton(TransactionRepository::class, function ($app) { return new TransactionRepository( useBackendSchema: config('features.use_backend_schema', false) ); }); } } ``` **bootstrap/providers.php**: ```php get(); return view('companies.index', compact('companies')); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string', 'country' => 'required|string|size:2', // ... ]); $company = Company::create($validated); return redirect()->route('companies.show', $company); } } ``` **Nachher**: ```php companies->all(); return view('companies.index', compact('companies')); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string', 'country' => 'required|string|size:2', // ... ]); $company = $this->companies->create($validated); return redirect()->route('companies.show', $company); } } ``` ### 3.2 Volt/Livewire Components anpassen **Vorher** (resources/views/pages/companies/index.blade.php): ```php with('transactions') ->orderBy('name') ->get(); ?>
@foreach($companies as $company) {{ $company->name }} @endforeach
``` **Nachher**: ```php all(); ?>
@foreach($companies as $company) {{ $company->name }} @endforeach
``` --- ## Phase 4: Monitoring & Verification (Parallel zu Phase 3) ### 4.1 Dual-Read Verification Middleware **app/Http/Middleware/VerifyDualSchemaReads.php**: ```php listen(function ($query) use (&$publicQueries) { $publicQueries[] = $query->sql; }); \DB::connection('backend')->listen(function ($query) use (&$backendQueries) { $backendQueries[] = $query->sql; }); $response = $next($request); // Log für Analyse if (!empty($publicQueries) || !empty($backendQueries)) { Log::channel('migration')->info('Dual Schema Access', [ 'route' => $request->path(), 'public_queries' => count($publicQueries), 'backend_queries' => count($backendQueries), ]); } return $response; } } ``` ### 4.2 Health Check Command **app/Console/Commands/VerifySchemaConsistency.php**: ```php info('Checking schema consistency...'); // Company Count $publicCount = PublicCompany::count(); $backendCount = BackendCompany::count(); $this->table( ['Schema', 'Companies', 'Status'], [ ['Public', $publicCount, '✓'], ['Backend', $backendCount, $backendCount === $publicCount ? '✓' : '⚠'], ] ); if ($backendCount !== $publicCount) { $this->warn("Company count mismatch: Public={$publicCount}, Backend={$backendCount}"); } // Sample Data Verification $sampleSize = min(10, $publicCount); $publicSample = PublicCompany::take($sampleSize)->get(); $mismatches = 0; foreach ($publicSample as $publicCompany) { $backendCompany = BackendCompany::find($publicCompany->id); if (!$backendCompany) { $this->warn("Company {$publicCompany->id} missing in backend"); $mismatches++; continue; } if ($publicCompany->name !== $backendCompany->name) { $this->warn("Company {$publicCompany->id} name mismatch"); $mismatches++; } } if ($mismatches === 0) { $this->info('✓ All consistency checks passed!'); return self::SUCCESS; } $this->error("✗ Found {$mismatches} inconsistencies"); return self::FAILURE; } } ``` --- ## Phase 5: Graduelle Umstellung (2-4 Wochen) ### Woche 1: Read-Only Access ```env FEATURE_USE_BACKEND_SCHEMA=false FEATURE_DUAL_WRITE=false FEATURE_VERIFY_READS=true ``` - ✅ Backend-Schema ist verfügbar - ✅ Monitoring läuft - ✅ Keine Produktions-Daten betroffen ### Woche 2: Dual-Write aktivieren ```env FEATURE_USE_BACKEND_SCHEMA=false # Lesen: Public FEATURE_DUAL_WRITE=true # Schreiben: Beide FEATURE_VERIFY_READS=true ``` - ✅ Neue Daten gehen in beide Schemas - ✅ Public bleibt Primary - ⚠️ Monitor auf Sync-Errors ### Woche 3: Backend als Primary (Canary) ```env # Nur für 10% Traffic oder spezifische Routes FEATURE_BACKEND_COMPANIES=true # Companies von Backend lesen FEATURE_BACKEND_TRANSACTIONS=false # Transactions noch von Public FEATURE_DUAL_WRITE=true ``` - ✅ Schrittweise Umstellung pro Feature - ✅ A/B Testing möglich - ✅ Rollback jederzeit möglich ### Woche 4: Full Switchover ```env FEATURE_USE_BACKEND_SCHEMA=true # Lesen: Backend FEATURE_DUAL_WRITE=true # Schreiben: Beide (Sicherheit) ``` - ✅ Backend ist Primary - ✅ Public als Safety Net ### Nach 2 Wochen stabiler Betrieb: ```env FEATURE_USE_BACKEND_SCHEMA=true FEATURE_DUAL_WRITE=false # Public wird deprecated ``` --- ## Phase 6: Testing-Strategie ### 6.1 Feature Tests mit Feature Flags ```php false]); $response = $this->post('/companies', [ 'name' => 'Test Corp', 'country' => 'DE', ]); $response->assertRedirect(); expect(PublicCompany::where('name', 'Test Corp')->exists())->toBeTrue(); }); test('can create company with backend schema', function () { config(['features.use_backend_schema' => true]); $response = $this->post('/companies', [ 'name' => 'Test Corp Backend', 'country' => 'DE', ]); $response->assertRedirect(); expect(BackendCompany::where('corporate_entity', 'Test Corp Backend')->exists())->toBeTrue(); }); test('dual write creates in both schemas', function () { config([ 'features.use_backend_schema' => true, 'features.dual_write' => true, ]); $response = $this->post('/companies', [ 'name' => 'Dual Write Test', 'country' => 'DE', ]); $response->assertRedirect(); expect(BackendCompany::where('corporate_entity', 'Dual Write Test')->exists())->toBeTrue(); expect(PublicCompany::where('name', 'Dual Write Test')->exists())->toBeTrue(); }); ``` ### 6.2 Performance Tests ```php false]); app(CompanyRepository::class)->all(); $publicTime = microtime(true) - $start; // Backend Schema $start = microtime(true); config(['features.use_backend_schema' => true]); app(CompanyRepository::class)->all(); $backendTime = microtime(true) - $start; // Backend sollte nicht mehr als 20% langsamer sein expect($backendTime)->toBeLessThan($publicTime * 1.2); }); ``` --- ## Phase 7: Cleanup & Optimization (Nach 4-6 Wochen) ### 7.1 Repository vereinfachen Wenn Backend-Schema stabil läuft: ```php >> DB::connection('backend')->select('SELECT 1') ``` ### Schritt 2: Backend Models (heute, 1-2 Std) ```bash # Models erstellen mkdir -p app/Models/Backend # Company.php & Transaction.php erstellen ``` ### Schritt 3: Erster Test (heute, 30 Min) ```bash php artisan test --filter=DualConnectionTest ``` ### Schritt 4: Feature Flags (morgen, 1-2 Std) ```bash # config/features.php erstellen # Repository Pattern implementieren ``` Möchten Sie, dass ich mit **Schritt 1-2 beginne** und die konkrete Implementierung starte?