Files
michaelschiemer/docs/claude/database-patterns.md

38 KiB
Raw Blame History

Database Patterns

Umfassende Dokumentation der Database-Patterns im Custom PHP Framework.

Migration System: Safe Rollback Architecture

Das Framework verwendet ein intelligentes Migration-System, das zwischen sicheren und unsicheren Rollbacks unterscheidet.

Core Concepts

Forward-Only by Default: Alle Migrations sind standardmäßig forward-only (nur up() Methode erforderlich).

Optional Safe Rollback: Migrations, die SICHER rückgängig gemacht werden können (ohne Datenverlust), implementieren zusätzlich das SafelyReversible Interface.

Migration Interface

/**
 * Base migration interface - Forward-only by default
 */
interface Migration
{
    public function up(ConnectionInterface $connection): void;
    public function getVersion(): MigrationVersion;
    public function getDescription(): string;
}

SafelyReversible Interface

/**
 * Optional interface for migrations that support safe rollback
 *
 * ONLY implement this if rollback is safe (no data loss)!
 */
interface SafelyReversible
{
    public function down(ConnectionInterface $connection): void;
}

Safe vs Unsafe Migrations

Safe for Rollback (implement SafelyReversible):

  • Creating new tables (can be dropped)
  • Adding nullable columns (can be removed)
  • Creating/dropping indexes (no data affected)
  • Renaming columns (data preserved)
  • Adding/removing foreign keys (constraints only)

Unsafe for Rollback (only Migration):

  • Dropping columns with data
  • Transforming data formats
  • Changing column types (data loss risk)
  • Merging/splitting tables
  • Deleting data

Example: Safe Rollback

use App\Framework\Database\Migration\{Migration, SafelyReversible};

/**
 * Safe: New table can be dropped without data loss
 */
final readonly class CreateSessionsTable implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->create('sessions', function ($table) {
            $table->string('id', 255)->primary();
            $table->text('data');
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->dropIfExists('sessions');
        $schema->execute();
    }
}

Example: Unsafe Migration

/**
 * UNSAFE: Dropping column with data - NOT reversible
 */
final readonly class RemoveLegacyIdColumn implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('legacy_id');  // Data is LOST!
        });
        $schema->execute();
    }

    // NO down() method - data cannot be recovered
}

Rollback Safety Check

Der MigrationRunner prüft automatisch, ob eine Migration sicher rollbar ist:

$ php console.php db:rollback 1

🔄 Rolling back migrations...
⚠️  Safety Check: Only migrations implementing SafelyReversible will be rolled back.

❌ Rollback failed: Migration 2024_12_19_150000 does not support safe rollback

  This migration cannot be safely rolled back.
   Reason: Data loss would occur during rollback.

💡 Recommendation:
   Create a new forward migration to undo the changes instead:
   php console.php make:migration FixYourChanges

📖 See docs/claude/examples/migrations/SafeVsUnsafeMigrations.md for guidelines.

Fix-Forward Strategy

Statt unsichere Rollbacks: Neue Forward-Migration erstellen.

// Original migration (dropped column)
final readonly class RemoveDeprecatedField implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('deprecated_field');
        });
        $schema->execute();
    }
}

// Fix-Forward migration (restore column)
final readonly class RestoreDeprecatedField implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->string('deprecated_field')->nullable();
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Column is empty after restoration
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('deprecated_field');
        });
        $schema->execute();
    }
}

Best Practices

  1. Default to Forward-Only: Nur Migration implementieren, außer du bist SICHER, dass Rollback safe ist
  2. Document Why Safe: Kommentiere, warum eine Migration SafelyReversible ist
  3. Test Rollback: Teste up()down()up() Cycle in Development
  4. Production: Forward-Only: Auch "safe" Rollbacks sollten in Production vermieden werden
  5. See Examples: Vollständige Guidelines in docs/claude/examples/migrations/SafeVsUnsafeMigrations.md

Database Value Objects

Das Framework verwendet Value Objects für alle Database-Identifier, um Type Safety und SQL-Injection-Prevention zu gewährleisten.

Verfügbare Database Value Objects

Core Database VOs (src/Framework/Database/ValueObjects/):

  • TableName: Validierte Tabellennamen mit optionalem Schema-Prefix
  • ColumnName: Validierte Spaltennamen
  • IndexName: Validierte Index-Namen
  • ConstraintName: Validierte Constraint-Namen (Foreign Keys, Check Constraints)
  • DatabaseName: Validierte Datenbank-Namen
  • SchemaName: Validierte Schema-Namen (PostgreSQL)

