Database Explorer

Explorer ofrece una forma intuitiva y eficiente de trabajar con la base de datos. Se encarga automáticamente de las relaciones entre tablas y la optimización de consultas, para que puedas concentrarte en tu aplicación. Funciona inmediatamente sin configuración. Si necesitas control total sobre las consultas SQL, puedes utilizar el acceso SQL.

  • Trabajar con datos es natural y fácil de entender.
  • Genera consultas SQL optimizadas que cargan solo los datos necesarios.
  • Permite un fácil acceso a los datos relacionados sin necesidad de escribir consultas JOIN.
  • Funciona instantáneamente sin ninguna configuración o generación de entidades.

Empiezas con Explorer llamando al método table() del objeto Nette\Database\Explorer (los detalles de la conexión se pueden encontrar en el capítulo Conexión y configuración):

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

El método devuelve un objeto Selection, que representa una consulta SQL. A este objeto podemos encadenar otros métodos para filtrar y ordenar los resultados. La consulta se construye y ejecuta solo cuando empezamos a solicitar datos. Por ejemplo, iterando con un bucle foreach. Cada fila está representada por un objeto ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // Salida de la columna 'title'
	echo $book->author_id;    // Salida de la columna 'author_id'
}

Explorer facilita fundamentalmente el trabajo con relaciones entre tablas. El siguiente ejemplo muestra lo fácil que es mostrar datos de tablas relacionadas (libros y sus autores). Ten en cuenta que no necesitamos escribir ninguna consulta JOIN, Nette las crea por nosotros:

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

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

Nette Database Explorer optimiza las consultas para que sean lo más eficientes posible. El ejemplo anterior ejecuta solo dos consultas SELECT, independientemente de si procesamos 10 o 10,000 libros.

Además, Explorer rastrea qué columnas se utilizan en el código y carga solo esas desde la base de datos, ahorrando así rendimiento adicional. Este comportamiento es completamente automático y adaptativo. Si más tarde modificas el código y comienzas a usar columnas adicionales, Explorer ajustará automáticamente las consultas. No necesitas configurar nada, ni pensar qué columnas necesitarás – déjaselo a Nette.

Filtrado y ordenación

La clase Selection proporciona métodos para filtrar y ordenar la selección de datos.

where($condition, ...$params) Añade una condición WHERE. Múltiples condiciones se unen con el operador AND
whereOr(array $conditions) Añade un grupo de condiciones WHERE unidas por el operador OR
wherePrimary($value) Añade una condición WHERE por clave primaria
order($columns, ...$params) Establece el orden ORDER BY
select($columns, ...$params) Especifica las columnas que se deben cargar
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 filas agrupadas

Los métodos se pueden encadenar (la llamada interfaz fluida): $table->where(...)->order(...)->limit(...).

En estos métodos también puedes usar notación especial para acceder a datos de tablas relacionadas.

Escape e identificadores

Los métodos escapan automáticamente los parámetros y entrecomillan los identificadores (nombres de tablas y columnas), previniendo así la inyección SQL. Para un funcionamiento correcto, es necesario seguir algunas reglas:

  • Escribe las palabras clave, nombres de funciones, procedimientos, etc., en MAYÚSCULAS.
  • Escribe los nombres de columnas y tablas en minúsculas.
  • Siempre inserta cadenas a través de parámetros.
where('name = ' . $name);         // VULNERABILIDAD CRÍTICA: Inyección SQL
where('name LIKE "%search%"');    // MAL: complica el entrecomillado automático
where('name LIKE ?', '%search%'); // CORRECTO: valor insertado mediante parámetro

where('name like ?', $name);     // MAL: genera: `name` `like` ?
where('name LIKE ?', $name);     // CORRECTO: genera: `name` LIKE ?
where('LOWER(name) = ?', $value);// CORRECTO: LOWER(`name`) = ?

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

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

Uso básico:

$table->where('id', $value);     // WHERE `id` = 123
$table->where('id > ?', $value); // WHERE `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 apropiados, no tenemos que lidiar con varios casos especiales. Nette los resuelve por nosotros:

