Explorador de bases de datos

Con Nette Database, puede trabajar de dos maneras: dejar que las consultas SQL se generen automáticamente (enfoque Explorer) o escribirlas usted mismo(acceso directo). El Explorador simplifica considerablemente el acceso a los datos. Gestiona las relaciones entre tablas para que usted pueda centrarse en la lógica de su aplicación.

  • Trabajar con datos es natural y fácil de entender
  • Genera consultas SQL optimizadas que obtienen sólo los datos necesarios
  • Facilita el acceso a datos relacionados sin necesidad de escribir consultas JOIN.
  • Funciona inmediatamente sin necesidad de configuración ni generación de entidades

El trabajo con el Explorador comienza llamando al método table() del objeto Nette\Database\Explorer (para obtener información sobre la creación de conexiones y la configuración, consulte la página dedicada):

$books = $explorer->table('book'); // 'libro' es el nombre de la tabla

El método devuelve un objeto Selección, que representa una consulta SQL. Se pueden encadenar métodos adicionales a este objeto para filtrar y ordenar los resultados. La consulta se monta y ejecuta sólo cuando se solicitan los datos, por ejemplo, iterando con foreach. Cada fila está representada por un objeto ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // produce la columna "title
	echo $book->author_id;    // produce la columna "author_id
}

Explorer simplifica enormemente el trabajo con las relaciones entre tablas. El siguiente ejemplo muestra con qué facilidad podemos obtener datos de tablas relacionadas (libros y sus autores). Observe que no es necesario escribir consultas JOIN; Nette las genera por nosotros:

$books = $explorer->table('book');

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // crea un JOIN a la tabla 'author
}

Nette Database Explorer optimiza las consultas para obtener la máxima eficacia. El ejemplo anterior sólo realiza dos consultas SELECT, independientemente de si procesamos 10 o 10.000 libros.

Además, Explorer realiza un seguimiento de las columnas que se utilizan en el código y obtiene sólo esas de la base de datos, lo que ahorra aún más rendimiento. Este comportamiento es totalmente automático y adaptable. Si más adelante modifica el código para utilizar columnas adicionales, Explorer ajusta automáticamente las consultas. No es necesario configurar nada ni pensar qué columnas se necesitarán, eso déjelo en manos de Nette.

Filtrado y ordenación

La clase Selection proporciona métodos para filtrar y ordenar datos.

where($condition, ...$params) Añade una condición WHERE. Las condiciones múltiples se combinan usando AND.
whereOr(array $conditions) Añade un grupo de condiciones WHERE combinadas mediante OR.
wherePrimary($value) Añade una condición WHERE basada en la clave primaria.
order($columns, ...$params) Establece la ordenación con ORDER BY
select($columns, ...$params) Especifica qué columnas obtener
limit($limit, $offset = null) Limita el número de filas (LIMIT) y, opcionalmente, establece OFFSET.
page($page, $itemsPerPage, &$total = null) Establece la paginación.
group($columns, ...$params) Agrupa las filas (GROUP BY)
having($condition, ...$params) Añade una condición HAVING para filtrar las filas agrupadas.

Los métodos pueden encadenarse ( interfaz fluida): $table->where(...)->order(...)->limit(...).

Estos métodos también permiten utilizar notaciones especiales para acceder a datos de tablas relacionadas.

Escapes e identificadores

Los métodos escapan automáticamente los parámetros y los identificadores de comillas (nombres de tablas y columnas), evitando la inyección SQL. Para garantizar un funcionamiento correcto, deben seguirse algunas reglas:

  • Escriba las palabras clave, nombres de funciones, procedimientos, etc., en mayúsculas.
  • Escriba los nombres de columnas y tablas en minúsculas.
  • Pasar siempre cadenas utilizando parámetros.
where('name = ' . $name);         // **DISASTER**: vulnerable a la inyección SQL
where('name LIKE "%search%"');    // **WRONG**: complica el entrecomillado automático
where('name LIKE ?', '%search%'); // **CORRECTO**: valor pasado como parámetro

where('name like ?', $name);     // **WRONG**: genera: `nombre` `como` ?
where('name LIKE ?', $name);     // **CORRECTO**: genera: `name` LIKE ?
where('LOWER(name) = ?', $value);// **CORRECTO**: LOWER(`nombre`) = ?