Value Object Patterns

Alle Database VOs folgen einheitlichen Patterns:

// Immutable readonly classes
final readonly class TableName
{
    public function __construct(
        public string $value,
        public ?SchemaName $schema = null
    ) {
        $this->validate();
    }

    // Factory Method Pattern
    public static function fromString(string $value): self
    {
        return new self($value);
    }

    // __toString() für String-Interpolation
    public function __toString(): string
    {
        return $this->schema
            ? "{$this->schema}.{$this->value}"
            : $this->value;
    }

    // Equality Comparison
    public function equals(self $other): bool
    {
        return $this->value === $other->value
            && $this->schema?->equals($other->schema ?? null);
    }
}

Schema Builder Integration

Der Schema Builder unterstützt sowohl VOs als auch Strings (Union Types) für Backwards Compatibility:

use App\Framework\Database\Schema\Blueprint;
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName};

// ✅ Modern: Value Objects (Type Safe)
$schema->create(TableName::fromString('users'), function (Blueprint $table) {
    $table->string(ColumnName::fromString('email'), 255);
    $table->unique(ColumnName::fromString('email'), IndexName::fromString('uk_users_email'));
});

// ✅ Legacy: Strings (Backwards Compatible)
$schema->create('users', function (Blueprint $table) {
    $table->string('email', 255);
    $table->unique('email', 'uk_users_email');
});

// ✅ Mixed: Kombiniert möglich
$schema->create('users', function (Blueprint $table) {
    $table->string('email', 255); // String
    $table->unique(
        ColumnName::fromString('email'),  // VO
        'uk_users_email'                  // String
    );
});

Variadic Parameters

Command-Methoden nutzen variadic parameters statt Arrays für natürlichere API:

// ✅ Variadic Parameters - Natural
$table->dropColumn('email', 'phone', 'address');

// ❌ Array Parameters - Clunky
$table->dropColumn(['email', 'phone', 'address']);

// ✅ Auch mit VOs
$table->dropColumn(
    ColumnName::fromString('email'),
    ColumnName::fromString('phone')
);

TableName mit Schema-Support

PostgreSQL unterstützt Schema-Prefixe:

use App\Framework\Database\ValueObjects\{TableName, SchemaName};

// Simple table name
$table = TableName::fromString('users');
// Output: "users"

// With schema prefix
$table = new TableName(
    value: 'users',
    schema: SchemaName::fromString('public')
);
// Output: "public.users"

// Schema extraction
if ($table->hasSchema()) {
    echo $table->schema->value; // "public"
}

Validation Rules

Alle Database VOs validieren ihre Eingaben:

TableName Validation:

  • Keine leeren Strings
  • Alphanumerisch + Underscore
  • Beginnt nicht mit Ziffer
  • Max. 63 Zeichen (PostgreSQL Limit)
  • Reservierte Keywords verboten (siehe RESERVED_KEYWORDS)

ColumnName Validation:

  • Keine leeren Strings
  • Alphanumerisch + Underscore
  • Beginnt nicht mit Ziffer
  • Max. 63 Zeichen
  • Reservierte Keywords verboten

IndexName/ConstraintName:

  • Gleiche Rules wie ColumnName
  • Prefix-Conventions: idx_, uk_, fk_, ck_ empfohlen

Example Validation:

// ✅ Valid
TableName::fromString('users');
TableName::fromString('user_profiles');

// ❌ Invalid - Exception geworfen
TableName::fromString('');           // Leer
TableName::fromString('123users');   // Beginnt mit Ziffer
TableName::fromString('user-table'); // Ungültiges Zeichen
TableName::fromString('select');     // Reserviertes Keyword

Migration Best Practices

Migration mit Database VOs

Neue Migrationen können VOs verwenden für zusätzliche Type Safety:

use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Schema\{Blueprint, Schema};
use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName, ConstraintName};
use App\Framework\Database\Schema\ForeignKeyAction;

final class CreateUserProfilesTable implements Migration
{
    public function up(Schema $schema): void
    {
        $schema->create(TableName::fromString('user_profiles'), function (Blueprint $table) {
            // Primary Key
            $table->string(ColumnName::fromString('ulid'), 26)
                ->primary();

            // Foreign Key
            $table->string(ColumnName::fromString('user_id'), 26);

            // Relationship
            $table->foreign(ColumnName::fromString('user_id'))
                ->references(ColumnName::fromString('ulid'))
                ->on(TableName::fromString('users'))
                ->onDelete(ForeignKeyAction::CASCADE);

            // Index
            $table->index(
                ColumnName::fromString('user_id'),
                IndexName::fromString('idx_user_profiles_user_id')
            );

            // Composite Index mit variadic parameters
            $table->index(
                ColumnName::fromString('user_id'),
                ColumnName::fromString('created_at'),
                IndexName::fromString('idx_user_profiles_lookup')
            );

            // Timestamps
            $table->timestamps();
        });
    }

