Drupalwoo ☉

Using Drupal Entity Query for Custom Data Retrieval

Custom data retrieval in Drupal often starts with a simple question: which records match these fields, and which user may see them? The Entity Query API provides a consistent query layer for entities, while entity storage loads the matching objects. Field values, bundles, translations and metadata therefore remain within Drupal’s normal data model.

Entity Query does not replace every database query. It is built around known entity types and fields. When information lives in custom SQL storage, a reporting calculation or a transient cache, another API may be cleaner. Choosing the right method begins with identifying where the data lives and who must access it.

Understanding Entity Query's role

Drupal separates the query from the loading process. The entity type manager supplies storage for a type, storage creates an entity query, and the query returns matching entity IDs. This makes the API useful for nodes, users, media, taxonomy terms and custom content entities without giving each one a separate retrieval mechanism.

$storage = $entityTypeManager->getStorage('node');

$ids = $storage->getQuery()
  ->accessCheck(TRUE)
  ->condition('type', 'article')
  ->condition('status', 1)
  ->range(0, 10)
  ->sort('created', 'DESC')
  ->execute();

$nodes = $storage->loadMultiple($ids);

An entity query does not return complete objects or render arrays. It returns identifiers, which keeps the query focused and lets the storage layer load records in bulk. Entity references, revisions and translations remain attached to the loaded entities, while field-level formatting is better left to the presentation layer.

This design suits data represented by Drupal fields. If a module keeps stock levels in a separate database structure, Entity Query has no field definition to target. A database query may be necessary there, but it must also handle entity conversion, caching and access deliberately. The entity model should guide where custom data is created.

Filtering fields and referenced entities

Once storage is available, conditions can target base fields, configurable fields and references. A custom field is queried by its machine name, not by the label shown to editors. The following example finds available records for one client that have not been archived.

$query = $storage->getQuery()
  ->accessCheck(TRUE)
  ->condition('field_client.target_id', $client_id)
  ->condition('field_state', 'available')
  ->condition('field_stock_count', 0, '>')
  ->notExists('field_archived')
  ->sort('field_stock_count', 'ASC');

$ids = $query->execute();

Operators include equality, comparisons, IN, BETWEEN and LIKE. Use strict field values where possible, and remember that LIKE treats percent signs as wildcards. Dates are usually stored as Unix timestamps, while a reference field commonly stores the related entity ID in its target_id property. Filtering by that ID is predictable, but joining through a referenced entity’s stored field can be clearer when the relationship matters.

Reference joins also work, such as condition('field_region.entity.name', 'Victoria'), although they add query complexity and depend on the referenced entity’s schema. Avoid filtering on a computed label because labels can be assembled from several values and may not exist as queryable storage. A stable code, UUID or dedicated field is safer than a display title.

Moving queries into services

Business logic should live in a service rather than a controller that calls Drupal::entityTypeManager() directly. A repository service accepts domain inputs, applies the query and returns loaded entities. Dependency injection makes the class easier to test and keeps static service calls out of reusable code.

use Drupal\Core\Entity\EntityTypeManagerInterface;

final class InventoryRepository {
  public function __construct(
    private EntityTypeManagerInterface $entityTypeManager,
  ) {}

  public function availableForClient(int $clientId): array {
    $storage = $this->entityTypeManager->getStorage('inventory_item');
    $query = $storage->getQuery()
      ->accessCheck(TRUE)
      ->condition('field_client.target_id', $clientId)
      ->condition('field_stock_count', 0, '>');

    $ids = $query->range(0, 50)->execute();

    return $storage->loadMultiple($ids);
  }
}

Register the class in the module’s services.yml file. Drupal then injects the entity type manager, and a controller can receive the repository through its constructor. The service can also centralise logging, default sorting and cache metadata rather than scattering those rules through route callbacks.

services:
  Drupal\my_module\Repository\InventoryRepository:
    arguments:
      - '@entity_type.manager'

Return domain objects, not raw query IDs, when downstream code needs titles, references or access-aware rendering. For a JSON endpoint, explicitly select fields and serialise only the data the endpoint exposes; returning a whole entity can disclose fields that were never intended for the API.

Controlling results and performance

