- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
243 lines
7.4 KiB
Markdown
243 lines
7.4 KiB
Markdown
# Hydrator Eager Loading System
|
||
|
||
Das Eager Loading System löst das **N+1 Query Problem** durch intelligente Batch-Queries und bietet massive Performance-Verbesserungen für Relations-intensive Anwendungen.
|
||
|
||
## Problem: N+1 Query Problem
|
||
|
||
### Vorher (Lazy Loading)
|
||
```php
|
||
// 1 Query für Users
|
||
$users = $repository->findAll(); // SELECT * FROM users
|
||
|
||
// N Queries für Relations (1 pro User)
|
||
foreach($users as $user) {
|
||
$user->posts; // SELECT * FROM posts WHERE user_id = ?
|
||
$user->profile; // SELECT * FROM profiles WHERE user_id = ?
|
||
}
|
||
// Total: 1 + (N × 2) = 201 Queries für 100 Users!
|
||
```
|
||
|
||
### Nachher (Eager Loading)
|
||
```php
|
||
// 3 Queries total durch Batch-Loading
|
||
$users = $hydrator->hydrateManyWithRelations($metadata, $userData, ['posts', 'profile']);
|
||
// 1. SELECT * FROM users
|
||
// 2. SELECT * FROM posts WHERE user_id IN (1,2,3,...,100)
|
||
// 3. SELECT * FROM profiles WHERE user_id IN (1,2,3,...,100)
|
||
// Total: 3 Queries für 100 Users = 98.5% Reduktion!
|
||
```
|
||
|
||
## Features
|
||
|
||
### ✅ Selective Eager Loading
|
||
```php
|
||
// Lade nur spezifizierte Relations
|
||
$user = $hydrator->hydrateWithRelations($metadata, $data, ['posts', 'comments']);
|
||
|
||
// Ohne Relations = normale Hydration
|
||
$user = $hydrator->hydrateWithRelations($metadata, $data, []);
|
||
```
|
||
|
||
### ✅ Batch Processing für Collections
|
||
```php
|
||
// N+1 Problem gelöst für Collections
|
||
$users = $hydrator->hydrateManyWithRelations($metadata, $dataRows, ['posts', 'profile']);
|
||
|
||
// Intelligente Gruppierung nach Relation-Types
|
||
// - belongsTo: IN-Queries für Foreign Keys
|
||
// - hasMany: Gruppierung nach Local Keys
|
||
// - one-to-one: Eindeutige Zuordnung
|
||
```
|
||
|
||
### ✅ Relation-Type Support
|
||
- **belongsTo**: Foreign Key Lookups mit IN-Queries
|
||
- **hasMany**: Reverse Foreign Key Lookups mit Gruppierung
|
||
- **one-to-one**: Eindeutige Relations-Zuordnung
|
||
|
||
## API Usage
|
||
|
||
### Single Entity mit Relations
|
||
```php
|
||
$user = $hydrator->hydrateWithRelations(
|
||
$userMetadata,
|
||
$userData,
|
||
['posts', 'profile', 'roles']
|
||
);
|
||
|
||
// Relations sind sofort verfügbar, keine zusätzlichen Queries
|
||
echo count($user->posts); // Kein Query
|
||
echo $user->profile->bio; // Kein Query
|
||
```
|
||
|
||
### Multiple Entities (Batch Loading)
|
||
```php
|
||
$users = $hydrator->hydrateManyWithRelations(
|
||
$userMetadata,
|
||
$userDataRows,
|
||
['posts', 'comments', 'profile']
|
||
);
|
||
|
||
// Alle Relations wurden mit nur 4 Queries geladen:
|
||
// 1x Users, 1x Posts, 1x Comments, 1x Profiles
|
||
```
|
||
|
||
### Performance-kritische Scenarios
|
||
```php
|
||
// Blog-System: Posts mit Comments, Tags, Categories
|
||
$posts = $hydrator->hydrateManyWithRelations(
|
||
$postMetadata,
|
||
$postDataRows,
|
||
['comments', 'tags', 'category', 'author']
|
||
);
|
||
|
||
// Ohne Eager Loading: 1 + (50 × 4) = 201 Queries
|
||
// Mit Eager Loading: 5 Queries = 97.5% Reduktion
|
||
```
|
||
|
||
## Performance Benchmarks
|
||
|
||
### Real-World Scenarios
|
||
|
||
| Scenario | Entities | Relations | Lazy Queries | Eager Queries | Reduction |
|
||
|----------|----------|-----------|--------------|---------------|-----------|
|
||
| User Dashboard | 50 users | posts, profile | 101 | 3 | **97.0%** |
|
||
| Blog Listing | 20 posts | comments, tags, author | 81 | 4 | **95.1%** |
|
||
| E-Commerce | 100 products | category, reviews, images | 301 | 4 | **98.7%** |
|
||
| Social Feed | 30 posts | author, comments, likes | 91 | 4 | **95.6%** |
|
||
|
||
### Database Load Reduction
|
||
```php
|
||
// Blog System Beispiel (50 Posts)
|
||
$lazyQueries = 1 + (50 × 3); // 151 Queries
|
||
$eagerQueries = 4; // 4 Queries
|
||
$loadReduction = 151 / 4; // 37.75x weniger DB-Load!
|
||
```
|
||
|
||
## Implementation Details
|
||
|
||
### Batch Loading Algorithm
|
||
1. **Entity Creation**: Erstelle alle Entities ohne Relations
|
||
2. **Key Collection**: Sammle alle IDs/Foreign Keys für Batch-Queries
|
||
3. **Batch Queries**: Ein Query pro Relation-Type mit IN-Clauses
|
||
4. **Grouping**: Gruppiere Related Entities nach Keys
|
||
5. **Assignment**: Weise Relations den entsprechenden Entities zu
|
||
|
||
### BelongsTo Relations (Foreign Key Lookup)
|
||
```php
|
||
// Sammle alle Foreign Keys
|
||
$foreignKeys = array_unique(array_column($dataRows, 'category_id'));
|
||
|
||
// Ein Batch-Query für alle Categories
|
||
$categories = $entityLoader->findBy(Category::class, ['id' => $foreignKeys]);
|
||
|
||
// Gruppiere nach ID für schnelle Zuordnung
|
||
$categoriesById = array_column($categories, null, 'id');
|
||
```
|
||
|
||
### HasMany Relations (Reverse Lookup)
|
||
```php
|
||
// Sammle alle Entity IDs
|
||
$userIds = array_column($userDataRows, 'id');
|
||
|
||
// Ein Batch-Query für alle Posts
|
||
$posts = $entityLoader->findBy(Post::class, ['user_id' => $userIds]);
|
||
|
||
// Gruppiere Posts nach user_id
|
||
$postsByUserId = [];
|
||
foreach($posts as $post) {
|
||
$postsByUserId[$post->user_id][] = $post;
|
||
}
|
||
```
|
||
|
||
### One-to-One Relations
|
||
```php
|
||
// Ähnlich wie hasMany, aber nur eine Relation pro Entity
|
||
$profiles = $entityLoader->findBy(Profile::class, ['user_id' => $userIds]);
|
||
$profileByUserId = array_column($profiles, null, 'user_id');
|
||
```
|
||
|
||
## Error Handling & Edge Cases
|
||
|
||
### Missing Relations
|
||
```php
|
||
// Entities ohne Relations erhalten Default-Werte
|
||
$userPosts = $postsByUserId[$userId] ?? []; // Empty array für hasMany
|
||
$userProfile = $profilesByUserId[$userId] ?? null; // null für belongsTo/one-to-one
|
||
```
|
||
|
||
### Empty Data Sets
|
||
```php
|
||
$result = $hydrator->hydrateManyWithRelations($metadata, [], ['posts']);
|
||
// Returns: [] (empty array, keine Fehler)
|
||
```
|
||
|
||
### Invalid Relations
|
||
```php
|
||
// Nicht-existierende oder Nicht-Relations werden übersprungen
|
||
$user = $hydrator->hydrateWithRelations($metadata, $data, ['invalid_relation']);
|
||
// Kein Fehler, Relation wird ignoriert
|
||
```
|
||
|
||
## Best Practices
|
||
|
||
### 1. Selective Loading
|
||
```php
|
||
// ✅ Gut: Nur benötigte Relations laden
|
||
$posts = $hydrator->hydrateManyWithRelations($metadata, $data, ['author', 'comments']);
|
||
|
||
// ❌ Schlecht: Alle Relations laden
|
||
$posts = $hydrator->hydrateManyWithRelations($metadata, $data, ['author', 'comments', 'tags', 'category', 'ratings']);
|
||
```
|
||
|
||
### 2. Batch Processing priorisieren
|
||
```php
|
||
// ✅ Gut: Batch Loading für Collections
|
||
$users = $hydrator->hydrateManyWithRelations($metadata, $dataRows, ['posts']);
|
||
|
||
// ❌ Schlecht: Einzelne Hydration in Loop
|
||
foreach($dataRows as $data) {
|
||
$users[] = $hydrator->hydrateWithRelations($metadata, $data, ['posts']);
|
||
}
|
||
```
|
||
|
||
### 3. Relation Depth begrenzen
|
||
```php
|
||
// ✅ Gut: Direkte Relations
|
||
$posts = $hydrator->hydrateManyWithRelations($metadata, $data, ['author', 'comments']);
|
||
|
||
// ⚠️ Consideration: Nested Relations
|
||
// (Aktuell nicht unterstützt, würde zusätzliche Implementierung benötigen)
|
||
$posts = $hydrator->hydrateManyWithRelations($metadata, $data, ['author.profile', 'comments.author']);
|
||
```
|
||
|
||
## Benefits Summary
|
||
|
||
1. **Performance**: 95-98% Reduktion der Database-Queries
|
||
2. **Scalability**: Lineare statt exponentieller Query-Wachstum
|
||
3. **Resource Efficiency**: Drastisch reduzierte DB-Connection Usage
|
||
4. **User Experience**: Faster page loads and API responses
|
||
5. **Server Stability**: Reduzierter Memory- und CPU-Verbrauch
|
||
6. **Database Health**: Weniger Lock-Contention und Connection-Pool-Pressure
|
||
|
||
## Migration Path
|
||
|
||
### Bestehende Code-Base
|
||
```php
|
||
// Vorher: Lazy Loading mit N+1 Problem
|
||
$users = $repository->findAll();
|
||
foreach($users as $user) {
|
||
// Implicit queries triggered
|
||
$user->posts;
|
||
$user->profile;
|
||
}
|
||
|
||
// Nachher: Explicit Eager Loading
|
||
$userData = $repository->findAllAsData(); // Raw data query
|
||
$users = $hydrator->hydrateManyWithRelations($metadata, $userData, ['posts', 'profile']);
|
||
```
|
||
|
||
### Performance Monitoring
|
||
- Monitor Query Count vor/nach Migration
|
||
- Database Profiling für Query-Performance
|
||
- Application Response Time Tracking
|
||
- Memory Usage Monitoring bei Large Collections |