    public function down(Schema $schema): void
    {
        $schema->dropIfExists(TableName::fromString('user_profiles'));
    }
}

Migration Naming Conventions

Index Names:

// Pattern: idx_{table}_{columns}
IndexName::fromString('idx_users_email')
IndexName::fromString('idx_users_email_active')

// Unique constraints: uk_{table}_{columns}
IndexName::fromString('uk_users_email')

// Composite
IndexName::fromString('idx_orders_user_created')

Foreign Key Constraints:

// Pattern: fk_{table}_{referenced_table}
ConstraintName::fromString('fk_user_profiles_users')
ConstraintName::fromString('fk_orders_users')

Check Constraints:

// Pattern: ck_{table}_{column}_{condition}
ConstraintName::fromString('ck_users_age_positive')
ConstraintName::fromString('ck_orders_total_min')

Backwards Compatibility

Bestehende Migrationen funktionieren ohne Änderungen:

// ✅ Legacy-Code bleibt voll funktionsfähig
$schema->create('users', function (Blueprint $table) {
    $table->string('email');
    $table->unique('email', 'uk_users_email');
});

// Union Types ermöglichen schrittweise Migration
// string|TableName, string|ColumnName, etc.

Drop Operations

Variadic parameters machen Drop-Operationen natürlich:

$schema->table('users', function (Blueprint $table) {
    // Drop multiple columns
    $table->dropColumn('old_field', 'deprecated_field', 'unused_field');

    // Drop foreign key by constraint name
    $table->dropForeign('fk_users_company');

    // Drop foreign key by column (finds constraint automatically)
    $table->dropForeign('company_id');

    // Drop index by name
    $table->dropIndex('idx_users_email');

    // Drop index by columns (finds index automatically)
    $table->dropIndex('email', 'active');
});

EntityManager Usage

Overview

Der EntityManager ist das zentrale Interface für alle Datenbank-Operationen. Er kombiniert mehrere Patterns:

  • Unit of Work: Change Tracking und Transaction Management
  • Identity Map: Vermeidung von Duplikaten
  • Lazy Loading: Performance-Optimierung durch verzögertes Laden
  • Batch Loading: N+1 Query Prevention

Core Features

Service Classes (interne Organisation):

  • EntityFinder: Entity-Suche und Lazy Loading
  • EntityPersister: Insert, Update, Delete Operationen
  • EntityQueryManager: QueryBuilder Integration
  • EntityUtilities: Hilfsmethoden und Profiling

Basic Operations

Finding Entities

use App\Framework\Database\EntityManager;

final readonly class UserService
{
    public function __construct(
        private EntityManager $entityManager
    ) {}

    public function getUserById(UserId $id): ?User
    {
        // Lazy Loading (Standard)
        return $this->entityManager->find(User::class, $id->value);
    }

    public function getUserByIdEager(UserId $id): ?User
    {
        // Eager Loading - lädt Relations sofort
        return $this->entityManager->findEager(User::class, $id->value);
    }

    public function getAllUsers(): array
    {
        return $this->entityManager->findAll(User::class);
    }

    public function findUserByEmail(Email $email): ?User
    {
        return $this->entityManager->findOneBy(
            User::class,
            ['email' => $email->value]
        );
    }
}

Saving Entities

public function createUser(CreateUserCommand $command): User
{
    $user = User::create(
        id: $this->entityManager->generateId(),
        email: $command->email,
        name: $command->name
    );

    // save() erkennt automatisch INSERT vs UPDATE
    return $this->entityManager->save($user);
}

public function updateUser(User $user, UpdateUserCommand $command): User
{
    // Explizites UPDATE
    $user->updateProfile($command->name);
    return $this->entityManager->update($user);
}

Batch Operations

public function createMultipleUsers(array $commands): array
{
    $users = [];
    foreach ($commands as $command) {
        $users[] = User::create(
            id: $this->entityManager->generateId(),
            email: $command->email,
            name: $command->name
        );
    }

    // Batch-Insert für bessere Performance
    return $this->entityManager->saveAll(...$users);
}

Deleting Entities

public function deleteUser(User $user): void
{
    $this->entityManager->delete($user);
}

N+1 Query Prevention

Problem: Lazy Loading führt zu N+1 Queries

