Enfoque SQL
Nette Database ofrece dos formas de trabajar: puede escribir usted mismo las consultas SQL (enfoque SQL) o dejar que se generen automáticamente (véase Explorer). El enfoque SQL le da control total sobre las consultas y a la vez garantiza que se construyan de forma segura.
Los detalles sobre la conexión y la configuración de la base de datos los encontrará en el capítulo Conexión y configuración.
Consultas básicas
Para consultar la base de datos se usa el método query(). Devuelve un objeto ResultSet, que representa el resultado de la
consulta. Si la consulta falla, el método lanza una excepción. Puede
recorrer el resultado de la consulta con un bucle foreach o usar alguno de los métodos auxiliares.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
Para insertar valores en las consultas SQL de forma segura, use consultas parametrizadas. Nette Database lo hace extremadamente sencillo: basta con añadir una coma y el valor después de la consulta SQL:
$database->query('SELECT * FROM users WHERE name = ?', $name);
Con varios parámetros tiene dos opciones. Puede intercalar la consulta SQL con los parámetros:
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
O escribir primero la consulta SQL entera y añadir después todos los parámetros:
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Protección contra la inyección SQL
¿Por qué es importante usar consultas parametrizadas? Porque le protegen de un ataque llamado inyección SQL, en el que un atacante podría inyectar sus propios comandos SQL y con ello acceder a los datos de la base de datos o dañarlos.
¡Nunca inserte variables directamente en una consulta SQL! Use siempre consultas parametrizadas, que le protegen de la inyección SQL.
// ❌ CÓDIGO PELIGROSO - vulnerable a la inyección SQL
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Consulta parametrizada segura
$database->query('SELECT * FROM users WHERE name = ?', $name);
Familiarícese con los posibles riesgos de seguridad.
Técnicas de consulta
Condiciones WHERE
Las condiciones WHERE se pueden escribir como un array asociativo en el que las claves son los nombres de las
columnas y los valores son los datos con los que comparar. Nette Database elige automáticamente el operador SQL más adecuado
según el tipo del valor.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
También puede indicar explícitamente el operador de comparación en la clave:
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // usa el operador >
'name LIKE' => '%John%', // usa el operador LIKE
'email NOT LIKE' => '%example.com%', // usa el operador NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette gestiona automáticamente los casos especiales, como los valores null o los arrays.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // usa el operador =
'category_id' => [1, 2, 3], // usa IN
'description' => null, // usa IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
Para las condiciones negativas use el operador NOT:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // usa el operador !=
'category_id NOT' => [1, 2, 3], // usa NOT IN
'description NOT' => null, // usa IS NOT NULL
'id NOT' => [], // se omite
]);
// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
De forma predeterminada, las condiciones se unen con el operador AND. Se puede cambiar con el marcador ?or.
Reglas de ORDER BY
La cláusula ORDER BY se puede escribir con un array. Indique las columnas en las claves y use un valor booleano
para señalar el orden ascendente (true) o descendente (false):
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // ascendente
'name' => false, // descendente
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Insertar datos (INSERT)
Para insertar registros se usa el comando SQL INSERT.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
El método getInsertId() devuelve el ID de la última fila insertada. En algunas bases de datos (p. ej.
PostgreSQL) hay que indicar como parámetro el nombre de la secuencia de la que debe generarse el ID, con
$database->getInsertId($sequenceId).
Como parámetros también puede pasar Valores especiales, como archivos, objetos DateTime o tipos enum.
Insertar varios registros a la vez:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
Un INSERT de varios registros es mucho más rápido, porque se ejecuta una sola consulta a la base de datos en lugar de muchas individuales.
Nota de seguridad: nunca use datos sin validar como $values. Familiarícese con los posibles riesgos.
Actualizar datos (UPDATE)
Para actualizar registros se usa el comando SQL UPDATE.
// Actualiza un solo registro
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
El número de filas afectadas lo devuelve $result->getRowCount().
En UPDATE podemos usar los operadores += y -=:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // incrementa login_count
], 1);
Ejemplo de insertar o actualizar un registro si ya existe. Usamos la técnica ON DUPLICATE KEY UPDATE:
$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
Fíjese en que Nette Database reconoce el contexto en el que se usa un parámetro de tipo array dentro del comando SQL y
construye el código SQL en consecuencia. Así, del primer array construyó
(id, name, year) VALUES (123, 'Jim', 1978), mientras que el segundo lo convirtió en la forma
name = 'Jim', year = 1978. Lo tratamos con más detalle en la sección Marcadores para construir SQL.
Borrar datos (DELETE)
Para borrar registros se usa el comando SQL DELETE. Ejemplo de cómo obtener el número de filas borradas:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Marcadores para construir SQL
Un marcador es un símbolo especial en una consulta SQL que indica cómo debe convertirse el valor del parámetro en una expresión SQL:
| Marcador | Descripción | Se usa automáticamente en |
|---|---|---|
?name |
Sirve para insertar nombres de tablas o columnas | – |
?values |
Genera (clave, ...) VALUES (valor, ...) |
INSERT ... ?, REPLACE ... ? |
?set |
Genera asignaciones clave = valor, ... |
SET ?, KEY UPDATE ? |
?and |
Une las condiciones de un array con AND |
WHERE ?, HAVING ? |
?or |
Une las condiciones de un array con OR |
– |
?order |
Genera la cláusula ORDER BY |
ORDER BY ?, GROUP BY ? |
El marcador ?name sirve para insertar dinámicamente nombres de tablas y columnas en la consulta. Nette Database
se encarga del entrecomillado correcto de los identificadores según las convenciones de la base de datos (p. ej. encerrándolos
entre acentos graves en MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (en MySQL)
Atención: use el marcador ?name solo para nombres de tablas y columnas validados. De lo contrario se
arriesga a sufrir vulnerabilidades de
seguridad.
Los demás marcadores no suelen hacer falta indicarlos, porque Nette usa una autodetección inteligente al construir la
consulta SQL (véase la tercera columna de la tabla). Pero puede usarlos, por ejemplo, en una situación en la que quiera unir las
condiciones con OR en lugar de 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'
Valores especiales
Además de los tipos escalares habituales (string, int, bool), como parámetros también puede pasar valores especiales:
- archivos:
fopen('image.gif', 'r')inserta el contenido binario del archivo - fecha y hora: los objetos
DateTimeInterfacese convierten al formato de la base de datos - tipos enum: las instancias de
enumse convierten a su valor - literales SQL: creados con
Connection::literal('NOW()'), se insertan directamente en la consulta
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTimeImmutable, // o new DateTime
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
En las bases de datos que no tienen soporte nativo para el tipo de dato datetime (como SQLite y Oracle), los
objetos DateTime y DateTimeImmutable se convierten a un valor indicado en la configuración de la base de datos mediante el elemento
formatDateTime (el valor por defecto es U, el timestamp de Unix).
Literales SQL
En algunos casos necesita pasar como valor código SQL en bruto, que no debe tratarse como cadena ni escaparse. Para eso sirven
los objetos de la clase Nette\Database\SqlLiteral. Se crean con el método Connection::literal().
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
O alternativamente:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
Los literales SQL pueden contener parámetros:
$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)
Eso permite combinaciones interesantes:
$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')
Obtener los datos
Atajos para las consultas SELECT
Para simplificar la obtención de datos, Connection ofrece varios atajos que combinan una llamada a
query() con la llamada posterior a fetch*(). Estos métodos aceptan los mismos parámetros que
query(), es decir, una consulta SQL y parámetros opcionales. La descripción completa de los métodos
fetch*() la encontrará más abajo.
fetch($sql, ...$params): ?Row |
Ejecuta la consulta y devuelve la primera fila como objeto Row, o null. |
fetchAll($sql, ...$params): array |
Ejecuta la consulta y devuelve todas las filas como array de objetos Row. |
fetchPairs($sql, ...$params): array |
Ejecuta la consulta y devuelve un array asociativo (pares clave ⇒ valor). |
fetchField($sql, ...$params): mixed |
Ejecuta la consulta y devuelve el valor de la primera columna de la primera fila. |
fetchList($sql, ...$params): ?array |
Ejecuta la consulta y devuelve la primera fila como array indexado, o null. |
Ejemplo:
// fetchField() - devuelve el valor de la primera celda
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach: recorrer las filas
Tras ejecutar una consulta se devuelve un objeto ResultSet, que permite recorrer los resultados de
varias maneras. La forma más fácil de ejecutar una consulta y obtener las filas es recorrerla con un bucle foreach.
Este método es el más eficiente en memoria, porque obtiene los datos fila a fila y no carga todo el conjunto de resultados en
memoria de una vez.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
El ResultSet solo se puede recorrer una vez. Si necesita recorrerlo repetidamente, primero tiene que
cargar los datos en un array, por ejemplo con el método fetchAll().
fetch(): ?Row
Devuelve una fila como objeto Row. Si ya no hay más filas, devuelve null. Avanza el puntero interno
a la fila siguiente.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // carga la primera fila
if ($row) {
echo $row->name;
}
fetchAll(): array
Devuelve todas las filas restantes del ResultSet como array de objetos Row.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // carga todas las filas
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
Devuelve el conjunto de resultados como array asociativo. El primer argumento indica la columna que se usará como claves y el segundo, la que se usará como valores:
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
Si solo se indica el primer parámetro ($key), como valor se usará la fila entera (el objeto
Row):
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
En caso de claves duplicadas se usa el valor de la última fila. Usar null como clave da un array indexado
numéricamente (empezando desde cero), lo que evita las colisiones de claves:
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
Alternativamente puede indicar un callback que procese cada fila. El callback puede devolver un único valor o un par clave-valor.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// El callback también puede devolver un array con un par clave y valor:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Devuelve el valor de la primera columna de la fila actual. Si ya no hay más filas, devuelve null. Avanza el
puntero interno a la fila siguiente.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // carga el nombre de la primera fila
fetchList(): ?array
Devuelve la fila como array indexado. Si ya no hay más filas, devuelve null. Avanza el puntero interno a la fila
siguiente.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Devuelve el número de filas afectadas por la última consulta UPDATE o DELETE. En las consultas
SELECT devuelve el número de filas del conjunto de resultados. Eso, sin embargo, no siempre se puede saber, y en ese
caso el método devuelve null.
getColumnCount(): ?int
Devuelve el número de columnas del ResultSet.
Información sobre la consulta
Para depurar podemos obtener información sobre la última consulta ejecutada:
echo $database->getLastQueryString(); // imprime la consulta SQL
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // imprime la consulta SQL
echo $result->getTime(); // imprime el tiempo de ejecución en segundos
Para mostrar el resultado como tabla HTML puede usar:
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet ofrece información sobre los tipos de las columnas:
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column is of type $type"; // p. ej. 'id is of type int'
}
Registro de las consultas
Podemos implementar nuestro propio registro de consultas. El evento onQuery es un array de callbacks que se llaman
después de cada consulta ejecutada:
$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');
}
};