$table->where('id', 1);          // WHERE `id` = 1
$table->where('id', null);       // WHERE `id` IS NULL
$table->where('id', [1, 2, 3]);  // WHERE `id` IN (1, 2, 3)
// También se puede usar un signo de interrogación de marcador de posición sin operador:
$table->where('id ?', 1);        // WHERE `id` = 1

El método también maneja correctamente condiciones negativas y arrays vacíos:

$table->where('id', []);         // WHERE `id` IS NULL AND FALSE -- No encontrará nada
$table->where('id NOT', []);     // WHERE `id` IS NULL OR TRUE -- Encontrará todo
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- Encontrará todo
// $table->where('NOT id ?', $ids);  Atención - esta sintaxis no es compatible

También podemos pasar el resultado de otra tabla como parámetro – se creará 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'));

También podemos pasar las condiciones como un array, cuyos elementos se unirán usando AND:

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

En el array podemos usar pares clave ⇒ valor y Nette elegirá automáticamente los operadores correctos de nuevo:

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

En el array podemos combinar expresiones SQL con marcadores de posición de signo de interrogación y múltiples parámetros. Esto es adecuado para condiciones complejas con operadores definidos con precisión:

// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // Pasamos dos parámetros como un array
]);

Múltiples llamadas a where() unen automáticamente las condiciones usando AND.

whereOr (array $parameters)static

Similar a where(), añade condiciones, pero con la diferencia de que las une usando OR:

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

Aquí también podemos usar expresiones más complejas:

// WHERE (`price` > 1000) OR (`price_with_tax` > 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:

// WHERE `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 un array:

// 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 devolverán las filas. Podemos ordenar por una o más columnas, en orden descendente o ascendente, o por una expresión personalizada:

$table->order('created');                   // ORDER BY `created`
$table->order('created DESC');              // ORDER BY `created` DESC
$table->order('priority DESC, created');    // ORDER BY `priority` DESC, `created`
$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC

select (string $columns, …$parameters)static

Especifica las columnas que deben devolverse de la base de datos. Por defecto, Nette Database Explorer devuelve solo aquellas columnas que se utilizan realmente en el código. Por lo tanto, usamos el método select() en casos donde necesitamos devolver 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 usando AS están entonces disponibles como propiedades del objeto ActiveRow:

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

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

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

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

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

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

Facilita la paginación de resultados. Acepta el número de página (contando desde 1) y el número de elementos por página. Opcionalmente, se 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 de páginas: $numOfPages";

group (string $columns, …$parameters)static

Agrupa las filas por las columnas especificadas (GROUP BY). Se utiliza generalmente junto con funciones de agregación:

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

having (string $having, …$parameters)static

Establece la condición para filtrar filas agrupadas (HAVING). Se puede usar junto con el método group() y funciones de agregación:

// Encuentra categorías que tienen 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, tenemos varios métodos útiles disponibles:

foreach ($table as $key => $row) Itera sobre todas las filas, $key es el valor de la clave primaria, $row es un objeto ActiveRow
$row = $table->get($key) Devuelve una fila por clave primaria
$row = $table->fetch() Devuelve la fila actual y mueve el puntero a la siguiente
$array = $table->fetchPairs() Crea un array asociativo a partir de los resultados
$array = $table->fetchAll() Devuelve todas las filas como un array
count($table) Devuelve el número de filas en el objeto Selection

El objeto ActiveRow está destinado solo para lectura. Esto significa que no se pueden cambiar los valores de sus propiedades. Esta restricción asegura la consistencia de los datos y previene efectos secundarios inesperados. Los datos se cargan desde la base de datos y cualquier cambio debe realizarse de forma explícita y controlada.

foreach – iteración sobre todas las filas

La forma más fácil de ejecutar una consulta y obtener filas es iterando en un bucle foreach. Ejecuta automáticamente la consulta SQL.

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

get ($key): ?ActiveRow

Ejecuta la consulta SQL y devuelve la fila por 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 mueve el puntero interno a la siguiente. Si no existen 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 un array asociativo. El primer argumento especifica el nombre de la columna que se usará como clave en el array, el segundo argumento especifica el nombre de la columna que se usará como valor:

$authors = $explorer->table('author')->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

