966 lines
24 KiB
Markdown
966 lines
24 KiB
Markdown
# 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
|
|
<?php
|
|
|
|
return [
|
|
'default' => 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
|
|
<?php
|
|
|
|
namespace App\Models\Backend;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
class Company extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $connection = 'backend';
|
|
protected $table = 'companies';
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'legal_name',
|
|
'ticker',
|
|
'sector',
|
|
'country',
|
|
'headquarters',
|
|
'kyc_risk_level',
|
|
'summary',
|
|
];
|
|
|
|
public function transactions(): HasMany
|
|
{
|
|
return $this->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
|
|
<?php
|
|
|
|
namespace App\Models\Backend;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
class Transaction extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $connection = 'backend';
|
|
protected $table = 'transactions';
|
|
|
|
public const UPDATED_AT = 'last_modified_at';
|
|
|
|
protected $fillable = [
|
|
'corporate_entity',
|
|
'corporate_counterparty',
|
|
'tx_date',
|
|
'tx_amount',
|
|
'tx_currency',
|
|
'tx_purpose',
|
|
'tx_country_outgoing',
|
|
'tx_country_incoming',
|
|
'source_file',
|
|
'raw_payload',
|
|
'status',
|
|
];
|
|
|
|
protected $casts = [
|
|
'tx_amount' => '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
|
|
<?php
|
|
// tests/Feature/DualConnectionTest.php
|
|
|
|
use App\Models\Backend\Company as BackendCompany;
|
|
use App\Models\Company as PublicCompany;
|
|
|
|
test('can access both schemas', function () {
|
|
// Public Schema
|
|
$publicCount = PublicCompany::count();
|
|
expect($publicCount)->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
|
|
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Models\Backend\Company as BackendCompany;
|
|
use App\Models\Company as PublicCompany;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class CompanyRepository
|
|
{
|
|
public function __construct(
|
|
private bool $useBackendSchema = false
|
|
) {
|
|
// Feature Flag aus Config
|
|
$this->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
|
|
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Models\Backend\Transaction as BackendTransaction;
|
|
use App\Models\Transaction as PublicTransaction;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class TransactionRepository
|
|
{
|
|
public function __construct(
|
|
private bool $useBackendSchema = false
|
|
) {
|
|
$this->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
|
|
<?php
|
|
|
|
return [
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Backend Schema Migration Flags
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
// Hauptschalter: Nutze Backend-Schema statt Public
|
|
'use_backend_schema' => 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
|
|
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Repositories\CompanyRepository;
|
|
use App\Repositories\TransactionRepository;
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
class RepositoryServiceProvider extends ServiceProvider
|
|
{
|
|
public function register(): void
|
|
{
|
|
$this->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
|
|
<?php
|
|
|
|
return [
|
|
App\Providers\AppServiceProvider::class,
|
|
App\Providers\RepositoryServiceProvider::class, // Neu hinzufügen
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: Controller Migration (3-5 Tage)
|
|
|
|
### 3.1 Controller auf Repository umstellen
|
|
|
|
**Vorher** (app/Http/Controllers/CompanyController.php):
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Company;
|
|
use Illuminate\Http\Request;
|
|
|
|
class CompanyController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
$companies = Company::with('transactions')->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
|
|
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Repositories\CompanyRepository;
|
|
use Illuminate\Http\Request;
|
|
|
|
class CompanyController extends Controller
|
|
{
|
|
public function __construct(
|
|
private CompanyRepository $companies
|
|
) {}
|
|
|
|
public function index()
|
|
{
|
|
$companies = $this->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
|
|
<?php
|
|
|
|
use App\Models\Company;
|
|
|
|
$companies = Company::query()
|
|
->with('transactions')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
?>
|
|
|
|
<div>
|
|
@foreach($companies as $company)
|
|
<flux:card>{{ $company->name }}</flux:card>
|
|
@endforeach
|
|
</div>
|
|
```
|
|
|
|
**Nachher**:
|
|
```php
|
|
<?php
|
|
|
|
use App\Repositories\CompanyRepository;
|
|
|
|
$companyRepo = app(CompanyRepository::class);
|
|
$companies = $companyRepo->all();
|
|
|
|
?>
|
|
|
|
<div>
|
|
@foreach($companies as $company)
|
|
<flux:card>{{ $company->name }}</flux:card>
|
|
@endforeach
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Monitoring & Verification (Parallel zu Phase 3)
|
|
|
|
### 4.1 Dual-Read Verification Middleware
|
|
|
|
**app/Http/Middleware/VerifyDualSchemaReads.php**:
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class VerifyDualSchemaReads
|
|
{
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
if (!config('features.verify_reads')) {
|
|
return $next($request);
|
|
}
|
|
|
|
// Capture queries von beiden Schemas
|
|
$publicQueries = [];
|
|
$backendQueries = [];
|
|
|
|
\DB::connection('pgsql')->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
|
|
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Backend\Company as BackendCompany;
|
|
use App\Models\Company as PublicCompany;
|
|
use Illuminate\Console\Command;
|
|
|
|
class VerifySchemaConsistency extends Command
|
|
{
|
|
protected $signature = 'schema:verify-consistency';
|
|
protected $description = 'Verify data consistency between public and backend schemas';
|
|
|
|
public function handle(): int
|
|
{
|
|
$this->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
|
|
<?php
|
|
// tests/Feature/CompanyControllerTest.php
|
|
|
|
use App\Models\Backend\Company as BackendCompany;
|
|
use App\Models\Company as PublicCompany;
|
|
|
|
test('can create company with public schema', function () {
|
|
config(['features.use_backend_schema' => 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
|
|
<?php
|
|
// tests/Performance/SchemaPerformanceTest.php
|
|
|
|
test('backend schema is not slower than public', function () {
|
|
// Public Schema
|
|
$start = microtime(true);
|
|
config(['features.use_backend_schema' => 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
|
|
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Models\Backend\Company;
|
|
|
|
class CompanyRepository
|
|
{
|
|
// Feature Flags entfernen
|
|
public function all()
|
|
{
|
|
return Company::all();
|
|
}
|
|
|
|
// Dual-Write Code entfernen
|
|
public function create(array $data)
|
|
{
|
|
return Company::create($data);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7.2 Public Schema deprecaten
|
|
|
|
```sql
|
|
-- Tabellen in _deprecated Schema verschieben
|
|
CREATE SCHEMA IF NOT EXISTS _deprecated;
|
|
|
|
ALTER TABLE public.companies SET SCHEMA _deprecated;
|
|
ALTER TABLE public.transactions SET SCHEMA _deprecated;
|
|
|
|
-- Optional: Views für Legacy-Support
|
|
CREATE VIEW public.companies AS
|
|
SELECT
|
|
id,
|
|
corporate_entity as name,
|
|
NULL as legal_name,
|
|
country,
|
|
'medium' as kyc_risk_level,
|
|
NULL as summary,
|
|
created_at::timestamp,
|
|
last_modified_at::timestamp as updated_at
|
|
FROM backend.companies;
|
|
```
|
|
|
|
---
|
|
|
|
## Vorteile dieser Strategie
|
|
|
|
### ✅ Zero Downtime
|
|
- Keine Breaking Changes
|
|
- Rollback jederzeit möglich
|
|
- Schrittweise Umstellung
|
|
|
|
### ✅ Sicherheit
|
|
- Dual-Write als Safety Net
|
|
- Continuous Verification
|
|
- Feature Flags für granulare Kontrolle
|
|
|
|
### ✅ Flexibilität
|
|
- A/B Testing möglich
|
|
- Schrittweise Migration pro Feature
|
|
- Team kann parallel arbeiten
|
|
|
|
### ✅ Lernkurve
|
|
- Team lernt neues Schema schrittweise
|
|
- Bugs können isoliert gefunden werden
|
|
- Keine Hektik
|
|
|
|
---
|
|
|
|
## Zeitplan (Realistisch)
|
|
|
|
| Woche | Phase | Aufwand | Risiko |
|
|
|-------|-------|---------|--------|
|
|
| 1 | Setup & Backend Models | 2-3 Tage | Niedrig |
|
|
| 2 | Repository Pattern | 2-3 Tage | Niedrig |
|
|
| 3-4 | Controller Migration | 5-7 Tage | Mittel |
|
|
| 5 | Read-Only Testing | 2-3 Tage | Niedrig |
|
|
| 6 | Dual-Write Phase | 1 Woche | Mittel |
|
|
| 7-8 | Gradual Switchover | 2 Wochen | Mittel |
|
|
| 9-10 | Monitoring & Stabilisierung | 2 Wochen | Niedrig |
|
|
| 11-12 | Cleanup | 1 Woche | Niedrig |
|
|
|
|
**Total**: 10-12 Wochen (inkl. Buffer)
|
|
|
|
---
|
|
|
|
## Nächste konkrete Schritte
|
|
|
|
### Schritt 1: Database Config (heute, 30 Min)
|
|
```bash
|
|
# config/database.php erweitern
|
|
# .env bleibt unverändert
|
|
php artisan config:clear
|
|
php artisan tinker
|
|
>>> 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?
|
|
|