- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
38 KiB
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
- Default to Forward-Only: Nur
Migrationimplementieren, außer du bist SICHER, dass Rollback safe ist - Document Why Safe: Kommentiere, warum eine Migration
SafelyReversibleist - Test Rollback: Teste
up()→down()→up()Cycle in Development - Production: Forward-Only: Auch "safe" Rollbacks sollten in Production vermieden werden
- 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 LoadingEntityPersister: Insert, Update, Delete OperationenEntityQueryManager: QueryBuilder IntegrationEntityUtilities: 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:
finalclasses, composition over inheritance - ✅ Value Objects: Keine Primitive Obsession
- ✅ Type Safety: Union Types für Backwards Compatibility
- ✅ Framework Integration:
__toString()für seamless integration