// ❌ N+1 Problem
$users = $this->entityManager->findAll(User::class);

foreach ($users as $user) {
    // Jede Iteration führt eine separate Query aus (N+1)
    echo $user->getProfile()->bio;
}

Solution: Batch Loading mit Relations

// ✅ Single Query mit Batch Loading
$users = $this->entityManager->findWithRelations(
    User::class,
    criteria: [],
    relations: ['profile', 'posts'] // Lädt Relations in Batches
);

foreach ($users as $user) {
    // Kein zusätzlicher Query - Profile bereits geladen
    echo $user->getProfile()->bio;
}

Lazy Loading vs Eager Loading

Lazy Loading (Standard):

  • Relations werden bei Zugriff geladen
  • Performance-Vorteil bei ungenutzten Relations
  • Potentielles N+1 Problem
$user = $this->entityManager->find(User::class, $id);
// Profile wird erst bei Zugriff geladen:
$bio = $user->getProfile()->bio; // Separate Query

Eager Loading:

  • Relations werden sofort geladen
  • Kein N+1 Problem
  • Overhead bei ungenutzten Relations
$user = $this->entityManager->findEager(User::class, $id);
// Profile ist bereits geladen - kein zusätzlicher Query
$bio = $user->getProfile()->bio;

Identity Map

Der EntityManager nutzt eine Identity Map zur Vermeidung von Duplikaten:

// Beide Aufrufe liefern die GLEICHE Instanz
$user1 = $this->entityManager->find(User::class, '123');
$user2 = $this->entityManager->find(User::class, '123');

var_dump($user1 === $user2); // true

Vorteile:

  • Keine Duplikate im Speicher
  • Konsistente Object Identity
  • Automatisches Change Tracking

Utility Methods:

// Detach Entity (aus Identity Map entfernen)
$this->entityManager->detach($user);

// Clear Identity Map (z.B. für Batch Processing)
$this->entityManager->clear();

// Identity Map Statistics
$stats = $this->entityManager->getIdentityMapStats();
// Returns: ['total_entities' => 150, 'entities_by_class' => [...]]

Reference Loading

getReference() - Lade Entity-Referenz ohne Existenz-Check:

// Erstellt Proxy ohne Database Query
$user = $this->entityManager->getReference(User::class, $userId);

// Query wird erst bei Zugriff ausgeführt
$name = $user->getName(); // Jetzt wird geladen

Use Cases:

  • Foreign Key Relationships
  • Performance-kritische Pfade
  • Wenn Existenz bereits bekannt ist

Profiling und Debugging

// Profiling aktivieren
$this->entityManager->setProfilingEnabled(true);

// Operationen ausführen
$users = $this->entityManager->findAll(User::class);

// Profiling-Statistiken abrufen
$stats = $this->entityManager->getProfilingStatistics();

// Profiling-Summary
$summary = $this->entityManager->getProfilingSummary();
// Returns: ProfileSummary mit total_queries, total_time, etc.

// Profiling-Daten löschen
$this->entityManager->clearProfilingData();

Criteria API (Type-Safe Queries)

Modern: Verwendung von Criteria statt Arrays

use App\Framework\Database\Criteria\DetachedCriteria;

// Type-safe Criteria
$criteria = DetachedCriteria::forEntity(User::class)
    ->where('email', '=', $email->value)
    ->andWhere('active', '=', true)
    ->orderBy('created_at', 'DESC')
    ->limit(10);

$users = $this->entityManager->findByCriteria($criteria);
$user = $this->entityManager->findOneByCriteria($criteria);
$count = $this->entityManager->countByCriteria($criteria);

QueryBuilder Integration

Für komplexe Queries:

// QueryBuilder für Entity-Klasse
$queryBuilder = $this->entityManager->createQueryBuilderFor(User::class);

$users = $queryBuilder
    ->select('*')
    ->where('email LIKE ?', ['%@example.com'])
    ->andWhere('created_at > ?', ['2024-01-01'])
    ->orderBy('name', 'ASC')
    ->limit(50)
    ->fetchAll();

// QueryBuilder für Table (ohne Entity)
$qb = $this->entityManager->createQueryBuilderForTable('users');

Domain Events Integration

// Domain Event aufzeichnen
$this->entityManager->recordDomainEvent($user, new UserRegisteredEvent($user));

// Events für Entity dispatchen
$this->entityManager->dispatchDomainEventsForEntity($user);

// Alle Events dispatchen
$this->entityManager->dispatchAllDomainEvents();

// Event-Statistiken
$stats = $this->entityManager->getDomainEventStats();
// Returns: ['total_events' => 23, 'events_by_entity' => [...]]