where (string|array $condition, …$parameters)static

Filtra los resultados utilizando condiciones WHERE. Su punto fuerte es el manejo inteligente de varios tipos de valores y la selección automática de operadores SQL.

Uso básico:

$table->where('id', $value);     // DONDE `id` = 123
$table->where('id > ?', $value); // DONDE `id` > 123
$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow'

Gracias a la detección automática de operadores adecuados, no tendrá que ocuparse de casos especiales: Nette los gestiona por usted:

$table->where('id', 1);          // DONDE `id` = 1
$table->where('id', null);       // DONDE `id` ES NULO
$table->where('id', [1, 2, 3]);  // WHERE `id` IN (1, 2, 3)
// El marcador de posición ? puede utilizarse sin operador:
$table->where('id ?', 1);        // WHERE `id` = 1

El método también gestiona correctamente las condiciones negativas y las matrices vacías:

$table->where('id', []);         // WHERE `id` IS NULL AND FALSE -- no encuentra nada
$table->where('id NOT', []);     // WHERE `id` IS NULL OR TRUE -- lo encuentra todo
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- encuentra todo
// $table->where('NOT id ?', $ids); // ADVERTENCIA: Esta sintaxis no está soportada

También puede pasar el resultado de otra consulta de tabla como parámetro, creando una subconsulta:

// WHERE `id` IN (SELECT `id` FROM `tableName`)
$table->where('id', $explorer->table($tableName));

// WHERE `id` IN (SELECT `col` FROM `tableName`)
$table->where('id', $explorer->table($tableName)->select('col'));

Las condiciones también pueden pasarse como una matriz, combinando los elementos mediante AND:

// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`)
$table->where([
	'price_final &lt; price_original',
	'stock_count > min_stock',
]);

En la matriz pueden utilizarse pares clave-valor, y Nette volverá a elegir automáticamente los operadores correctos:

// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3))
$table->where([
	'status' => 'active',
	'id' => [1, 2, 3],
]);

También podemos mezclar expresiones SQL con marcadores de posición y múltiples parámetros. Esto es útil para condiciones complejas con operadores definidos con precisión:

// WHERE (`edad` > 18) AND (ROUND(`puntuación`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // se pasan dos parámetros en forma de matriz
]);

Las llamadas múltiples a where() combinan automáticamente las condiciones utilizando AND.

whereOr (array $parameters)static

Similar a where(), pero combina condiciones utilizando OR:

// WHERE (`status` = 'active') OR (`deleted` = 1)
$table->whereOr([
	'status' => 'active',
	'deleted' => true,
]);

También se pueden utilizar expresiones más complejas:

// WHERE (`precio` > 1000) OR (`precio_con_impuesto` > 1500)
$table->whereOr([
	'price > ?' => 1000,
	'price_with_tax > ?' => 1500,
]);

wherePrimary (mixed $key)static

Añade una condición para la clave primaria de la tabla:

// DONDE `id` = 123
$table->wherePrimary(123);

// WHERE `id` IN (1, 2, 3)
$table->wherePrimary([1, 2, 3]);

Si la tabla tiene una clave primaria compuesta (por ejemplo, foo_id, bar_id), la pasamos como una matriz:

// WHERE `foo_id` = 1 AND `bar_id` = 5
$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch();

// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3))
$table->wherePrimary([
	['foo_id' => 1, 'bar_id' => 5],
	['foo_id' => 2, 'bar_id' => 3],
])->fetchAll();

order (string $columns, …$parameters)static

Especifica el orden en que se devuelven las filas. Puede ordenar por una o más columnas, en orden ascendente o descendente, o por una expresión personalizada:

$table->order('created');                   // ordenado por "creado
$table->order('created DESC');              // ORDER BY `created` DESC
$table->order('priority DESC, created');    // ORDENADO POR "prioridad" DESC, "creado
$table->order('status = ? DESC', 'active'); // ORDER BY "status" = "active" DESC

select (string $columns, …$parameters)static

Especifica las columnas que se devolverán de la base de datos. Por defecto, Nette Database Explorer sólo devuelve las columnas que se utilizan realmente en el código. Utilice el método select() cuando necesite recuperar expresiones específicas:

// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date`
$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y');

Los alias definidos con AS son accesibles como propiedades del objeto ActiveRow:

