# 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(); ?>