Repository Pattern

Overview

Das Repository Pattern abstrahiert Daten-Zugriff und bietet domain-spezifische Query-Methoden. Das Framework nutzt Composition over Inheritance.

Base EntityRepository

Framework-Service für Common Operations:

namespace App\Framework\Database\Repository;

final readonly class EntityRepository
{
    public function __construct(
        private EntityManager $entityManager
    ) {}

    public function find(string $entityClass, string $id): ?object
    public function findAll(string $entityClass): array
    public function findBy(string $entityClass, array $criteria, ...): array
    public function findOneBy(string $entityClass, array $criteria): ?object
    public function save(object $entity): object
    public function delete(object $entity): void
    public function transaction(callable $callback): mixed

    // Batch Loading (N+1 Prevention)
    public function findWithRelations(...): array

    // Pagination
    public function findPaginated(...): PaginatedResult

    // Batch Operations
    public function saveBatch(array $entities, int $batchSize = 100): array
    public function deleteBatch(array $entities, int $batchSize = 100): void
}

Domain Repository Pattern

Composition-basiert statt Inheritance:

namespace App\Domain\User\Repositories;

use App\Framework\Database\Repository\EntityRepository;

final readonly class UserRepository
{
    public function __construct(
        private EntityRepository $entityRepository
    ) {}

    public function findById(UserId $id): ?User
    {
        return $this->entityRepository->find(User::class, $id->value);
    }

    public function findByEmail(Email $email): ?User
    {
        return $this->entityRepository->findOneBy(
            User::class,
            ['email' => $email->value]
        );
    }

    public function findActiveUsers(): array
    {
        return $this->entityRepository->findBy(
            User::class,
            criteria: ['active' => true],
            orderBy: ['created_at' => 'DESC']
        );
    }

    public function save(User $user): User
    {
        return $this->entityRepository->save($user);
    }

    public function delete(User $user): void
    {
        $this->entityRepository->delete($user);
    }

    // Domain-spezifische Methode mit Batch Loading
    public function findUsersWithProfiles(array $userIds): array
    {
        return $this->entityRepository->findWithRelations(
            User::class,
            criteria: ['id' => $userIds],
            relations: ['profile', 'settings']
        );
    }

    // Pagination
    public function findPaginated(int $page, int $limit = 20): PaginatedResult
    {
        return $this->entityRepository->findPaginated(
            User::class,
            page: $page,
            limit: $limit,
            orderBy: ['created_at' => 'DESC']
        );
    }
}

Repository Best Practices

Composition Over Inheritance:

// ✅ Framework Pattern - Composition
final readonly class OrderRepository
{
    public function __construct(
        private EntityRepository $entityRepository
    ) {}
}

// ❌ Avoid - Inheritance
class OrderRepository extends BaseRepository
{
    // Problematisch: Tight coupling
}

Domain-Specific Methods:

// ✅ Sprechende Domain-Methoden
public function findOverdueOrders(): array
{
    return $this->entityRepository->findBy(
        Order::class,
        ['status' => 'pending'],
        orderBy: ['due_date' => 'ASC']
    );
}

// ❌ Generic Methods außerhalb Domain
public function findBy(array $criteria): array
{
    // Zu generisch - nutze EntityRepository direkt
}

N+1 Prevention:

// ✅ Batch Loading für Relations
public function findOrdersWithItems(array $orderIds): array
{
    return $this->entityRepository->findWithRelations(
        Order::class,
        criteria: ['id' => $orderIds],
        relations: ['items', 'customer'] // Lädt in Batches
    );
}

Unit of Work Pattern

Overview

Der Unit of Work Pattern verwaltet Entities und deren Änderungen für transaktionale Konsistenz.

Key Features:

  • Change Tracking: Automatische Änderungserkennung
  • Transactional Consistency: Atomic Commits/Rollbacks
  • Batch Operations: Optimierte Bulk-Operationen
  • Entity States: NEW, MANAGED, DIRTY, DELETED, DETACHED

Entity Lifecycle

use App\Framework\Database\UnitOfWork\UnitOfWork;

final readonly class OrderService
{
    public function __construct(
        private EntityManager $entityManager
    ) {}

    public function createOrder(CreateOrderCommand $command): Order
    {
        $unitOfWork = $this->entityManager->unitOfWork;

        // 1. Create new entity (state: NEW)
        $order = Order::create($command);

        // 2. Persist marks entity for INSERT (state: NEW → MANAGED)
        $unitOfWork->persist($order);

        // 3. Flush writes to database (state: MANAGED)
        $unitOfWork->flush();

        // 4. Commit transaction
        $unitOfWork->commit();

        return $order;
    }
}