foreach ($table as $row) {
	echo $row->formatted_date;   // acceder al alias
}

limit (?int $limit, ?int $offset = null)static

Limita el número de filas devueltas (LIMIT) y opcionalmente establece un offset:

$table->limit(10);        // LIMIT 10 (devuelve las 10 primeras filas)
$table->limit(10, 20);    // LIMIT 10 OFFSET 20

Para la paginación, es más apropiado utilizar el método page().

page (int $page, int $itemsPerPage, &$numOfPages = null)static

Simplifica la paginación de los resultados. Acepta el número de página (empezando por 1) y el número de elementos por página. Opcionalmente, puede pasar una referencia a una variable donde se almacenará el número total de páginas:

$numOfPages = null;
$table->page(page: 3, itemsPerPage: 10, $numOfPages);
echo "Total pages: $numOfPages";

group (string $columns, …$parameters)static

Agrupa las filas por las columnas especificadas (GROUP BY). Suele utilizarse en combinación con funciones de agregación:

// Cuenta el número de productos de cada categoría
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Establece una condición para filtrar filas agrupadas (HAVING). Puede utilizarse en combinación con el método group() y las funciones de agregado:

// Encuentra categorías con más de 100 productos
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Lectura de datos

Para leer datos de la base de datos, existen varios métodos útiles:

foreach ($table as $key => $row) Recorre todas las filas, $key es el valor de la clave primaria, $row es un objeto ActiveRow
$row = $table->get($key) Devuelve una única fila por clave primaria.
$row = $table->fetch() Devuelve la fila actual y avanza el puntero a la siguiente
$array = $table->fetchPairs() Crea una matriz asociativa a partir de los resultados.
$array = $table->fetchAll() Devuelve todas las filas en forma de matriz
count($table) Devuelve el número de filas del objeto Selección.

El objeto ActiveRow es de sólo lectura. Esto significa que no puede cambiar los valores de sus propiedades. Esta restricción garantiza la coherencia de los datos y evita efectos secundarios inesperados. Los datos se obtienen de la base de datos, y cualquier cambio debe hacerse explícitamente y de forma controlada.

foreach – Iterar todas las filas

La forma más sencilla de ejecutar una consulta y recuperar filas es iterando con el bucle foreach. Este ejecuta automáticamente la consulta SQL.

$books = $explorer->table('book');
foreach ($books as $key => $book) {
	// $key = clave primaria, $book = ActiveRow
	echo "$book->title ({$book->author->name})";
}

get ($key): ?ActiveRow

Ejecuta una consulta SQL y devuelve una fila por su clave primaria o null si no existe.

$book = $explorer->table('book')->get(123); // devuelve ActiveRow con ID 123 o null
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Devuelve una fila y avanza el puntero interno a la siguiente. Si no hay más filas, devuelve null.

$books = $explorer->table('book');
while ($book = $books->fetch()) {
	$this->processBook($book);
}

fetchPairs (string|int|null $key = null, string|int|null $value = null)array

Devuelve los resultados como una matriz asociativa. El primer argumento especifica el nombre de la columna que se utilizará como clave de la matriz, y el segundo argumento especifica el nombre de la columna que se utilizará como valor:

$authors = $explorer->table('author')->fetchPairs('id', 'name');
// [1 => 'Juan Pérez', 2 => 'Juana Pérez', ...]

Si sólo se proporciona el primer parámetro, se utilizará toda la fila como valor, representada como un objeto ActiveRow:

$authors = $explorer->table('author')->fetchPairs('id');
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

Si se proporciona null como clave, la matriz se indexará numéricamente empezando por cero:

$authors = $explorer->table('author')->fetchPairs(null, 'name');
// [0 => 'Juan Nadie', 1 => 'Juana Nadie', ...]

fetchPairs (Closure $callback)array

Alternativamente, puede pasar una llamada de retorno como parámetro. La llamada de retorno se aplicará a cada fila para devolver un único valor o un par clave-valor.

$titles = $explorer->table('book')
	->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// [1 => 'Primer libro (Jan Novak)', ...]

// La llamada de retorno también puede devolver un array que contenga un par clave-valor:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Primer libro' => 'Jan Novak', ...]

fetchAll(): array

Devuelve todas las filas como una matriz asociativa de objetos ActiveRow, donde las claves son los valores de la clave principal.

