Skip to content

Performance

Understand the runtime behavior that matters once your usage moves beyond small CRUD flows.

EntityManager keeps an in-memory identity map. Loading the same entity class and primary key twice returns the same PHP object instance until the manager is cleared.

This avoids duplicate objects for the same row, but it also means long-running imports should clear the manager periodically.

$qb->enableResultCache($lifetime, $cacheId);

Result cache stores query results. Locked queries are never cached.

$em = new EntityManager($connection, secondLevelCache: $pool);

Second-level cache stores raw entity row data for find() lookups by entity class and primary key. It survives EntityManager::clear(), unlike the identity map.

When a flush updates, deletes, or soft-deletes an entity, Articulate evicts cache entries for every mapped entity class that shares the same table and primary key — keeping same-row projections consistent after writes.

Implement QueryLoggerInterface to profile query count, SQL text, and cache effects.

Use PartialHydrator or ScalarHydrator when full entity hydration is unnecessary — reporting paths, aggregate output, and read-only projections all benefit. See Hydration & Proxies.

Avoid loading very large datasets with a single getResult() call. Process rows in bounded batches and call $entityManager->clear() between batches when the identity map would otherwise grow too large.

$batch = $entityManager
->createQueryBuilder(OrderSnapshot::class)
->orderBy('placed_at', 'ASC')
->limit($chunkSize)
->offset($offset)
->getResult();
$entityManager->clear();

QueryBuilder::chunk($size) wraps this limit/offset loop in a generator, so you don’t manage $offset by hand:

foreach ($qb->orderBy('placed_at', 'ASC')->chunk($chunkSize) as $batch) {
foreach ($batch as $snapshot) {
// process $snapshot
}
$entityManager->clear();
}