Using entity.query for custom entity queriesfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Lesson one of this section showed a quick entity query in passing. Now it's time to actually understand the tool: \Drupal::entityQuery() is Drupal's way of letting you ask "which entities match these conditions?" without writing a single line of SQL, regardless of which entity type you're querying or how its data is actually stored underneath. Think of it as an order form for a librarian rather than wandering the stacks yourself — you fill in what you're looking for, and the query builder figures out how to actually search the shelves.

What you'll learn in this lesson

  • How to build a query with conditions, sorting, and a result limit
  • The difference between AND and OR condition groups
  • Why an entity query returns IDs, and what to do with them next

Building a basic query

$query = \Drupal::entityQuery('content_entity_example_contact')
  ->condition('role', 'administrator')
  ->condition('name', 'Jane', 'CONTAINS')
  ->sort('created', 'DESC')
  ->range(0, 20)
  ->accessCheck(TRUE);

$ids = $query->execute();
$contacts = \Drupal::entityTypeManager()
  ->getStorage('content_entity_example_contact')
  ->loadMultiple($ids);

Multiple condition() calls are combined with AND by default — this query finds contacts whose role is exactly "administrator" and whose name contains "Jane". The third argument to condition() is the comparison operator; it defaults to =, but accepts CONTAINS, STARTS_WITH, >, <, IN, and more. sort() and range() work exactly like their SQL equivalents — order first, then page through with range.

Grouping conditions with OR

$or_group = $query->orConditionGroup()
  ->condition('role', 'administrator')
  ->condition('role', 'editor');

$query->condition($or_group)
  ->condition('status', 1);

A plain chain of condition() calls can only express AND. To express "role is administrator OR editor, AND status is published," build a separate orConditionGroup(), add your OR conditions to that, and then add the whole group to the main query as if it were a single condition. andConditionGroup() exists too, for nesting AND logic inside an outer OR.

Why the query returns IDs, not entities

Notice execute() returns an array of IDs, and loading the actual entities is a separate call. This two-step design exists because computing a matching ID set is comparatively cheap and easy to reason about, while fully loading an entity (running it through every field's storage layer) is comparatively expensive — keeping the steps apart means Drupal never loads more than you actually asked to load, and it lets you count results (->count()->execute()) without paying the cost of loading anything at all.

accessCheck() is not optional — since Drupal 9.3 you must call accessCheck(TRUE) or accessCheck(FALSE) explicitly on every entity query. TRUE filters results down to what the current user is actually allowed to see; FALSE deliberately bypasses that, which is correct in a cron job or an admin report but a real security bug if the results ever reach an anonymous request.

Quick check: you want a simple count of how many published contacts exist, without needing the actual contact data. Would you call execute() then count($ids) in PHP, or use the query builder's own count() method? Use ->count()->execute() — it asks the database to return a single number directly, rather than transferring every matching ID over the wire just to throw them away.

Key takeaways

  • \Drupal::entityQuery($entity_type_id) builds conditions, sorting, and paging without any raw SQL, and works identically across entity types.
  • Chained condition() calls combine with AND; use orConditionGroup()/andConditionGroup() to express anything more complex.
  • condition()'s third argument is the comparison operator — CONTAINS, STARTS_WITH, IN, and the usual relational operators are all supported.
  • execute() returns matching entity IDs, not loaded entities — load them separately, and only when you actually need the full data.
  • accessCheck(TRUE) or accessCheck(FALSE) is mandatory on every query; getting the choice wrong is a genuine access-control risk, not just a style issue.

Coming up next

You've now covered the full arc of working with entities: loading them, defining a brand-new type, wiring up its handlers, giving it fields, and querying it. Next, we shift from entities living inside your site to exposing that same data to the outside world, starting with Creating a REST Resource.