Change Tracking

Automatic Change Detection:

$unitOfWork = $this->entityManager->unitOfWork;

// Load entity (state: MANAGED)
$user = $this->entityManager->find(User::class, $userId);

// Modify entity
$user->updateProfile($newName);

// Change Tracking erkennt Änderung automatisch
$unitOfWork->flush(); // UPDATE query wird generiert
$unitOfWork->commit();

Manual Change Tracking:

// Entity aus externer Quelle (state: DETACHED)
$user = unserialize($serializedUser);

// Merge in Unit of Work (state: DETACHED → MANAGED)
$managedUser = $unitOfWork->merge($user);

// Änderungen werden getrackt
$unitOfWork->flush();

Transaction Management

Explicit Transactions:

public function processOrder(Order $order): void
{
    $unitOfWork = $this->entityManager->unitOfWork;

    try {
        $unitOfWork->beginTransaction();

        // Multiple operations
        $order->confirm();
        $unitOfWork->persist($order);

        $this->inventoryService->reserve($order->items);
        $this->paymentService->charge($order->total);

        // Write changes to database
        $unitOfWork->flush();

        // Commit transaction
        $unitOfWork->commit();
    } catch (\Exception $e) {
        $unitOfWork->rollback();
        throw $e;
    }
}

Implicit Transactions (Auto-Commit):

// Auto-commit ist standardmäßig aktiviert
$unitOfWork->persist($entity); // Automatisch committed

Disable Auto-Commit für Batch Operations:

$unitOfWork->setAutoCommit(false);

try {
    $unitOfWork->beginTransaction();

    foreach ($orders as $order) {
        $unitOfWork->persist($order); // Kein Auto-Commit
    }

    $unitOfWork->flush();   // Batch-Write
    $unitOfWork->commit();  // Atomic Commit
} catch (\Exception $e) {
    $unitOfWork->rollback();
    throw $e;
} finally {
    $unitOfWork->setAutoCommit(true);
}

Entity States

use App\Framework\Database\UnitOfWork\EntityState;

// Check Entity State
$state = $unitOfWork->getEntityState($entity);

match ($state) {
    EntityState::NEW => 'Entity marked for INSERT',
    EntityState::MANAGED => 'Entity being tracked, no changes',
    EntityState::DIRTY => 'Entity modified, pending UPDATE',
    EntityState::DELETED => 'Entity marked for DELETE',
    EntityState::DETACHED => 'Entity not managed by UnitOfWork',
};

// Check if entity is managed
if ($unitOfWork->contains($entity)) {
    // Entity is managed (NEW, MANAGED, DIRTY, or DELETED)
}

Detaching Entities

// Detach entity from Unit of Work
$unitOfWork->detach($user);

// Entity state: DETACHED
// Changes to entity are no longer tracked

Use Cases:

  • Long-running processes
  • Serialization/Caching
  • Read-only operations
  • Memory management in batch processing

Bulk Operations

// Bulk Insert
$unitOfWork->bulkInsert($entities); // Optimized batch INSERT

// Bulk Update
$unitOfWork->bulkUpdate($entities); // Optimized batch UPDATE

// Bulk Delete
$unitOfWork->bulkDelete($entities); // Optimized batch DELETE

Query Optimization

N+1 Query Problem

Erkennnung: Jede Iteration führt zusätzliche Query aus

// ❌ N+1 Problem
$orders = $this->entityManager->findAll(Order::class); // 1 Query

foreach ($orders as $order) {
    echo $order->getCustomer()->name; // N Queries (1 pro Order)
}
// Total: 1 + N Queries

Solution: Batch Loading

// ✅ Batch Loading
$orders = $this->entityManager->findWithRelations(
    Order::class,
    criteria: [],
    relations: ['customer', 'items'] // Preload relations
);

foreach ($orders as $order) {
    echo $order->getCustomer()->name; // Kein Query - bereits geladen
}
// Total: 2-3 Queries (1 für Orders, 1-2 für Relations)

Eager vs Lazy Loading

Strategic Loading:

// Heavy Operation - Eager Loading
public function getOrderDetails(OrderId $id): OrderDetails
{
    $order = $this->entityManager->findEager(Order::class, $id->value);
    // Alle Relations sofort geladen
    return OrderDetails::fromOrder($order);
}

// Light Operation - Lazy Loading
public function listOrders(): array
{
    // Relations werden nur bei Bedarf geladen
    return $this->entityManager->findAll(Order::class);
}