$allBooks = $explorer->table('book')->fetchAll();
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

count(): int

El método count() sin parámetros devuelve el número de filas del objeto Selection:

$table->where('category', 1);
$count = $table->count();
$count = count($table); // alternativa

Nota: count() con un parámetro realiza la función de agregación COUNT en la base de datos, como se describe a continuación.

ActiveRow::toArray(): array

Convierte el objeto ActiveRow en una matriz asociativa donde las claves son los nombres de las columnas y los valores son los datos correspondientes.

$book = $explorer->table('book')->get(1);
$bookArray = $book->toArray();
// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...]

Agregación

La clase Selection proporciona métodos para realizar fácilmente funciones de agregación (COUNT, SUM, MIN, MAX, AVG, etc.).

count($expr) Cuenta el número de filas.
min($expr) Devuelve el valor mínimo de una columna.
max($expr) Devuelve el valor máximo de una columna.
sum($expr) Devuelve la suma de los valores de una columna.
aggregation($function) Permite cualquier función de agregación, como AVG()GROUP_CONCAT()

count (string $expr): int

Ejecuta una consulta SQL con la función COUNT y devuelve el resultado. Este método se utiliza para determinar cuántas filas coinciden con una determinada condición:

$count = $table->count('*');                 // SELECT COUNT(*) FROM `tabla`.
$count = $table->count('DISTINCT column');   // SELECT COUNT(DISTINCT `columna`) FROM `tabla`.

Nota: count() sin un parámetro simplemente devuelve el número de filas del objeto Selection.

min (string $expr) and max(string $expr)

Los métodos min() y max() devuelven los valores mínimo y máximo de la columna o expresión especificada:

// SELECT MAX(`precio`) FROM `productos` WHERE `activo` = 1
$maxPrice = $products->where('active', true)
	->max('price');

sum (string $expr): int

Devuelve la suma de los valores de la columna o expresión especificada:

// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1
$totalPrice = $products->where('active', true)
	->sum('price * items_in_stock');

aggregation (string $function, ?string $groupFunction = null)mixed

Permite la ejecución de cualquier función de agregación.

// Calcula el precio medio de los productos de una categoría
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// Combina las etiquetas de los productos en una sola cadena
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Si necesitamos agregar resultados que a su vez son resultado de una agregación y agrupación (por ejemplo, SUM(value) sobre filas agrupadas), especificamos la función de agregación que se aplicará a estos resultados intermedios como segundo argumento:

// Calcula el precio total de los productos en stock para cada categoría y, a continuación, suma estos precios.
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
	->group('category_id')
	->aggregation('SUM(category_total)', 'SUM');

En este ejemplo, primero calculamos el precio total de los productos de cada categoría (SUM(price * stock) AS category_total) y agrupamos los resultados por category_id. A continuación, utilizamos aggregation('SUM(category_total)', 'SUM') para sumar estos subtotales. El segundo argumento 'SUM' especifica la función de agregación que se aplicará a los resultados intermedios.

Insertar, actualizar y eliminar

Nette Database Explorer simplifica la inserción, actualización y eliminación de datos. Todos los métodos mencionados lanzan un Nette\Database\DriverException en caso de error.

Selection::insert (iterable $data)static

Inserta nuevos registros en una tabla.

Insertar un solo registro:

El nuevo registro se pasa como un array asociativo u objeto iterable (como ArrayHash utilizado en formularios), donde las claves coinciden con los nombres de las columnas de la tabla.

Si la tabla tiene una clave primaria definida, el método devuelve un objeto ActiveRow, que se vuelve a cargar desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, disparadores, valores de columna por defecto o cálculos de autoincremento). Esto garantiza la coherencia de los datos, y el objeto siempre contiene los datos actuales de la base de datos. Si no se define explícitamente una clave primaria, el método devuelve los datos de entrada como una matriz.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row es una instancia de ActiveRow que contiene los datos completos de la fila insertada,
// incluyendo el ID autogenerado y cualquier cambio realizado por triggers
echo $row->id;          // Muestra el ID del nuevo usuario insertado
echo $row->created_at;  // Indica la hora de creación si ha sido establecida por un activador

Insertar varios registros a la vez:

El método insert() permite insertar varios registros con una sola consulta SQL. En este caso, devuelve el número de filas insertadas.

