fix: added upload formular, reorganized some files and put them into a new folder misc
This commit is contained in:
@@ -0,0 +1,965 @@
|
||||
# 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?
|
||||
|
||||
Reference in New Issue
Block a user