Files
michaelschiemer/backups/docs-backup-20250731125004/database/eager-loading.md
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

7.4 KiB
Raw Blame History

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)

// 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)

// 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

// 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

// 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

$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)

$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

// 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

// 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)

// 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)

// 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

// Ä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

// 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

$result = $hydrator->hydrateManyWithRelations($metadata, [], ['posts']);
// Returns: [] (empty array, keine Fehler)

Invalid Relations

// Nicht-existierende oder Nicht-Relations werden übersprungen
$user = $hydrator->hydrateWithRelations($metadata, $data, ['invalid_relation']); 
// Kein Fehler, Relation wird ignoriert

Best Practices

1. Selective Loading

// ✅ 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

// ✅ 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

// ✅ 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

// 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