Pagination

use App\Framework\Database\Repository\EntityRepository;

public function getUsers(int $page): PaginatedResult
{
    return $this->entityRepository->findPaginated(
        User::class,
        page: $page,
        limit: 20,
        orderBy: ['created_at' => 'DESC']
    );
}

// PaginatedResult enthält:
// - items: array (Entities)
// - total: int (Total count)
// - page: int (Current page)
// - limit: int (Items per page)
// - totalPages: int (Calculated)

Query Caching

// EntityManager mit Cache
$entityManager = new EntityManager(
    // ...
    cacheManager: $cacheManager
);

// Find verwendet Cache automatisch
$user = $entityManager->find(User::class, $id);
// Beim zweiten Aufruf: Cache Hit

Connection Pooling

Overview

Connection Pooling verbessert Performance durch Wiederverwendung von Datenbankverbindungen.

Features:

  • Min/Max Connection Limits
  • Connection Health Monitoring
  • Automatic Retry mit Exponential Backoff
  • Idle Connection Cleanup
  • Connection Warmup

Configuration

use App\Framework\Database\Config\PoolConfig;

$poolConfig = new PoolConfig(
    minConnections: 2,        // Minimum pool size
    maxConnections: 10,       // Maximum pool size
    maxIdleTime: 300,         // Idle timeout (seconds)
    healthCheckInterval: 60,  // Health check frequency
    enableWarmup: true        // Pre-create min connections
);

Usage

use App\Framework\Database\ConnectionPool;

$pool = new ConnectionPool($driverConfig, $poolConfig, $timer);

// Get connection from pool
$connection = $pool->getConnection();

try {
    // Use connection
    $result = $connection->query('SELECT * FROM users');
} finally {
    // Return connection to pool (automatic with PooledConnection)
    $connection->release();
}

Health Monitoring

// Pool automatically monitors connection health
$pool->performHealthCheck();

// Get pool statistics
$stats = $pool->getPoolStatistics();
// Returns:
[
    'current_connections' => 5,
    'max_connections' => 10,
    'connections_in_use' => 3,
    'healthy_connections' => 5,
    'total_created' => 12,
    'total_destroyed' => 7
]

Retry Logic

// Automatic retry with exponential backoff
$retryStrategy = ExponentialBackoffStrategy::create()
    ->withMaxAttempts(3)
    ->withBaseDelay(Duration::fromMilliseconds(100));

// Connection pool uses retry automatically
$connection = $pool->getConnectionWithRetry($retryStrategy);

Production Configuration

// Production Pool Config
$productionPoolConfig = new PoolConfig(
    minConnections: 5,         // Keep 5 connections ready
    maxConnections: 50,        // Scale up to 50 under load
    maxIdleTime: 600,          // 10 minutes idle timeout
    healthCheckInterval: 30,   // Check every 30 seconds
    enableWarmup: true,        // Pre-warm connections
    connectionTimeout: 5000    // 5 second timeout
);

Transaction Management

Basic Transactions

// EntityManager Transaction Helper
$this->entityManager->transaction(function (EntityManager $em) {
    $order = Order::create($command);
    $em->save($order);

    $this->inventoryService->reserve($order->items);
    $this->paymentService->charge($order->total);

    // Automatic commit on success, rollback on exception
});

Manual Transaction Control

$unitOfWork = $this->entityManager->unitOfWork;

$unitOfWork->beginTransaction();

try {
    // Operations
    $unitOfWork->persist($entity1);
    $unitOfWork->persist($entity2);

    $unitOfWork->flush();
    $unitOfWork->commit();
} catch (\Exception $e) {
    $unitOfWork->rollback();
    throw $e;
}

Nested Transactions (Savepoints)

$unitOfWork->beginTransaction();

try {
    $order = Order::create($command);
    $unitOfWork->persist($order);

    // Nested transaction (savepoint)
    $unitOfWork->beginTransaction();
    try {
        $this->inventoryService->reserve($order->items);
        $unitOfWork->commit(); // Commit savepoint
    } catch (\Exception $e) {
        $unitOfWork->rollback(); // Rollback to savepoint
        throw $e;
    }

    $unitOfWork->commit(); // Commit main transaction
} catch (\Exception $e) {
    $unitOfWork->rollback(); // Rollback main transaction
    throw $e;
}

Transaction Isolation Levels

use App\Framework\Database\TransactionIsolation;

$unitOfWork->beginTransaction(
    isolation: TransactionIsolation::READ_COMMITTED
);

// Available isolation levels:
// - READ_UNCOMMITTED (lowest isolation)
// - READ_COMMITTED (default)
// - REPEATABLE_READ
// - SERIALIZABLE (highest isolation)