$insertedRows = $explorer->table('users')->insert([
	[
		'name' => 'John',
		'year' => 1994,
	],
	[
		'name' => 'Jack',
		'year' => 1995,
	],
]);
// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995)
// $insertedRows será 2

También puede pasar como parámetro un objeto Selection con una selección de datos.

$newUsers = $explorer->table('potential_users')
	->where('approved', 1)
	->select('name, email');

$insertedRows = $explorer->table('users')->insert($newUsers);

Inserción de valores especiales:

Los valores pueden incluir archivos, objetos DateTime o literales SQL:

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // convierte al formato de la base de datos
	'avatar' => fopen('image.jpg', 'rb'),   // inserta el contenido del archivo binario
	'uuid' => $explorer::literal('UUID()'), // llama a la función UUID()
]);

Selection::update (iterable $data)int

Actualiza las filas de una tabla basándose en un filtro especificado. Devuelve el número de filas realmente modificadas.

Las columnas a actualizar se pasan como un array asociativo u objeto iterable (como ArrayHash utilizado en formularios), donde las claves coinciden con los nombres de las columnas de la tabla:

$affected = $explorer->table('users')
	->where('id', 10)
	->update([
		'name' => 'John Smith',
		'year' => 1994,
	]);
// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10

Para modificar valores numéricos, puede utilizar los operadores += y -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // aumenta el valor de la columna "puntos" en 1
		'coins-=' => 1,   // disminuye el valor de la columna 'coins' en 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Elimina filas de una tabla basándose en un filtro especificado. Devuelve el número de filas eliminadas.

$count = $explorer->table('users')
	->where('id', 10)
	->delete();
// DELETE FROM `users` WHERE `id` = 10

Cuando llame a update() o delete(), asegúrese de utilizar where() para especificar las filas que se van a actualizar o eliminar. Si no se utiliza where(), la operación se realizará en toda la tabla.

ActiveRow::update (iterable $data)bool

Actualiza los datos de una fila de la base de datos representada por el objeto ActiveRow. Acepta datos iterables como parámetro, donde las claves son nombres de columnas. Para modificar valores numéricos, puede utilizar los operadores += y -=:

Una vez realizada la actualización, el ActiveRow se vuelve a cargar automáticamente desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, triggers). El método devuelve true sólo si se ha producido un cambio real en los datos.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // incrementa el número de visitas
]);
echo $article->views; // Muestra el recuento de visitas actual

Este método sólo actualiza una fila específica de la base de datos. Para actualizaciones masivas de varias filas, utilice el método Selection::update().

ActiveRow::delete()

Elimina una fila de la base de datos representada por el objeto ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // Borra el libro con ID 1

Este método borra sólo una fila específica de la base de datos. Para el borrado masivo de múltiples filas, utilice el método Selection::delete().

Relaciones entre tablas

En las bases de datos relacionales, los datos se dividen en varias tablas y se conectan a través de claves externas. Nette Database Explorer ofrece una forma revolucionaria de trabajar con estas relaciones – sin escribir consultas JOIN ni requerir ninguna configuración o generación de entidades.

Para la demostración, utilizaremos el ejemplo de base de datos(disponible en GitHub). La base de datos incluye las siguientes tablas:

  • author – autores y traductores (columnas id, name, web, born)
  • book – libros (columnas id, author_id, translator_id, title, sequel_id)
  • tag – etiquetas (columnas id, name)
  • book_tag – tabla de enlaces entre libros y etiquetas (columnas book_id, tag_id)

Estructura de la base de datos

En este ejemplo de base de datos de libros, encontramos varios tipos de relaciones (simplificadas respecto a la realidad):

  • Uno-a-muchos (1:N) – Cada libro tiene un autor; un autor puede escribir múltiples libros.
  • De cero a muchos (0:N)** – Un libro puede tener un traductor; un traductor puede traducir múltiples libros.
  • Cero a uno (0:1)** – Un libro puede tener una secuela.
  • De muchos a muchos (M:N)** – Un libro puede tener varias etiquetas, y una etiqueta puede asignarse a varios libros.

En estas relaciones, siempre hay una tabla padre y una tabla hijo. Por ejemplo, en la relación entre autores y libros, la tabla author es el padre, y la tabla book es el hijo – se puede pensar que un libro siempre “pertenece” a un autor. Esto también se refleja en la estructura de la base de datos: la tabla hija book contiene la clave foránea author_id, que hace referencia a la tabla padre author.

