15 KiB
Analyse: Laravel Models vs Migrations vs Datenbank
Übersicht
Diese Analyse vergleicht die Laravel Eloquent Models mit den entsprechenden Migration-Dateien und der tatsächlichen Datenbankstruktur.
1. Company Model & Migration
✅ PERFEKT SYNCHRON
Migration
Datei: database/migrations/2025_10_20_181750_create_companies_table.php
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$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();
});
Felder:
id(auto-increment)name(string, unique)legal_name(string, nullable)ticker(string, nullable)sector(string, nullable)country(string(2), default: 'DE')headquarters(string, nullable)kyc_risk_level(string, default: 'medium')summary(text, nullable)timestamps(created_at, updated_at)
Model
Datei: app/Models/Company.php
protected $fillable = [
'name',
'legal_name',
'ticker',
'sector',
'country',
'headquarters',
'kyc_risk_level',
'summary',
];
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
Datenbank-Status
- ✅ Alle Felder vorhanden
- ✅ Datentypen stimmen überein
- ✅ Defaults korrekt gesetzt
- ✅ Unique Constraint auf
name - ✅ Relationship
hasMany(Transaction::class)definiert
2. Transaction Model & Migration
⚠️ TEILWEISE DISKREPANZEN
Migration
Datei: database/migrations/2025_10_20_181753_create_transactions_table.php
Gesamt: 49 Spalten (ohne timestamps)
Core Felder (15 Spalten)
$table->id();
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->string('reference')->unique();
$table->decimal('amount', 16, 2);
$table->string('currency', 3)->default('EUR');
$table->string('counterparty');
$table->string('counterparty_country', 2)->nullable();
$table->string('channel')->nullable();
$table->dateTime('executed_at');
$table->unsignedTinyInteger('risk_score')->default(0);
$table->string('status', 32)->index();
$table->boolean('requires_review')->default(true);
$table->string('flagged_by')->nullable();
$table->text('flagged_reason')->nullable();
$table->json('signals')->nullable();
Enrichment-Felder (11 Datenquellen, 34 Spalten)
1. Registry (5 Felder)
$table->text('registry_company_number')->nullable();
$table->text('registry_source')->nullable();
$table->double('registry_match_score')->nullable();
$table->jsonb('registry_data')->nullable();
$table->dateTime('registry_last_refreshed_at')->nullable();
2. Genesis (2 Felder)
$table->jsonb('genesis_context')->nullable();
$table->dateTime('genesis_last_refreshed_at')->nullable();
3. GovData (2 Felder)
$table->jsonb('govdata_data')->nullable();
$table->dateTime('govdata_last_refreshed_at')->nullable();
4. Bundesanzeiger (2 Felder)
$table->jsonb('bundesanzeiger_data')->nullable();
$table->dateTime('bundesanzeiger_last_refreshed_at')->nullable();
5. Insolvency (2 Felder)
$table->jsonb('insolvency_data')->nullable();
$table->dateTime('insolvency_last_refreshed_at')->nullable();
6. RSS Alerts (2 Felder)
$table->jsonb('rss_alerts')->nullable();
$table->dateTime('rss_last_refreshed_at')->nullable();
7. Sanctions (2 Felder)
$table->jsonb('sanctions_data')->nullable();
$table->dateTime('sanctions_last_refreshed_at')->nullable();
8. PEP (2 Felder)
$table->jsonb('pep_data')->nullable();
$table->dateTime('pep_last_refreshed_at')->nullable();
9. GLEIF (3 Felder)
$table->text('gleif_lei')->nullable();
$table->json('gleif_data')->nullable();
$table->dateTime('gleif_last_refreshed_at')->nullable();
10. EU Sanctions (2 Felder)
$table->jsonb('eu_sanctions_data')->nullable();
$table->dateTime('eu_sanctions_last_refreshed_at')->nullable();
11. Handelsregister (4 Felder)
$table->jsonb('handelsregister_data')->nullable();
$table->dateTime('handelsregister_last_refreshed_at')->nullable();
$table->text('handelsregister_status')->nullable();
$table->bigInteger('handelsregister_entity_id')->nullable();
Model
Datei: app/Models/Transaction.php
// Status-Konstanten
public const STATUS_TRUE_POSITIVE = 'true_positive';
public const STATUS_FALSE_POSITIVE = 'false_positive';
public const STATUS_CLEARED = 'cleared';
// Fillable (nur Core-Felder!)
protected $fillable = [
'company_id',
'reference',
'amount',
'currency',
'counterparty',
'counterparty_country',
'channel',
'executed_at',
'risk_score',
'status',
'requires_review',
'flagged_by',
'flagged_reason',
'signals',
];
// Casts
protected $casts = [
'executed_at' => 'datetime',
'requires_review' => 'boolean',
'signals' => 'array',
];
// Relationship
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
// Helper-Methode
public function statusLabel(): string
{
return match ($this->status) {
self::STATUS_TRUE_POSITIVE => __('Bestätigter Treffer'),
self::STATUS_FALSE_POSITIVE => __('Fehlalarm'),
default => __('Freigegeben'),
};
}
⚠️ FEHLENDE FELDER IM MODEL
Das Transaction Model hat NUR 14 Core-Felder im $fillable Array, aber die Migration definiert 49 Felder (exkl. timestamps).
Fehlende Enrichment-Felder (34 Spalten):
Registry-Felder:
registry_company_numberregistry_sourceregistry_match_scoreregistry_dataregistry_last_refreshed_at
Genesis-Felder:
genesis_contextgenesis_last_refreshed_at
GovData-Felder:
govdata_datagovdata_last_refreshed_at
Bundesanzeiger-Felder:
bundesanzeiger_databundesanzeiger_last_refreshed_at
Insolvency-Felder:
insolvency_datainsolvency_last_refreshed_at
RSS-Felder:
rss_alertsrss_last_refreshed_at
Sanctions-Felder:
sanctions_datasanctions_last_refreshed_at
PEP-Felder:
pep_datapep_last_refreshed_at
GLEIF-Felder:
gleif_leigleif_datagleif_last_refreshed_at
EU Sanctions-Felder:
eu_sanctions_dataeu_sanctions_last_refreshed_at
Handelsregister-Felder:
handelsregister_datahandelsregister_last_refreshed_athandelsregister_statushandelsregister_entity_id
⚠️ FEHLENDE CASTS
Das Model sollte Casts für alle zeitbasierten und JSON-Felder haben:
Fehlende DateTime-Casts:
registry_last_refreshed_atgenesis_last_refreshed_atgovdata_last_refreshed_atbundesanzeiger_last_refreshed_atinsolvency_last_refreshed_atrss_last_refreshed_atsanctions_last_refreshed_atpep_last_refreshed_atgleif_last_refreshed_ateu_sanctions_last_refreshed_athandelsregister_last_refreshed_at
Fehlende JSON/Array-Casts:
registry_datagenesis_contextgovdata_databundesanzeiger_datainsolvency_datarss_alertssanctions_datapep_datagleif_dataeu_sanctions_datahandelsregister_data
3. Vergleich: Datenbank vs Migration
public.companies
Status: ✅ 100% Übereinstimmung
| Feature | Migration | Datenbank | Status |
|---|---|---|---|
| Spalten | 11 | 11 | ✅ |
| Unique Constraint | name |
name |
✅ |
| Defaults | country='DE', kyc_risk_level='medium' |
Identisch | ✅ |
public.transactions
Status: ✅ Migration deckt alle DB-Felder ab
Constraints & Indizes
| Constraint | Migration | Datenbank | Status |
|---|---|---|---|
| Foreign Key | company_id → companies.id |
Vorhanden | ✅ |
| Cascade Delete | cascadeOnDelete() |
Implementiert | ✅ |
| Unique | reference |
Vorhanden | ✅ |
| Index | status |
Vorhanden | ✅ |
Datentypen-Vergleich
| Feld | Migration | Datenbank | Status |
|---|---|---|---|
| id | id() |
bigint | ✅ |
| company_id | foreignId() |
bigint | ✅ |
| amount | decimal(16,2) |
numeric | ✅ |
| currency | string(3) |
varchar | ✅ |
| counterparty_country | string(2) |
varchar | ✅ |
| risk_score | unsignedTinyInteger |
smallint | ⚠️* |
| status | string(32) |
varchar | ✅ |
| requires_review | boolean |
boolean | ✅ |
| signals | json |
json | ✅ |
| *_data | jsonb |
jsonb | ✅ |
| gleif_data | json |
json | ✅ |
| executed_at | dateTime |
timestamp | ✅ |
| *_last_refreshed_at | dateTime |
timestamp | ✅ |
*unsignedTinyInteger (0-255) vs smallint (-32768 bis 32767) sind funktional kompatibel
Zusammenfassung
✅ Stärken
-
Migration-Dateien sind vollständig
- Alle Datenbank-Felder korrekt definiert
- Foreign Key Constraints implementiert
- Indizes sinnvoll gesetzt
-
Core-Model-Felder stimmen überein
- Basis-Transaktionsfelder vollständig
- Relationships sauber definiert
-
Datenbank-Konsistenz
- Migrations wurden korrekt ausgeführt
- Constraints sind aktiv
⚠️ Probleme & Empfehlungen
Problem 1: Transaction Model ist unvollständig
Problem:
- Das Model definiert nur 14 von 49 Feldern im
$fillableArray - Alle 34 Enrichment-Felder fehlen
Auswirkungen:
- ❌ Enrichment-Felder können nicht via Mass Assignment gesetzt werden
- ❌ Keine automatischen Type Casts für externe Datenfelder
- ❌ Potenzielle Fehler beim Zugriff auf nicht-gecastete JSON-Daten
- ❌ DateTime-Felder werden als Strings zurückgegeben
Lösungsvorschläge:
Option 1: Alle Felder zu $fillable hinzufügen
protected $fillable = [
// Core fields
'company_id', 'reference', 'amount', 'currency',
'counterparty', 'counterparty_country', 'channel',
'executed_at', 'risk_score', 'status',
'requires_review', 'flagged_by', 'flagged_reason', 'signals',
// Registry
'registry_company_number', 'registry_source', 'registry_match_score',
'registry_data', 'registry_last_refreshed_at',
// Genesis
'genesis_context', 'genesis_last_refreshed_at',
// ... alle weiteren Felder
];
Option 2: $guarded verwenden (empfohlen für interne Anwendungen)
protected $guarded = ['id'];
Option 3: Separate Accessor/Mutator für Enrichment-Felder
public function registryData(): Attribute
{
return Attribute::make(
get: fn ($value) => json_decode($value, true),
set: fn ($value) => json_encode($value),
);
}
Problem 2: Fehlende Casts für Enrichment-Felder
Problem:
- Keine Casts für
*_last_refreshed_atFelder - Keine Casts für
*_dataJSON-Felder
Auswirkungen:
- DateTime-Felder werden als Strings zurückgegeben (kein Carbon-Objekt)
- JSON-Felder müssen manuell dekodiert werden
Lösung:
protected $casts = [
// Existing
'executed_at' => 'datetime',
'requires_review' => 'boolean',
'signals' => 'array',
// DateTime casts for all refresh timestamps
'registry_last_refreshed_at' => 'datetime',
'genesis_last_refreshed_at' => 'datetime',
'govdata_last_refreshed_at' => 'datetime',
'bundesanzeiger_last_refreshed_at' => 'datetime',
'insolvency_last_refreshed_at' => 'datetime',
'rss_last_refreshed_at' => 'datetime',
'sanctions_last_refreshed_at' => 'datetime',
'pep_last_refreshed_at' => 'datetime',
'gleif_last_refreshed_at' => 'datetime',
'eu_sanctions_last_refreshed_at' => 'datetime',
'handelsregister_last_refreshed_at' => 'datetime',
// JSON/Array casts for all data fields
'registry_data' => 'array',
'genesis_context' => 'array',
'govdata_data' => 'array',
'bundesanzeiger_data' => 'array',
'insolvency_data' => 'array',
'rss_alerts' => 'array',
'sanctions_data' => 'array',
'pep_data' => 'array',
'gleif_data' => 'array',
'eu_sanctions_data' => 'array',
'handelsregister_data' => 'array',
];
Problem 3: Datenbank-Schema-Anomalie
Problem:
- Die
public.transactionsTabelle zeigt 9x duplizierteidSpalten im describe_table Output
Mögliche Ursachen:
- Korruptes Schema-Metadaten
- Mehrfache Migration-Ausführungen ohne Rollback
- PostgreSQL-Katalog-Problem
Lösung:
- Schema inspizieren:
\d+ transactionsin psql - Bei Bedarf Migration neu ausführen
- Oder manuelles ALTER TABLE zur Bereinigung
Nächste Schritte
Empfohlene Reihenfolge:
-
✅ Transaction Model aktualisieren
- Alle fehlenden Felder zu
$fillablehinzufügen - Alle fehlenden Casts definieren
- Alle fehlenden Felder zu
-
✅ Tests schreiben
- Unit-Tests für Model-Casts
- Feature-Tests für Enrichment-Datenfluss
-
⚠️ Datenbank-Anomalie untersuchen
- PostgreSQL-Schema inspizieren
- Ggf. Migration neu ausführen
-
📝 Dokumentation erweitern
- Enrichment-Pipeline dokumentieren
- API für externe Datenquellen dokumentieren
Checkliste
Companies
- Migration vollständig
- Model synchron mit Migration
- Datenbank korrekt strukturiert
- Relationships definiert
- Casts korrekt
Transactions
- Migration vollständig
- Model synchron mit Migration ⚠️
- Datenbank korrekt strukturiert
- Relationships definiert
- Casts vollständig ⚠️
- Schema-Anomalie behoben ⚠️
Anhang: Vollständige Feldliste Transaction Model
Core Felder (14)
✅ Im Model vorhanden
- company_id
- reference
- amount
- currency
- counterparty
- counterparty_country
- channel
- executed_at
- risk_score
- status
- requires_review
- flagged_by
- flagged_reason
- signals
Enrichment Felder (34)
❌ Im Model fehlend
Registry (5) 15. registry_company_number 16. registry_source 17. registry_match_score 18. registry_data 19. registry_last_refreshed_at
Genesis (2) 20. genesis_context 21. genesis_last_refreshed_at
GovData (2) 22. govdata_data 23. govdata_last_refreshed_at
Bundesanzeiger (2) 24. bundesanzeiger_data 25. bundesanzeiger_last_refreshed_at
Insolvency (2) 26. insolvency_data 27. insolvency_last_refreshed_at
RSS (2) 28. rss_alerts 29. rss_last_refreshed_at
Sanctions (2) 30. sanctions_data 31. sanctions_last_refreshed_at
PEP (2) 32. pep_data 33. pep_last_refreshed_at
GLEIF (3) 34. gleif_lei 35. gleif_data 36. gleif_last_refreshed_at
EU Sanctions (2) 37. eu_sanctions_data 38. eu_sanctions_last_refreshed_at
Handelsregister (4) 39. handelsregister_data 40. handelsregister_last_refreshed_at 41. handelsregister_status 42. handelsregister_entity_id
Analysiert am: 2025-11-12