Transaction Best Practices

Short Transactions:

// ✅ Keep transactions short
$this->entityManager->transaction(fn(EntityManager $em) =>
    $em->save($entity)
);

Long Transactions:

// ❌ Avoid long-running transactions
$this->entityManager->transaction(function (EntityManager $em) {
    $this->sendEmail($user); // External I/O - BAD
    $this->processHeavyComputation(); // CPU-intensive - BAD
    $em->save($entity);
});

Batch with Periodic Commits:

// ✅ Batch processing with periodic commits
$unitOfWork->setAutoCommit(false);
$batchSize = 100;

for ($i = 0; $i < count($items); $i++) {
    $unitOfWork->persist($items[$i]);

    if (($i + 1) % $batchSize === 0) {
        $unitOfWork->flush();
        $unitOfWork->commit();
        $unitOfWork->beginTransaction();
    }
}

$unitOfWork->flush();
$unitOfWork->commit();
$unitOfWork->setAutoCommit(true);

Database Testing

Testing mit Database VOs

use App\Framework\Database\ValueObjects\{TableName, ColumnName};

it('creates table with value objects', function () {
    $tableName = TableName::fromString('test_users');

    $this->schema->create($tableName, function (Blueprint $table) {
        $table->string(ColumnName::fromString('email'));
    });

    expect($this->schema->hasTable($tableName))->toBeTrue();
});

it('validates table name format', function () {
    TableName::fromString('123invalid'); // Should throw
})->throws(\InvalidArgumentException::class);

Test Cleanup

afterEach(function () {
    // Cleanup mit VOs
    $this->schema->dropIfExists(TableName::fromString('test_users'));
    $this->schema->dropIfExists(TableName::fromString('test_profiles'));
});

Performance Considerations

Value Object Overhead

Database VOs haben minimalen Performance-Overhead:

  • Creation: ~0.01ms pro VO (Validation einmalig)
  • String Conversion: ~0.001ms via __toString()
  • Memory: ~200 bytes pro VO Instance
  • Recommendation: VOs sind für alle Database Operations akzeptabel

Caching Strategies

// VO Instances können gecached werden
final class TableNameCache
{
    private static array $cache = [];

    public static function get(string $name): TableName
    {
        return self::$cache[$name] ??= TableName::fromString($name);
    }
}

// Usage
$users = TableNameCache::get('users');
$profiles = TableNameCache::get('user_profiles');

Migration Pattern Recommendations

Simple Tables (String-basiert OK)

Für einfache Tabellen ohne komplexe Constraints:

$schema->create('simple_logs', function (Blueprint $table) {
    $table->id();
    $table->string('message');
    $table->timestamps();
});

Complex Tables (VOs empfohlen)

Für Tabellen mit Relationships, Constraints, Composite Indexes:

use App\Framework\Database\ValueObjects\{TableName, ColumnName, IndexName, ConstraintName};

$schema->create(TableName::fromString('complex_orders'), function (Blueprint $table) {
    // Type Safety für alle Identifier
    $table->string(ColumnName::fromString('ulid'), 26)->primary();
    $table->string(ColumnName::fromString('user_id'), 26);

    $table->foreign(ColumnName::fromString('user_id'))
        ->references(ColumnName::fromString('ulid'))
        ->on(TableName::fromString('users'))
        ->onDelete(ForeignKeyAction::CASCADE);

    // Composite indexes mit explicit naming
    $table->index(
        ColumnName::fromString('user_id'),
        ColumnName::fromString('status'),
        ColumnName::fromString('created_at'),
        IndexName::fromString('idx_orders_user_status_created')
    );
});

SQL Injection Prevention

Database VOs bieten eingebauten Schutz:

// ✅ VOs validieren Input - SQL Injection unmöglich
$tableName = TableName::fromString($_GET['table']); // Wirft Exception bei Injection-Versuch

// ❌ Raw strings sind gefährlich
$query = "SELECT * FROM {$_GET['table']}"; // SQL Injection möglich!

// ✅ VOs in Queries nutzen
$query = "SELECT * FROM {$tableName}"; // Validated and safe

Framework Compliance

Database VOs folgen allen Framework-Prinzipien:

  • Readonly Classes: Alle VOs sind final readonly
  • Immutability: Keine State-Mutation nach Construction
  • No Inheritance: final classes, composition over inheritance
  • Value Objects: Keine Primitive Obsession
  • Type Safety: Union Types für Backwards Compatibility
  • Framework Integration: __toString() für seamless integration