SQL Way
Nette Database offers two ways of working: you can write SQL queries yourself (SQL way), or have them generated automatically (see Explorer). The SQL way gives you full control over the queries while ensuring they are constructed securely.
Details on database connection and configuration can be found in the Connection and Configuration chapter.
Basic Querying
The query()
method is used for database querying. It returns a ResultSet object, which represents the query
result. If the query fails, the method throws an exception. You can
iterate through the query result using a foreach
loop, or use one of the helper
methods.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
To securely insert values into SQL queries, use parameterized queries. Nette Database makes this extremely simple: just add a comma and the value after the SQL query:
$database->query('SELECT * FROM users WHERE name = ?', $name);
With multiple parameters, you have two options: You can either interleave the SQL query with parameters:
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
Or write the entire SQL query first and then append all the parameters:
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Protection Against SQL Injection
Why is it important to use parameterized queries? Because they protect you from an attack called SQL injection, where an attacker could inject their own SQL commands and thereby gain access to or damage data in the database.
Never insert variables directly into an SQL query! Always use parameterized queries, which protect you from SQL injection.
// ❌ DANGEROUS CODE - vulnerable to SQL injection
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Secure parameterized query
$database->query('SELECT * FROM users WHERE name = ?', $name);
Familiarize yourself with potential security risks.
Querying Techniques
WHERE Conditions
You can write WHERE
conditions as an associative array, where keys are column names and values are the data for
comparison. Nette Database automatically selects the most suitable SQL operator based on the value type.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
You can also explicitly specify the comparison operator in the key:
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // uses the > operator
'name LIKE' => '%John%', // uses the LIKE operator
'email NOT LIKE' => '%example.com%', // uses the NOT LIKE operator
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette automatically handles special cases like null
values or arrays.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // uses the = operator
'category_id' => [1, 2, 3], // uses IN
'description' => null, // uses IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
For negative conditions, use the NOT
operator:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // uses the <> operator
'category_id NOT' => [1, 2, 3], // uses NOT IN
'description NOT' => null, // uses IS NOT NULL
'id' => [], // skipped
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
By default, conditions are joined using the AND
operator. This can be changed using the ?or placeholder.
ORDER BY Rules
The ORDER BY
clause can be written using an array. Specify columns in the keys, and use a boolean value to
indicate ascending (true
) or descending (false
) order:
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // ascending
'name' => false, // descending
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Inserting Data (INSERT)
The SQL INSERT
command is used for inserting records.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
The getInsertId()
method returns the ID of the last inserted row. For some databases (e.g., PostgreSQL), it is
necessary to specify the name of the sequence from which the ID should be generated as a parameter, using
$database->getInsertId($sequenceId)
.
You can also pass special values, such as files, DateTime objects, or enum types, as parameters.
Inserting multiple records at once:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
A multi-record INSERT is much faster because only a single database query is executed, instead of many individual ones.
Security Note: Never use unvalidated data as $values
. Familiarize yourself with possible risks.
Updating Data (UPDATE)
The SQL UPDATE
command is used for updating records.
// Update a single record
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
The number of affected rows is returned by $result->getRowCount()
.
For UPDATE
, we can use the +=
and -=
operators:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // increment login_count
], 1);
Example of inserting or updating a record if it already exists. We use the ON DUPLICATE KEY UPDATE
technique:
$values = [
'name' => $name,
'year' => $year,
];
$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?',
$values + ['id' => $id],
$values,
);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978
Notice that Nette Database recognizes the context in which an array parameter is used within the SQL command and constructs the
SQL code accordingly. So, from the first array, it constructed (id, name, year) VALUES (123, 'Jim', 1978)
, while it
converted the second into the form name = 'Jim', year = 1978
. We discuss this in more detail in the section SQL Construction Hints.
Deleting Data (DELETE)
The SQL DELETE
command is used for deleting records. Example of obtaining the number of deleted rows:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
SQL Construction Hints
A hint is a special placeholder in an SQL query that specifies how the parameter value should be converted into an SQL expression:
Hint | Description | Automatically Used For |
---|---|---|
?name |
Used for inserting table or column names | – |
?values |
Generates (key, ...) VALUES (value, ...) |
INSERT ... ? , REPLACE ... ? |
?set |
Generates assignments key = value, ... |
SET ? , KEY UPDATE ? |
?and |
Joins conditions in an array with AND |
WHERE ? , HAVING ? |
?or |
Joins conditions in an array with OR |
– |
?order |
Generates the ORDER BY clause |
ORDER BY ? , GROUP BY ? |
The ?name
placeholder is used for dynamically inserting table and column names into the query. Nette Database
handles the correct quoting of identifiers according to the database conventions (e.g., enclosing in backticks in MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (in MySQL)
Warning: Only use the ?name
placeholder for validated table and column names. Otherwise, you risk security vulnerabilities.
Other hints usually do not need to be specified, as Nette uses smart autodetection when constructing the SQL query (see the
third column of the table). But you can use it, for example, in a situation where you want to join conditions using
OR
instead of AND
:
$database->query('SELECT * FROM users WHERE ?or', [
'name' => 'John',
'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'
Special Values
In addition to common scalar types (string, int, bool), you can also pass special values as parameters:
- files:
fopen('image.gif', 'r')
inserts the binary content of the file - date and time:
DateTime
andDateTimeImmutable
objects are converted to the database format - enum types: instances of
enum
are converted to their value - SQL literals: created using
Connection::literal('NOW()')
are inserted directly into the query
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTimeImmutable, // or new DateTime
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
For databases that do not have native support for the datetime
data type (like SQLite and Oracle),
DateTime
and DateTimeImmutable
objects are converted to a value specified in the database configuration by the formatDateTime
item (default
value is U
– Unix timestamp).
SQL Literals
In some cases, you need to pass raw SQL code as a value, which should not be treated as a string and escaped. Objects of the
Nette\Database\SqlLiteral
class are used for this purpose. They are created by the Connection::literal()
method.
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
Alternatively:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
SQL literals can contain parameters:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)
This allows for interesting combinations:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('?or', [
'active' => true,
'role' => $role,
]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')
Fetching Data
Shortcuts for SELECT Queries
To simplify data retrieval, Connection
offers several shortcuts that combine a query()
call with a
subsequent fetch*()
call. These methods accept the same parameters as query()
, i.e., an SQL query and
optional parameters. A full description of the fetch*()
methods can be found below.
fetch($sql, ...$params): ?Row |
Executes the query and returns the first row as a Row object or null . |
fetchAll($sql, ...$params): array |
Executes the query and returns all rows as an array of Row objects. |
fetchPairs($sql, ...$params): array |
Executes the query and returns an associative array (key ⇒ value pairs). |
fetchField($sql, ...$params): mixed |
Executes the query and returns the value of the first column in the first row. |
fetchList($sql, ...$params): ?array |
Executes the query and returns the first row as an indexed array or null . |
Example:
// fetchField() - returns the value of the first cell
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach
– Iterating Over Rows
After executing a query, a ResultSet object
is returned, which allows iterating through the results in several ways. The easiest way to execute a query and retrieve rows is
by iterating in a foreach
loop. This method is the most memory-efficient, as it fetches data row by row and does not
load the entire result set into memory at once.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
The ResultSet
can only be iterated once. If you need to iterate repeatedly, you must first load the
data into an array, for example, using the fetchAll()
method.
fetch(): ?Row
Returns a row as a Row
object. If no more rows exist, it returns null
. Advances the internal pointer
to the next row.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // loads the first row
if ($row) {
echo $row->name;
}
fetchAll(): array
Returns all remaining rows from the ResultSet
as an array of Row
objects.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // loads all rows
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
Returns the result set as an associative array. The first argument specifies the column to use as keys, and the second argument specifies the column to use as values:
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
If only the first parameter ($key
) is provided, the entire row (Row
object) will be used as
the value:
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
In case of duplicate keys, the value from the last row is used. Using null
as the key results in a numerically
indexed array (starting from zero), preventing key collisions:
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
Alternatively, you can provide a callback that processes each row. The callback can return a single value or a key-value pair.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// The callback can also return an array with a key & value pair:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Returns the value of the first column from the current row. If no more rows exist, it returns null
. Advances the
internal pointer to the next row.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // loads the name from the first row
fetchList(): ?array
Returns the row as an indexed array. If no more rows exist, it returns null
. Advances the internal pointer to the
next row.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Returns the number of affected rows from the last UPDATE
or DELETE
query. For SELECT
queries, it returns the number of rows in the result set. However, this might not always be known, in which case the method
returns null
.
getColumnCount(): ?int
Returns the number of columns in the ResultSet
.
Query Information
For debugging purposes, we can obtain information about the last executed query:
echo $database->getLastQueryString(); // prints the SQL query
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // prints the SQL query
echo $result->getTime(); // prints the execution time in seconds
To display the result as an HTML table, you can use:
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet
provides information about column types:
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column is of type $type->type"; // e.g., 'id is of type int'
}
Query Logging
We can implement custom query logging. The onQuery
event is an array of callbacks that are called after each
executed query:
$database->onQuery[] = function ($database, $result) use ($logger) {
$logger->info('Query: ' . $result->getQueryString());
$logger->info('Time: ' . $result->getTime());
if ($result->getRowCount() > 1000) {
$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
}
};