Si solo especificamos el primer parámetro, el valor será la fila completa, es decir, el objeto ActiveRow:

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

En caso de claves duplicadas, se utiliza el valor de la última fila. Al usar null como clave, el array se indexará numéricamente desde cero (entonces no ocurren colisiones):

$authors = $explorer->table('author')->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]

fetchPairs (Closure $callback)array

Alternativamente, puedes pasar un callback como parámetro, que devolverá el valor en sí, o un par clave-valor para cada fila.

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

// El callback también puede devolver un array con un par clave & valor:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Primer libro' => 'Jan Novák', ...]

fetchAll(): array

Devuelve todas las filas como un array asociativo de objetos ActiveRow, donde las claves son los valores de las claves primarias.

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

count(): int

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

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

Atención, count() con un parámetro realiza la función de agregación COUNT en la base de datos, ver más abajo.

ActiveRow::toArray(): array

Convierte el objeto ActiveRow en un array asociativo, 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 en la columna
max($expr) Devuelve el valor máximo en la columna
sum($expr) Devuelve la suma de los valores en la columna
aggregation($function) Permite ejecutar cualquier función de agregación. Por ejemplo, AVG()GROUP_CONCAT()

count (string $expr): int

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

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

Atención, count() sin parámetro solo devuelve el número de filas en el objeto Selection.

min (string $expr) y max(string $expr)

Los métodos min() y max() devuelven el valor mínimo y máximo en la columna o expresión especificada:

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

sum (string $expr)

Devuelve la suma de los valores en 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)

Permite ejecutar cualquier función de agregación.

// Precio promedio de los productos en la categoría
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

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

Si necesitamos agregar resultados que ya provienen de alguna función de agregación y agrupación (por ejemplo, SUM(valor) 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 luego 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 en cada categoría (SUM(price * stock) AS category_total) y agrupamos los resultados por category_id. Luego usamos aggregation('SUM(category_total)', 'SUM') para sumar estos subtotales category_total. El segundo argumento 'SUM' indica que la función SUM debe aplicarse 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 lanzarán una excepción Nette\Database\DriverException en caso de error.

Selection::insert (iterable $data)

Inserta nuevos registros en la tabla.

Insertar un solo registro:

Pasamos el nuevo registro como un array asociativo o un objeto iterable (por ejemplo, ArrayHash usado en formularios), donde las claves corresponden a los nombres de las columnas en la tabla.

Si la tabla tiene una clave primaria definida, el método devuelve un objeto ActiveRow, que se recarga desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (disparadores, valores de columna predeterminados, cálculos de columnas autoincrementales). Esto asegura la consistencia de los datos y el objeto siempre contiene los datos actuales de la base de datos. Si no tiene una clave primaria única, devuelve los datos pasados en forma de array.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row es una instancia de ActiveRow y contiene los datos completos de la fila insertada,
// incluyendo el ID generado automáticamente y cualquier cambio realizado por disparadores
echo $row->id; // Imprime el ID del usuario recién insertado
echo $row->created_at; // Imprime la hora de creación si está establecida por un disparador

Insertar múltiples registros a la vez:

El método insert() permite insertar múltiples registros usando 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 se puede pasar un objeto Selection con una selección de datos como parámetro.

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

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

Insertar valores especiales:

También podemos pasar archivos, objetos DateTime o literales SQL como valores:

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

Selection::update (iterable $data)int

Actualiza las filas en la tabla según el filtro especificado. Devuelve el número de filas realmente cambiadas.

Pasamos las columnas a cambiar como un array asociativo o un objeto iterable (por ejemplo, ArrayHash usado en formularios), donde las claves corresponden a los nombres de las columnas en 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 cambiar valores numéricos, podemos usar los operadores += y -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // Incrementa el valor de la columna 'points' en 1
		'coins-=' => 1,   // Decrementa 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 la tabla según el filtro especificado. Devuelve el número de filas eliminadas.

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

Al llamar a update() y delete(), no olvides especificar las filas a modificar/eliminar usando where(). Si no usas where(), ¡la operación se realizará en toda la tabla!

ActiveRow::update (iterable $data)bool

Actualiza los datos en la fila de la base de datos representada por el objeto ActiveRow. Acepta un iterable con los datos a actualizar como parámetro (las claves son los nombres de las columnas). Para cambiar valores numéricos, podemos usar los operadores += y -=:

Después de realizar la actualización, ActiveRow se recarga automáticamente desde la base de datos para reflejar cualquier cambio realizado a nivel de base de datos (por ejemplo, disparadores). El método devuelve true solo si los datos realmente cambiaron.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // Incrementamos el número de vistas
]);
echo $article->views; // Imprime el número actual de vistas