Si queremos mostrar los libros junto con los nombres de sus autores, tenemos dos opciones. O bien recuperamos los datos mediante una única consulta SQL con un JOIN:

SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id;

O bien recuperamos los datos en dos pasos -primero los libros, luego sus autores- y los ensamblamos en PHP:

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books

El segundo enfoque es, sorprendentemente, más eficiente. Los datos se obtienen una sola vez y pueden utilizarse mejor en caché. Así es exactamente como funciona Nette Database Explorer – se encarga de todo bajo el capó y le proporciona una API limpia:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'title: ' . $book->title;
	echo 'written by: ' . $book->author->name; // $book->author es un registro de la tabla 'author
	echo 'translated by: ' . $book->translator?->name;
}

Acceso a la tabla principal

Acceder a la tabla padre es sencillo. Se trata de relaciones del tipo un libro tiene un autorun libro puede tener un traductor. Se puede acceder al registro relacionado a través de la propiedad del objeto ActiveRow – el nombre de la propiedad coincide con el nombre de la columna de la clave ajena sin el sufijo id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // busca al autor mediante la columna "author_id
echo $book->translator?->name; // busca al traductor mediante la columna "translator_id

Al acceder a la propiedad $book->author, Explorer busca una columna en la tabla book que contenga la cadena author (es decir, author_id). En función del valor de esta columna, recupera el registro correspondiente de la tabla author y lo devuelve como un objeto ActiveRow. Del mismo modo, $book->translator utiliza la columna translator_id. Dado que la columna translator_id puede contener null, se utiliza el operador ?->.

Un método alternativo es ref(), que acepta dos argumentos (el nombre de la tabla de destino y la columna de enlace) y devuelve una instancia de ActiveRow o null:

echo $book->ref('author', 'author_id')->name;      // enlace al autor
echo $book->ref('author', 'translator_id')->name;  // enlace al traductor

El método ref() es útil si no se puede utilizar el acceso basado en propiedades, por ejemplo, cuando la tabla contiene una columna con el mismo nombre que la propiedad (author). En otros casos, se recomienda utilizar el acceso basado en propiedades para mejorar la legibilidad.

Explorer optimiza automáticamente las consultas a la base de datos. Al iterar por los libros y acceder a sus registros relacionados (autores, traductores), Explorer no genera una consulta para cada libro individualmente. En su lugar, ejecuta sólo una consulta SELECT para cada tipo de relación, reduciendo significativamente la carga de la base de datos. Por ejemplo:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo $book->title . ': ';
	echo $book->author->name;
	echo $book->translator?->name;
}

Este código ejecutará sólo tres consultas optimizadas a la base de datos:

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- IDs from 'translator_id' column in selected books

La lógica para identificar la columna de enlace está definida por la implementación de Conventions. Recomendamos utilizar DiscoveredConventions, que analiza las claves externas y permite trabajar sin problemas con las relaciones de tablas existentes.

Acceso a la tabla hija

El acceso a la tabla hija funciona en sentido inverso. Ahora preguntamos qué libros escribió este autorqué libros tradujo este traductor. Para este tipo de consulta, utilizamos el método related(), que devuelve un objeto Selection con registros relacionados. He aquí un ejemplo:

$author = $explorer->table('author')->get(1);

// Muestra todos los libros escritos por el autor
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Salidas de todos los libros traducidos por el autor
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

El método related() acepta la descripción de la relación como argumento único utilizando la notación de puntos o como dos argumentos separados:

$author->related('book.translator_id');    // un solo argumento
$author->related('book', 'translator_id'); // dos argumentos

Explorer puede detectar automáticamente la columna de vinculación correcta basándose en el nombre de la tabla padre. En este caso, enlaza a través de la columna book.author_id porque el nombre de la tabla origen es author:

$author->related('book'); // usa book.author_id

Si existen múltiples conexiones posibles, Explorer lanzará una excepción AmbiguousReferenceKeyException.

Por supuesto, también podemos utilizar el método related() cuando iteramos a través de múltiples registros en un bucle, y Explorer optimizará automáticamente las consultas también en este caso:

$authors = $explorer->table('author');
foreach ($authors as $author) {
	echo $author->name . ' wrote:';
	foreach ($author->related('book') as $book) {
		echo $book->title;
	}
}