range() controls the number of records returned but does not create a complete pager. A route can use it for a lightweight page, but a searchable public listing is usually better served by Views, which provides exposed filters, result counts, pagination and display configuration. Entity Query remains valuable underneath custom services and endpoints.

$query = $storage->getQuery()
  ->accessCheck(TRUE)
  ->condition('type', 'stock_item')
  ->range($offset, $page_size)
  ->sort('created', 'DESC');

$ids = $query->execute();

Large result sets also need care. loadMultiple() is more efficient than loading each record separately, yet hundreds of entities can still consume substantial memory. Process predictable exports in chunks, cache entities that change rarely and avoid N+1 loads caused by rendering references one at a time.

Indexes matter for frequently filtered fields, especially large catalogues. Review the query’s execution plan during development, keep sorting fields deliberate and avoid broad conditions that force a full scan. Measuring first is important: an Entity Query returning twenty records can be cheaper than a custom aggregate that scans millions of rows.

Extending retrieval for custom needs

Most custom requirements are better solved with a deliberate data model than with query tricks. Put a value in a field when editors must filter, validate or display it. A stored warehouse code, publication status or external ISBN gives Entity Query a dependable target and keeps the value attached to the entity lifecycle.

An entity query alter hook can adjust an existing query, and type-specific variants can target one entity type. Treat these hooks as controlled extensions because they can change unrelated repository calls or administrator queries. Prefer a dedicated query service when the behaviour is a normal part of the module.

Entity Query does not provide SQL aggregation or a direct path to arbitrary database storage. For sums, averages or reporting across normalised records, use the database service with parameterised SQL. For configuration entities, use the Configuration API; for simple counters or cached values, use the key-value or state API. These tools have narrower purposes and clearer access semantics.

Protecting data in Australian projects

Access control belongs in the query and at the point of output. Leave accessCheck(TRUE) enabled for user-facing retrieval. Disabling it can expose unpublished or restricted identifiers, even if a later template happens to hide them. Administrative, command-line or migration code that bypasses access should operate in a tightly controlled context and should never pass those records directly to an unauthorised user.

Drupal’s generic entity access does not amount to field-level permissions. Sensitive information may need a separate restricted entity type, encrypted storage or an application-specific permission check. Also check route access, entity access and the selected display before returning a record. Entity Query access is one layer, not a complete authorisation policy.

Australian projects add practical constraints that are easy to miss in generic examples. A Melbourne agency may support a Sydney publisher while a Brisbane warehouse team records NSW, VIC or QLD stock in Australian dollars and distinguishes GST-inclusive pricing. Australian teams also say “before arvo” for the afternoon, which still needs a timezone and a precise deadline. Model currency, tax treatment, warehouse state and timestamps deliberately instead of embedding them into free-form notes. Many sites are maintained by local agencies or internal teams for councils, universities and nonprofits, so ownership, support hours and a documented handover matter.

Privacy rules may apply as well. If the Drupal site falls under the Privacy Act 1988, assess the Australian Privacy Principles and relevant state or territory obligations, including what must be disclosed after a notifiable data breach. Publish a clear notice that matches the real collection and use of information. The published privacy details can help show why that notice should be easy to find from the relevant form, while each operator should adapt it to its own legal context.

Comparing Drupal retrieval approaches

The correct retrieval approach depends on the data and the interface. Entity Query suits entity-backed records; Views handles configurable listings; the database API serves custom reporting; and configuration APIs manage administrator-defined settings. Each option has a different access model and level of Drupal integration.

Compare them by their intended use:

Approach Best suited to Access model Main limitation
Entity Query Entity-backed domain data and custom field filtering Query access can be enforced; view access still applies No aggregation or display pipeline
Views Filterable administrative and public listings Configured view access and displays More setup for domain-specific services
Database API Custom database storage, aggregates and reporting Developers must enforce permissions and parameterise input Bypasses entity loading, validation and caching
Configuration API Site configuration entities Configuration permissions and dependency management Unsuitable for user-generated content

For a custom inventory service, Entity Query usually provides the best balance of field filtering, entity loading and access awareness. Use Views when editors need a flexible list, and move aggregate reporting to parameterised SQL. This division keeps domain logic testable while giving each Drupal subsystem the job it was designed to handle.