Este método actualiza solo una fila específica en la base de datos. Para la actualización masiva de múltiples filas, usa el método Selection::update().

ActiveRow::delete()

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

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

Este método elimina solo una fila específica en la base de datos. Para la eliminación masiva de múltiples filas, usa el método Selection::delete().

Relaciones entre tablas

En las bases de datos relacionales, los datos se dividen en múltiples tablas y se interconectan mediante claves foráneas. Nette Database Explorer introduce una forma revolucionaria de trabajar con estas relaciones, sin escribir consultas JOIN y sin necesidad de configurar o generar nada.

Para ilustrar el trabajo con relaciones, usaremos un ejemplo de base de datos de libros (puedes encontrarlo en GitHub). En la base de datos tenemos tablas:

  • author – escritores 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 unión entre libros y etiquetas (columnas book_id, tag_id)
Estructura de la base de datos utilizada en los ejemplos ***

En nuestro ejemplo de base de datos de libros, encontramos varios tipos de relaciones (aunque el modelo está simplificado en comparación con la realidad):

  • Uno a muchos (1:N) – cada libro tiene un autor, un autor puede escribir varios libros.
  • Cero a muchos (0:N) – un libro puede tener un traductor, un traductor puede traducir varios libros.
  • Cero a uno (0:1) – un libro puede tener una secuela.
  • 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 hija. Por ejemplo, en la relación entre autor y libro, la tabla author es la padre y book es la hija – podemos imaginarlo como que un libro siempre “pertenece” a algún 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 necesitamos listar libros incluyendo los nombres de sus autores, tenemos dos opciones. O bien obtenemos los datos con una única consulta SQL usando JOIN:

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

O cargamos los datos en dos pasos – primero los libros y luego sus autores – y luego los ensamblamos en PHP:

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3);  -- IDs de los autores de los libros obtenidos

El segundo enfoque es en realidad más eficiente, aunque pueda sorprender. Los datos se cargan solo una vez y pueden utilizarse mejor en la caché. Así es exactamente como funciona Nette Database Explorer: resuelve todo bajo el capó y te ofrece una API elegante:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'título: ' . $book->title;
	echo 'escrito por: ' . $book->author->name; // $book->author es un registro de la tabla 'author'
	echo 'traducido por: ' . $book->translator?->name;
}

Acceso a la tabla padre

El acceso a la tabla padre es directo. Se trata de relaciones como un libro tiene un autorun libro puede tener un traductor. Obtenemos el registro relacionado a través de una propiedad del objeto ActiveRow; su nombre corresponde al nombre de la columna de clave foránea sin _id:

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

Cuando accedemos a la propiedad $book->author, Explorer busca en la tabla book una columna cuyo nombre contenga la cadena author (es decir, author_id). Según el valor en esta columna, carga el registro correspondiente de la tabla author y lo devuelve como ActiveRow. De manera similar funciona $book->translator, que utiliza la columna translator_id. Dado que la columna translator_id puede contener null, usamos el operador ?-> en el código.

Una ruta alternativa la ofrece el método ref(), que acepta dos argumentos, el nombre de la tabla de destino y el nombre de la columna de unión, y devuelve una instancia de ActiveRow o null:

echo $book->ref('author', 'author_id')->name;      // Relación con el autor
echo $book->ref('author', 'translator_id')->name;  // Relación con el traductor

El método ref() es útil si no se puede usar el acceso a través de la propiedad porque la tabla contiene una columna con el mismo nombre (es decir, author). En otros casos, se recomienda usar el acceso a través de la propiedad, que es más legible.