Este código genera sólo dos consultas SQL eficientes:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors

Relación múltiple

Para una relación de muchos a muchos (M:N), se requiere una tabla de unión (en nuestro caso, book_tag). Esta tabla contiene dos columnas de clave externa (book_id, tag_id). Cada columna hace referencia a la clave primaria de una de las tablas conectadas. Para recuperar los datos relacionados, primero obtenemos los registros de la tabla de enlace mediante related('book_tag'), y luego continuamos con los datos de destino:

$book = $explorer->table('book')->get(1);
// Muestra los nombres de las etiquetas asignadas al libro
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name; // obtiene el nombre de la etiqueta a través de la tabla de enlaces
}

$tag = $explorer->table('tag')->get(1);
// Dirección opuesta: muestra los títulos de los libros con esta etiqueta
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // busca el título del libro
}

De nuevo, Explorer optimiza las consultas SQL de forma eficiente:

SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...));  -- IDs of the selected books
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- IDs of the tags found in book_tag

En los métodos where(), select(), order(), y group(), puede utilizar notaciones especiales para acceder a columnas de otras tablas. Explorer crea automáticamente los JOINs necesarios.

La notación de puntos (parent_table.column) se utiliza para relaciones 1:N vistas desde la perspectiva de la tabla padre:

$books = $explorer->table('book');

// Busca libros cuyo nombre de autor empiece por "Jon".
$books->where('author.name LIKE ?', 'Jon%');

// Ordena los libros por nombre de autor de forma descendente
$books->order('author.name DESC');

// Muestra el título del libro y el nombre del autor
$books->select('book.title, author.name');

La notación de puntos se utiliza para las relaciones 1:N desde la perspectiva de la tabla padre:

$authors = $explorer->table('author');

// Busca autores que hayan escrito un libro con "PHP" en el título
$authors->where(':book.title LIKE ?', '%PHP%');

// Cuenta el número de libros de cada autor
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

En el ejemplo anterior con notación de dos puntos (:book.title), la columna de clave externa no se especifica explícitamente. Explorer detecta automáticamente la columna correcta basándose en el nombre de la tabla padre. En este caso, se une a través de la columna book.author_id porque el nombre de la tabla origen es author. Si existen múltiples conexiones posibles, Explorer lanza la excepción AmbiguousReferenceKeyException.

La columna de enlace se puede especificar explícitamente entre paréntesis:

// Busca autores que hayan traducido un libro cuyo título incluya "PHP".
$authors->where(':book(translator).title LIKE ?', '%PHP%');

Las notaciones pueden encadenarse para acceder a datos de varias tablas:

// Encuentra autores de libros etiquetados con "PHP".
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Ampliación de las condiciones para JOIN

El método joinWhere() añade condiciones adicionales a las uniones de tablas en SQL después de la palabra clave ON.

Por ejemplo, supongamos que queremos encontrar libros traducidos por un traductor concreto:

// Busca libros traducidos por un traductor llamado 'David
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN autor traductor ON book.translator_id = translator.id AND (translator.name = 'David')

En la condición joinWhere(), puede utilizar las mismas construcciones que en el método where(): operadores, marcadores de posición, matrices de valores o expresiones SQL.

Para consultas más complejas con múltiples JOINs, se pueden definir alias de tablas:

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born &lt; ?', 1950)
	->alias(':book_tag.book.author', 'book_author');
// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id`
// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id`
// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id`
//   AND (`book_author`.`born` < 1950)

Tenga en cuenta que mientras que el método where() añade condiciones a la cláusula WHERE, el método joinWhere() amplía las condiciones de la cláusula ON durante las uniones de tablas.

Creación manual del explorador

Si no está utilizando el contenedor Nette DI, puede crear una instancia de Nette\Database\Explorer manualmente:

use Nette\Database;

// $storage implementa Nette\Caching\Storage, por ejemplo:
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir');
// conexión a base de datos
$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password');
// gestiona el reflejo de la estructura de la base de datos
$structure = new Database\Structure($connection, $storage);
// define reglas para la asignación de nombres de tablas, columnas y claves externas
$conventions = new Database\Conventions\DiscoveredConventions($structure);
$explorer = new Database\Explorer($connection, $structure, $conventions, $storage);
versión: 4.0