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
SELECTwith the fluentselect()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).
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 separatecondition()call on a select query narrows the result set further; to get OR logic you'd need to build an explicitdb_or()/orConditionGroup()condition group instead.)
Key takeaways
- Always get a connection via
\Drupal::database()(or, better, an injecteddatabaseservice) — 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.