Skip to content

Entity Query Patterns

When to Use

When querying entities by field values, properties, or relationships, requiring performant database queries with access control and cache invalidation.

Decision

If you need... Use... Why
Load by field value Entity query with condition() Efficient database query, access check
Load by property Query on entity keys Base table query, no field table joins
Complex conditions conditionAggregate(), orConditionGroup() AND/OR logic, aggregates
Sort by field sort() Database-level sorting
Count results count()->execute() Get count without loading entities
Bypass access control accessCheck(FALSE) Admin operations only

Pattern

Basic entity query:

// Inject entity_type.manager service
$query = $this->entityTypeManager->getStorage('node')->getQuery()
  ->accessCheck(TRUE)  // ALWAYS explicit
  ->condition('type', 'article')
  ->condition('status', 1)
  ->condition('field_tags.entity.name', 'Drupal', 'CONTAINS')
  ->sort('created', 'DESC')
  ->range(0, 10);

$nids = $query->execute();
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);

Complex query with OR conditions:

$query = $this->entityTypeManager->getStorage('node')->getQuery()
  ->accessCheck(TRUE);

$orGroup = $query->orConditionGroup()
  ->condition('field_category', 'Technology')
  ->condition('field_tags.entity.name', 'Tech', 'CONTAINS');

$query->condition($orGroup)
  ->condition('status', 1)
  ->sort('field_priority', 'DESC')
  ->sort('created', 'DESC');

$nids = $query->execute();

Count query:

$count = $this->entityTypeManager->getStorage('node')->getQuery()
  ->accessCheck(TRUE)
  ->condition('type', 'article')
  ->condition('status', 1)
  ->count()
  ->execute();

Reference: /core/lib/Drupal/Core/Entity/Query/QueryInterface.php

Common Mistakes

  • Wrong: Forgetting accessCheck() → Right: Exception in Drupal 10+; MUST be explicit (TRUE or FALSE)
  • Wrong: Using accessCheck(FALSE) without justification → Right: Security issue; bypasses access control
  • Wrong: Loading entities when only IDs needed → Right: Performance waste; use count() or return IDs only
  • Wrong: Querying in loops (N+1 problem) → Right: Use single query with IN condition instead
  • Wrong: Not using range() for large result sets → Right: Load thousands of entities; memory exhaustion
  • Wrong: Sorting by computed fields → Right: Not in database; sort after loading or denormalize

Security: - ALWAYS use accessCheck(TRUE) unless you have explicit reason to bypass. - accessCheck(FALSE) is for admin operations, cron, batch processes only. - Document WHY when using accessCheck(FALSE). It's a security smell. - Entity query respects hook_query_TAG_alter(). Be aware of query alterations.

Performance: - Use count()->execute() instead of loading entities when only count needed. - range() limits results at database level. Use for pagination. - Sorting by fields requires joins. Sort by base properties (created, changed) when possible. - Entity reference queries (field_ref.entity.name) create joins. Limit when possible. - Cache query results using cache API. Invalidate on entity save/delete.

Performance improvements (Drupal 10.5/11.2): - Fixed performance regression for sites with large revision counts - Optimized latest revision calculation for entity queries - See: https://www.md-systems.ch/en/blog/2025-12-16/performance-improvements-drupal-11-3

Development Standards: - Inject entity_type.manager service, don't use \Drupal::entityTypeManager() - Always call accessCheck() explicitly (required in Drupal 10+) - Use storage methods, not Entity::load() static calls - Add @todo comment for accessCheck(FALSE) with justification

See Also