Explorer optimiza automáticamente las consultas a la base de datos. Cuando iteramos sobre libros en un bucle y accedemos a sus registros relacionados (autores, traductores), Explorer no genera una consulta para cada libro por separado. En su lugar, ejecuta solo un 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 solo llama a estas tres consultas rápidas a la base de datos:

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs de la columna author_id de los libros seleccionados
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- IDs de la columna translator_id de los libros seleccionados

La lógica para encontrar la columna de unión viene dada por la implementación de Conventions. Recomendamos usar DiscoveredConventions, que analiza las claves foráneas y permite trabajar fácilmente con las relaciones existentes entre tablas.

Acceso a la tabla hija

El acceso a la tabla hija funciona en la dirección opuesta. Ahora preguntamos qué libros escribió este autortradujo este traductor. Para este tipo de consulta, usamos el método related(), que devuelve una Selection con registros relacionados. Veamos un ejemplo:

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

// Imprime todos los libros del autor
foreach ($author->related('book.author_id') as $book) {
	echo "Escrito por: $book->title";
}

// Imprime todos los libros que el autor tradujo
foreach ($author->related('book.translator_id') as $book) {
	echo "Traducido por: $book->title";
}

El método related() acepta la descripción de la unión como un argumento con notación de puntos o como dos argumentos separados:

$author->related('book.translator_id');  // Un argumento
$author->related('book', 'translator_id');  // Dos argumentos

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

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

Si hubiera múltiples uniones posibles, Explorer lanzaría una excepción AmbiguousReferenceKeyException.

Por supuesto, también podemos usar el método related() al iterar sobre múltiples registros en un bucle, y Explorer también optimiza automáticamente las consultas en este caso:

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

Este código genera solo dos consultas SQL rápidas:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs de los autores seleccionados

Relación Muchos a Muchos

Para una relación muchos a muchos (M:N), se requiere una tabla de unión (en nuestro caso book_tag), que contiene dos columnas con claves foráneas (book_id, tag_id). Cada una de estas columnas hace referencia a la clave primaria de una de las tablas conectadas. Para obtener los datos relacionados, primero obtenemos los registros de la tabla de unión usando related('book_tag') y luego continuamos hacia los datos de destino:

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

$tag = $explorer->table('tag')->get(1);
// O viceversa: imprime los nombres de los libros etiquetados con esta etiqueta
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // Imprime el título del libro
}

Explorer optimiza de nuevo las consultas SQL a una forma eficiente:

SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...));  -- IDs de los libros seleccionados
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- IDs de las etiquetas encontradas en book_tag

Consultas a través de tablas relacionadas

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

La notación de puntos (tabla_padre.columna) se utiliza para la relación 1:N desde la perspectiva de la tabla hija:

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

// Encuentra libros cuyo autor tiene un nombre que empieza por 'Jon'
$books->where('author.name LIKE ?', 'Jon%');

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

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

La notación de dos puntos (:tabla_hija.columna) se utiliza para la relación 1:N desde la perspectiva de la tabla padre:

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

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

// Cuenta el número de libros para 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), no se especifica la columna de clave foránea. Explorer detecta automáticamente la columna correcta basándose en el nombre de la tabla padre. En este caso, la unión se realiza a través de la columna book.author_id, porque el nombre de la tabla de origen es author. Si hubiera múltiples uniones posibles, Explorer lanzaría una excepción AmbiguousReferenceKeyException.

La columna de unión se puede especificar explícitamente entre paréntesis:

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

Las notaciones se pueden encadenar para acceder a través de múltiples tablas:

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

Extensión de condiciones para JOIN

El método joinWhere() extiende las condiciones que se especifican al unir tablas en SQL después de la palabra clave ON.

Supongamos que queremos encontrar libros traducidos por un traductor específico:

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

En la condición joinWhere(), podemos usar las mismas construcciones que en el método where() – operadores, marcadores de posición de signo de interrogación, arrays de valores o expresiones SQL.

Para consultas más complejas con múltiples JOIN, podemos definir alias de tabla:

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born < ?', 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)

Ten en cuenta que mientras el método where() añade condiciones a la cláusula WHERE, el método joinWhere() extiende las condiciones en la cláusula ON al unir tablas.

versión: 4.0