22 KiB
22 KiB
Migrationsplan: Schema-Restrukturierung
Zielsetzung
Aktueller Zustand
public.companies (11 Spalten)
↓ 1:N
public.transactions (49 Spalten) - Angereicherte Produktionsdaten
backend.transactions (14 Spalten) - Rohdaten
↓ N:M
backend.transaction_outputs (5 Spalten) - AI-generierte Outputs
Ziel-Zustand
backend.transactions (14 Spalten) - Ersetzt public.companies
↓ 1:N
public.transactions (5 Spalten, ähnlich backend.transaction_outputs) - Vereinfacht
⚠️ Klärungsfragen (KRITISCH)
Bevor wir fortfahren, müssen folgende Fragen geklärt werden:
1. Companies → backend.transactions Mapping
Problem: Die Strukturen sind sehr unterschiedlich
| public.companies | backend.transactions | Kompatibilität |
|---|---|---|
| id (bigint) | id (integer) | ⚠️ Typ-Unterschied |
| name | corporate_entity | ✅ Ähnlich |
| legal_name | - | ❌ Fehlt |
| ticker | - | ❌ Fehlt |
| sector | - | ❌ Fehlt |
| country | tx_country_outgoing? | ⚠️ Unklar |
| headquarters | - | ❌ Fehlt |
| kyc_risk_level | - | ❌ Fehlt |
| summary | - | ❌ Fehlt |
| - | corporate_counterparty | ❌ Neu |
| - | tx_date, tx_amount, tx_currency | ❌ Neu |
| - | tx_purpose | ❌ Neu |
| - | source_file, raw_payload | ❌ Neu |
| - | status | ❌ Neu |
Frage:
- Soll
backend.transactionserweitert werden, um Company-Felder aufzunehmen? - Oder sollen Companies als einzelne Zeilen ohne Transaktionsdaten gespeichert werden?
- Wie wird
corporate_entityzucompanies.namegemappt?
2. public.transactions → transaction_outputs Mapping
Problem: Drastischer Datenverlust bei Vereinfachung
| public.transactions (49 Felder) | backend.transaction_outputs (5 Felder) |
|---|---|
| Alle Core-Felder (14) | ❌ Verloren |
| Alle Enrichment-Felder (34) | ❌ Verloren |
| - | transaction_id (Foreign Key) |
| - | prompt_id (Welcher?) |
| - | output_key (Welcher Typ?) |
| - | content (Wie strukturiert?) |
| - | run_id (Optional) |
Frage:
- Welche Daten aus den 49 Feldern sollen in
contentserialisiert werden? - Welchen
output_keyverwenden wir? (z.B. "transaction_data", "risk_assessment"?) - Welchen
prompt_idverwenden wir? (Muss inbackend.prompt_templatesexistieren) - Was passiert mit den 11 Enrichment-Datenquellen?
3. Relationship & Foreign Keys
Problem: Beziehungen ändern sich fundamental
Aktuell:
companies.id → transactions.company_id (1:N)
Ziel (unklar):
backend.transactions.id → public.transactions.transaction_id (1:N)?
Frage:
- Bleibt die 1:N Beziehung erhalten?
- Wie wird
transaction_idin der neuenpublic.transactionsgemappt?
Vorgeschlagene Alternative: Erweiterte Migration
Ich schlage eine modifizierte Zielstruktur vor, die Datenverlust minimiert:
Option A: Erweitere backend.transactions (Empfohlen)
backend.companies (neue Tabelle)
- id (integer)
- name (text)
- legal_name (text, nullable)
- country (text)
- kyc_risk_level (text)
- ... weitere Company-Felder
backend.transactions (erweitert, bleibt)
- id (integer)
- company_id (integer FK → backend.companies)
- corporate_counterparty (text)
- tx_date (text → sollte timestamp werden)
- tx_amount (double precision)
- tx_currency (text)
- ... bestehende Felder
public.transaction_enrichments (neue Tabelle)
- id (bigint)
- transaction_id (integer FK → backend.transactions)
- enrichment_type (varchar) -- 'registry', 'sanctions', etc.
- data (jsonb)
- last_refreshed_at (timestamp)
- created_at, updated_at
Vorteile:
- ✅ Kein Datenverlust
- ✅ Klare Trennung: Companies, Transactions, Enrichments
- ✅ Backend-Schema behält Rohdaten
- ✅ Public-Schema behält angereicherte Daten
- ✅ Laravel-Logik kann schrittweise migriert werden
Option B: Vollständiger Umzug zu backend Schema
backend.companies (neu)
- Alle Felder von public.companies
backend.transactions (bleibt)
- Bestehende Struktur
backend.transaction_enrichments (neu)
- Alle Enrichment-Daten aus public.transactions
public.* (deprecated, später löschen)
Vorteile:
- ✅ Alles im backend Schema
- ✅ Klare Schema-Trennung
- ❌ Laravel-App muss komplett umgeschrieben werden
- ❌ Größere Breaking Changes
Migrationsplan (nach Klärung)
Phase 1: Vorbereitung (1-2 Tage)
1.1 Backup erstellen
# Vollständiges Backup
pg_dump -h localhost -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
# Schema-spezifische Backups
pg_dump -h localhost -U username -d database_name -n public > backup_public_$(date +%Y%m%d).sql
pg_dump -h localhost -U username -d database_name -n backend > backup_backend_$(date +%Y%m%d).sql
1.2 Datenanalyse
-- Anzahl Companies
SELECT COUNT(*) FROM public.companies;
-- Anzahl Transactions
SELECT COUNT(*) FROM public.transactions;
-- Datenintegrität prüfen
SELECT
COUNT(*) as total_transactions,
COUNT(DISTINCT company_id) as unique_companies,
COUNT(*) FILTER (WHERE company_id NOT IN (SELECT id FROM public.companies)) as orphaned_transactions
FROM public.transactions;
-- Enrichment-Daten Analyse
SELECT
COUNT(*) FILTER (WHERE registry_data IS NOT NULL) as has_registry,
COUNT(*) FILTER (WHERE sanctions_data IS NOT NULL) as has_sanctions,
COUNT(*) FILTER (WHERE pep_data IS NOT NULL) as has_pep
FROM public.transactions;
1.3 Test-Umgebung aufsetzen
# Kopie der Datenbank für Tests
createdb -T production_db test_migration_db
Phase 2: Schema-Erweiterung (2-3 Tage)
2.1 backend.companies erstellen
Laravel Migration:
<?php
// database/migrations/2025_11_12_create_backend_companies_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::connection('backend')->create('companies', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('legal_name')->nullable();
$table->string('ticker')->nullable();
$table->string('sector')->nullable();
$table->string('country', 2)->default('DE');
$table->string('headquarters')->nullable();
$table->string('kyc_risk_level')->default('medium');
$table->text('summary')->nullable();
$table->timestamps();
$table->index('name');
});
}
public function down(): void
{
Schema::connection('backend')->dropIfExists('companies');
}
};
2.2 backend.transactions erweitern
SQL Migration (wenn Laravel Multi-Schema-Support limitiert ist):
-- Füge company_id zu backend.transactions hinzu
ALTER TABLE backend.transactions
ADD COLUMN company_id INTEGER REFERENCES backend.companies(id) ON DELETE CASCADE;
-- Index für Performance
CREATE INDEX idx_backend_transactions_company_id ON backend.transactions(company_id);
2.3 public.transaction_enrichments erstellen
Laravel Migration:
<?php
// database/migrations/2025_11_12_create_transaction_enrichments_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('transaction_enrichments', function (Blueprint $table) {
$table->id();
$table->integer('backend_transaction_id'); // FK zu backend.transactions
$table->string('enrichment_type', 50); // 'registry', 'sanctions', etc.
$table->jsonb('data');
$table->timestamp('last_refreshed_at')->nullable();
$table->timestamps();
$table->index(['backend_transaction_id', 'enrichment_type']);
$table->unique(['backend_transaction_id', 'enrichment_type']);
});
}
public function down(): void
{
Schema::dropIfExists('transaction_enrichments');
}
};
Phase 3: Datenmigration (3-5 Tage)
3.1 Companies migrieren
<?php
// database/migrations/2025_11_12_migrate_companies_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Kopiere Companies von public → backend
DB::statement("
INSERT INTO backend.companies (
id, name, legal_name, ticker, sector,
country, headquarters, kyc_risk_level, summary,
created_at, updated_at
)
SELECT
id, name, legal_name, ticker, sector,
country, headquarters, kyc_risk_level, summary,
created_at, updated_at
FROM public.companies
ON CONFLICT (id) DO NOTHING
");
// Aktualisiere Sequence
DB::statement("
SELECT setval('backend.companies_id_seq',
(SELECT MAX(id) FROM backend.companies)
)
");
}
public function down(): void
{
DB::statement("TRUNCATE backend.companies CASCADE");
}
};
3.2 Transaction Core-Daten migrieren
<?php
// database/migrations/2025_11_12_migrate_transaction_core_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Füge company_id zu bestehenden backend.transactions hinzu
// (falls diese Daten schon existieren und verknüpft werden müssen)
// ODER: Migriere von public.transactions → backend.transactions
DB::statement("
INSERT INTO backend.transactions (
corporate_entity,
corporate_counterparty,
tx_date,
tx_amount,
tx_currency,
tx_purpose,
tx_country_incoming,
company_id,
status,
created_at,
last_modified_at
)
SELECT
c.name as corporate_entity,
t.counterparty as corporate_counterparty,
t.executed_at::text as tx_date,
t.amount as tx_amount,
t.currency as tx_currency,
t.flagged_reason as tx_purpose,
t.counterparty_country as tx_country_incoming,
t.company_id,
t.status,
t.created_at::text,
t.updated_at::text
FROM public.transactions t
JOIN public.companies c ON t.company_id = c.id
WHERE NOT EXISTS (
SELECT 1 FROM backend.transactions bt
WHERE bt.corporate_entity = c.name
AND bt.tx_date = t.executed_at::text
)
");
}
public function down(): void
{
// Rollback nur für migrierte Daten
DB::statement("
DELETE FROM backend.transactions
WHERE company_id IS NOT NULL
");
}
};
3.3 Enrichment-Daten migrieren
<?php
// database/migrations/2025_11_12_migrate_enrichment_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private array $enrichmentSources = [
'registry' => ['registry_data', 'registry_last_refreshed_at'],
'genesis' => ['genesis_context', 'genesis_last_refreshed_at'],
'govdata' => ['govdata_data', 'govdata_last_refreshed_at'],
'bundesanzeiger' => ['bundesanzeiger_data', 'bundesanzeiger_last_refreshed_at'],
'insolvency' => ['insolvency_data', 'insolvency_last_refreshed_at'],
'rss' => ['rss_alerts', 'rss_last_refreshed_at'],
'sanctions' => ['sanctions_data', 'sanctions_last_refreshed_at'],
'pep' => ['pep_data', 'pep_last_refreshed_at'],
'gleif' => ['gleif_data', 'gleif_last_refreshed_at'],
'eu_sanctions' => ['eu_sanctions_data', 'eu_sanctions_last_refreshed_at'],
'handelsregister' => ['handelsregister_data', 'handelsregister_last_refreshed_at'],
];
public function up(): void
{
foreach ($this->enrichmentSources as $type => [$dataField, $refreshField]) {
DB::statement("
INSERT INTO public.transaction_enrichments (
backend_transaction_id,
enrichment_type,
data,
last_refreshed_at,
created_at,
updated_at
)
SELECT
bt.id as backend_transaction_id,
'{$type}' as enrichment_type,
t.{$dataField} as data,
t.{$refreshField} as last_refreshed_at,
NOW(),
NOW()
FROM public.transactions t
JOIN backend.transactions bt ON (
bt.tx_date = t.executed_at::text
AND bt.tx_amount = t.amount
)
WHERE t.{$dataField} IS NOT NULL
ON CONFLICT (backend_transaction_id, enrichment_type) DO NOTHING
");
}
}
public function down(): void
{
DB::statement("TRUNCATE public.transaction_enrichments");
}
};
Phase 4: Model-Anpassung (2-3 Tage)
4.1 Neue Models erstellen
Backend\Company Model:
<?php
// app/Models/Backend/Company.php
namespace App\Models\Backend;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Company extends Model
{
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);
}
}
Backend\Transaction Model:
<?php
// app/Models/Backend/Transaction.php
namespace App\Models\Backend;
use App\Models\TransactionEnrichment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Transaction extends Model
{
protected $connection = 'backend';
protected $table = 'transactions';
protected $fillable = [
'company_id', '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',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function enrichments(): HasMany
{
return $this->hasMany(TransactionEnrichment::class, 'backend_transaction_id');
}
public function getEnrichment(string $type): ?array
{
return $this->enrichments()
->where('enrichment_type', $type)
->first()
?->data;
}
}
TransactionEnrichment Model:
<?php
// app/Models/TransactionEnrichment.php
namespace App\Models;
use App\Models\Backend\Transaction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TransactionEnrichment extends Model
{
protected $fillable = [
'backend_transaction_id',
'enrichment_type',
'data',
'last_refreshed_at'
];
protected $casts = [
'data' => 'array',
'last_refreshed_at' => 'datetime'
];
public function transaction(): BelongsTo
{
return $this->belongsTo(Transaction::class, 'backend_transaction_id');
}
}
Phase 5: Code-Refactoring (5-7 Tage)
5.1 Controller anpassen
Vorher:
use App\Models\Transaction;
$transactions = Transaction::with('company')
->where('risk_score', '>', 50)
->get();
Nachher:
use App\Models\Backend\Transaction;
$transactions = Transaction::with(['company', 'enrichments'])
->where('risk_score', '>', 50)
->get();
// Enrichment-Daten abrufen
foreach ($transactions as $transaction) {
$sanctionsData = $transaction->getEnrichment('sanctions');
$pepData = $transaction->getEnrichment('pep');
}
5.2 Compatibility Layer (Optional)
<?php
// app/Models/Transaction.php (Legacy-Support)
namespace App\Models;
use App\Models\Backend\Transaction as BackendTransaction;
/**
* @deprecated Use App\Models\Backend\Transaction instead
*/
class Transaction extends BackendTransaction
{
// Proxy zu neuem Model für Backward-Compatibility
public function __get($key)
{
// Prüfe ob Enrichment-Feld
if (str_ends_with($key, '_data')) {
$type = str_replace('_data', '', $key);
return $this->getEnrichment($type);
}
return parent::__get($key);
}
}
Phase 6: Testing (3-5 Tage)
6.1 Unit Tests
<?php
// tests/Unit/Models/Backend/TransactionTest.php
use App\Models\Backend\Transaction;
use App\Models\TransactionEnrichment;
test('transaction has enrichments relationship', function () {
$transaction = Transaction::factory()->create();
TransactionEnrichment::factory()->create([
'backend_transaction_id' => $transaction->id,
'enrichment_type' => 'sanctions',
'data' => ['status' => 'clear']
]);
expect($transaction->enrichments)->toHaveCount(1);
expect($transaction->getEnrichment('sanctions'))->toBe(['status' => 'clear']);
});
6.2 Integration Tests
<?php
// tests/Feature/TransactionMigrationTest.php
test('data integrity after migration', function () {
// Vergleiche Anzahl
$oldCount = DB::table('public.transactions')->count();
$newCount = DB::table('backend.transactions')->count();
expect($newCount)->toBeGreaterThanOrEqual($oldCount);
// Vergleiche Enrichment-Daten
$oldEnrichments = DB::table('public.transactions')
->whereNotNull('sanctions_data')
->count();
$newEnrichments = DB::table('public.transaction_enrichments')
->where('enrichment_type', 'sanctions')
->count();
expect($newEnrichments)->toBe($oldEnrichments);
});
Phase 7: Deployment (1-2 Tage)
7.1 Deployment-Schritte
# 1. Backup
php artisan backup:database
# 2. Migrations ausführen (in Reihenfolge!)
php artisan migrate --path=database/migrations/2025_11_12_create_backend_companies_table.php
php artisan migrate --path=database/migrations/2025_11_12_create_transaction_enrichments_table.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_companies_data.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_transaction_core_data.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_enrichment_data.php
# 3. Verification
php artisan tinker
>>> DB::table('backend.companies')->count()
>>> DB::table('backend.transactions')->count()
>>> DB::table('public.transaction_enrichments')->count()
# 4. Clear caches
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# 5. Run tests
php artisan test --filter=TransactionMigration
7.2 Rollback-Plan
# Falls etwas schief geht
php artisan migrate:rollback --step=5
# Restore from backup
psql -U username -d database_name < backup_20251112_120000.sql
Phase 8: Cleanup (nach 2-4 Wochen Monitoring)
-- Wenn alles stabil läuft, alte Tabellen entfernen
DROP TABLE public.transactions CASCADE;
DROP TABLE public.companies CASCADE;
-- Views für Backward-Compatibility (optional)
CREATE VIEW public.companies AS
SELECT * FROM backend.companies;
CREATE VIEW public.transactions AS
SELECT
bt.id,
bt.company_id,
bt.corporate_counterparty as counterparty,
bt.tx_amount as amount,
bt.tx_currency as currency,
bt.tx_date::timestamp as executed_at,
bt.status,
bt.created_at::timestamp,
bt.last_modified_at::timestamp as updated_at
FROM backend.transactions bt;
Zeitplan
| Phase | Dauer | Abhängigkeiten |
|---|---|---|
| 1. Vorbereitung | 1-2 Tage | - |
| 2. Schema-Erweiterung | 2-3 Tage | Phase 1 |
| 3. Datenmigration | 3-5 Tage | Phase 2 |
| 4. Model-Anpassung | 2-3 Tage | Phase 3 |
| 5. Code-Refactoring | 5-7 Tage | Phase 4 |
| 6. Testing | 3-5 Tage | Phase 5 |
| 7. Deployment | 1-2 Tage | Phase 6 |
| 8. Cleanup | Nach 2-4 Wochen | Phase 7 |
Gesamtdauer: 17-27 Arbeitstage (3-5 Wochen)
Risiken & Mitigation
Risiko 1: Datenverlust
Mitigation:
- Vollständige Backups vor jedem Schritt
- Test-Migration in Staging-Umgebung
- Datenvalidierung nach jeder Phase
Risiko 2: Downtime
Mitigation:
- Migrations während Wartungsfenster
- Blue-Green Deployment
- Read-Replica für Zero-Downtime
Risiko 3: Performance-Probleme
Mitigation:
- Indizes auf Foreign Keys
- Batch-Processing für große Datasets
- Query-Optimierung mit EXPLAIN ANALYZE
Risiko 4: Code-Inkompatibilität
Mitigation:
- Compatibility Layer
- Schrittweises Refactoring
- Feature-Flags für graduelle Umstellung
Nächste Schritte
- ✅ Klärung der Fragen oben
- ⬜ Detaillierte Datenanalyse durchführen
- ⬜ Test-Umgebung aufsetzen
- ⬜ Erste Migration in Staging testen
- ⬜ Review & Approval vom Team
- ⬜ Production-Migration planen
Erstellt am: 2025-11-12 Status: ENTWURF - Wartet auf Klärung der kritischen Fragen