Using Database::getConnection() for queriesfor Drupal 8 , 9 , 10 , and 11

Last updated :  

You wouldn't hand a stranger the keys to every filing cabinet in the building just to let them read one folder. Drupal's database layer works the same way: your module never opens a raw connection to MySQL or PostgreSQL directly — it asks Drupal's own database service for a connection object, and that object is what every query, insert, and update flows through from then on.

What you'll learn in this lesson

  • How to get a database connection with \Drupal::database()
  • How to build a basic SELECT with the fluent select() query builder
  • Why you almost never write raw SQL strings by hand in modern Drupal
  • How to turn query results into usable PHP data with fetchAll()

Getting a connection

$connection = \Drupal::database();

That single line is doing more than it looks. Behind it, Drupal's service container resolves whichever database backend the site is actually configured for — MySQL, MariaDB, PostgreSQL, SQLite — and hands you back a Connection object with a single, backend-agnostic API. Your query code never needs an if-statement for "which database am I running on."

Building a query with select()

Here's a real repository method from Drupal's Examples project that reads rows out of a custom table:

public function load(array $entry = []) {
  $select = $this->connection
    ->select('dbtng_example')
    ->fields('dbtng_example');

  foreach ($entry as $field => $value) {
    $select->condition($field, $value);
  }
  return $select->execute()->fetchAll();
}

select('dbtng_example') starts a query against that table; fields('dbtng_example') says "give me every column." Each call to condition() adds a WHERE clause — and here's the important part: the value is never concatenated into a string. DBTNG (Drupal's query layer) always uses placeholders under the hood, so this method is immune to SQL injection no matter what gets passed into $entry, without you writing a single line of escaping code.

execute() and fetchAll()

Building a query object does nothing on its own — it's inert until execute() runs it and returns a result set. fetchAll() then pulls every matching row back as an array of objects, one per row, with each column available as a public property ($row->name, $row->age, and so on).

Why this matters: notice this method takes zero conditions by default (array $entry = []) and still works — an empty foreach just means no WHERE clause gets added, so calling load() with nothing returns every row in the table. That's a deliberate, convenient default, not an accident.

Quick check: if you called load(['name' => 'John', 'age' => 100]), would the two conditions be combined with AND or OR? (AND — each separate condition() call on a select query narrows the result set further; to get OR logic you'd need to build an explicit db_or()/orConditionGroup() condition group instead.)

Key takeaways

  • Always get a connection via \Drupal::database() (or, better, an injected database service) — never open a raw PDO connection yourself.
  • select($table)->fields($table) is the standard starting shape for reading every column from a table.
  • condition() calls are automatically parameterized — DBTNG queries are SQL-injection-safe by default.
  • A query object does nothing until execute() is called; fetchAll() returns the full result set as an array of row objects.
  • Multiple condition() calls on the same query combine with AND, not OR.

Coming up next

Simple equality conditions cover a lot of ground, but real applications often need joins, ranges, and conditions built up dynamically at runtime. Next, you'll extend this same query builder into genuinely dynamic queries.