Database Explorer

Explorer ofrece una forma intuitiva y eficiente de trabajar con la base de datos. Gestiona automáticamente las relaciones entre tablas y optimiza las consultas, lo que le permite concentrarse en la lógica de su aplicación. Funciona de inmediato, sin configuración. Si necesita control total sobre las consultas SQL, puede usar el enfoque SQL.

  • trabajar con los datos es natural y fácil de entender
  • genera consultas SQL optimizadas que obtienen solo los datos necesarios
  • da acceso sencillo a los datos relacionados sin necesidad de escribir consultas JOIN
  • funciona de inmediato, sin ninguna configuración ni generación de entidades

El trabajo con Explorer empieza llamando al método table() sobre el objeto Nette\Database\Explorer (véase Conexión y configuración para los detalles sobre cómo configurar la conexión a la base de datos):

$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 se le pueden encadenar más métodos para filtrar y ordenar los resultados. La consulta se monta y se ejecuta solo cuando se piden los datos, por ejemplo al recorrerla con foreach. Cada fila está representada por un objeto ActiveRow:

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

Explorer simplifica enormemente el trabajo con las relaciones entre tablas. El siguiente ejemplo muestra con qué facilidad podemos mostrar datos de tablas relacionadas (libros y sus autores). Fíjese en que no hace falta escribir ninguna consulta 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 con la tabla 'author'
}

Nette Database Explorer optimiza las consultas para lograr la máxima eficiencia. El ejemplo anterior ejecuta solo dos consultas SELECT, independientemente de si procesamos 10 o 10 000 libros.

Además, Explorer lleva la cuenta de qué columnas se usan en el código y obtiene de la base de datos solo esas, lo que ahorra aún más rendimiento. Este comportamiento es completamente automático y adaptativo. Si más tarde modifica el código para usar más columnas, Explorer ajusta las consultas automáticamente. No tiene que configurar nada ni pensar en qué columnas hará falta: déjeselo a Nette.

Filtrado y ordenación

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

where($condition, ...$params) Añade una condición WHERE. Varias condiciones se combinan con AND
whereOr(array $conditions) Añade un grupo de condiciones WHERE combinadas con OR
wherePrimary($value) Añade una condición WHERE sobre la clave primaria
order($columns, ...$params) Establece la ordenación con ORDER BY
select($columns, ...$params) Indica qué columnas obtener
limit($limit, $offset = null) Limita el número de filas (LIMIT) y opcionalmente fija el OFFSET
page($page, $itemsPerPage, &$numOfPages = 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 se pueden encadenar (la llamada interfaz fluida): $table->where(...)->order(...)->limit(...).

En estos métodos también puede usar las notaciones especiales para acceder a los datos de tablas relacionadas.

Escapado e identificadores

Los métodos escapan automáticamente los parámetros y entrecomillan los identificadores (nombres de tablas y columnas), lo que evita la inyección SQL. Para que funcione correctamente hay que seguir unas pocas reglas:

  • Escriba las palabras clave, los nombres de funciones, de procedimientos, etc. en mayúsculas.
  • Escriba los nombres de columnas y tablas en minúsculas.
  • Pase siempre las cadenas mediante 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%'); // BIEN: el valor se pasa como parámetro

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

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

Filtra los resultados con condiciones WHERE. Su fuerza está en tratar de forma inteligente los distintos tipos de valores y elegir automáticamente los operadores SQL adecuados.

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 del operador adecuado no tiene que ocuparse de los distintos casos especiales: Nette los resuelve por usted:

$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 puede usar el marcador ? sin operador:
$table->where('id ?', 1);        // WHERE `id` = 1

El método trata correctamente las condiciones negativas y los arrays vacíos:

$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) -- lo encuentra todo
// $table->where('NOT id ?', $ids); // ATENCIÓN: esta sintaxis no está soportada

Como parámetro también puede pasar el resultado de otra consulta a una tabla, con lo que se crea 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 se pueden pasar como array, cuyos elementos se combinan con AND:

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

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

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

En el array puede combinar expresiones SQL con marcadores y varios 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], // los dos parámetros se pasan como array
]);

Varias llamadas a where() combinan las condiciones automáticamente con AND.

whereOr (array $parameters)static

Parecido a where(), añade condiciones, pero las combina con OR:

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

Aquí también se pueden 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 sobre 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 (p. ej. foo_id, bar_id), pásela como 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

Indica el orden en el que se devuelven las filas. Puede ordenar por una o varias columnas, de forma ascendente o descendente, o según una expresión propia:

$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

Indica las columnas que se devolverán de la base de datos. De forma predeterminada, Nette Database Explorer devuelve solo las columnas que realmente se usan en el código. Use el método select() cuando necesite obtener expresiones concretas:

// 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 después accesibles 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 fijar un desplazamiento:

$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 adecuado usar el método page().

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

Facilita 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 en la que se guardará el número total de páginas:

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

group (string $columns, …$parameters)static

Agrupa las filas según las columnas indicadas (GROUP BY). Se usa normalmente junto 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 las filas agrupadas (HAVING). Se puede usar junto con el método group() y funciones de agregación:

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

Leer los datos

Para leer datos de la base de datos hay disponibles 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 sola fila por su clave primaria
$row = $table->fetch() Devuelve la fila actual y avanza 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 array
count($table) Devuelve el número de filas del objeto Selection

El objeto ActiveRow es de solo lectura. Eso significa que no puede cambiar los valores de sus propiedades. Esta restricción asegura la consistencia de los datos y evita efectos secundarios inesperados. Los datos se cargan de la base de datos y cualquier cambio debe hacerse de forma explícita y controlada.

foreach: recorrer todas las filas

La forma más fácil de ejecutar una consulta y obtener las filas es recorrerla con 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 con la clave primaria dada, 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 la fila actual y avanza el puntero interno a la siguiente. Si ya 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 array asociativo. El primer argumento indica el nombre de la columna que se usará como clave del array y el segundo, el de la columna que se usará como valor:

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

Si solo se indica el primer parámetro, el valor será la fila entera, 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 usa el valor de la última fila. Al usar null como clave, el array se indexa numéricamente empezando por cero (entonces no se producen colisiones):

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

fetchPairs (Closure $callback)array

Alternativamente puede pasar como parámetro un callback que devuelva para cada fila un único valor o un par clave-valor.

$titles = $explorer->table('book')
	->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// ['First Book (John Novak)', ...]

// El callback también puede devolver un array con un par clave y valor:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['First Book' => 'John Novak', ...]

fetchAll(): array

Devuelve todas las filas como array asociativo de objetos ActiveRow, donde las claves son los valores de la clave primaria.

$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 del objeto Selection:

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

Nota: count() con un parámetro ejecuta la función de agregación COUNT en la base de datos, véase más abajo.

ActiveRow::toArray(): array

Convierte el objeto ActiveRow en un array asociativo en el que las claves son los nombres de las columnas y los valores, los datos correspondientes.

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

Agregación

La clase Selection ofrece métodos para ejecutar 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. El método sirve para averiguar cuántas filas cumplen una determinada condición:

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

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

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

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

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

sum (string $expr): mixed

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

// 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 ejecutar cualquier función de agregación.

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

// une 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 son a su vez el resultado de alguna función de agregación y de una agrupación (p. ej. SUM(value) sobre filas agrupadas), indicamos como segundo argumento la función de agregación que debe aplicarse a esos resultados intermedios:

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

En este ejemplo calculamos primero el precio total de los productos de cada categoría (SUM(price * stock) AS category_total) y agrupamos los resultados por category_id. Después usamos aggregation('SUM(category_total)', 'SUM') para sumar esos totales intermedios category_total. El segundo argumento 'SUM' indica que a los resultados intermedios debe aplicárseles la función SUM.

Insert, Update y Delete

Nette Database Explorer simplifica insertar, actualizar y borrar datos. Todos los métodos mencionados lanzan una Nette\Database\DriverException en caso de error.

Selection::insert (iterable $data)

Inserta registros nuevos en la tabla.

Insertar un solo registro:

Pase el registro nuevo como array asociativo u objeto iterable (como el ArrayHash que se usa en los formularios), donde las claves corresponden a los nombres de las columnas de la tabla.

Si la tabla tiene definida una clave primaria, el método devuelve un objeto ActiveRow, que se recarga de la base de datos para reflejar los cambios hechos a nivel de base de datos (triggers, valores por defecto de las columnas, cálculo de las columnas autoincrementales). Eso asegura la consistencia de los datos y hace que el objeto contenga siempre los datos actuales de la base de datos. Si la tabla no tiene clave primaria, no hay ninguna fila identificable y el método devuelve null.

$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,
// incluidos el ID generado automáticamente y los cambios hechos por los triggers
echo $row->id; // Imprime el ID del usuario recién insertado
echo $row->created_at; // Imprime la hora de creación si la estableció un trigger

Insertar varios registros a la vez:

El método insert() permite insertar varios registros con una sola consulta SQL. En ese 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

Como parámetro también se puede pasar 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);

Insertar valores especiales:

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

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // se convierte al formato de la 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 de la tabla según el filtro indicado. Devuelve el número de filas realmente modificadas.

Pase las columnas que hay que cambiar como array asociativo u objeto iterable (como el ArrayHash que se usa en los formularios), donde las claves corresponden a 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 cambiar valores numéricos puede usar los operadores += y -=:

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

Selection::delete(): int

Borra las filas de la tabla según el filtro indicado. Devuelve el número de filas borradas.

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

Al llamar a update() o delete(), no olvide usar where() para indicar las filas que hay que modificar o borrar. ¡Si no se usa where(), la operación se realizará sobre toda la tabla!

ActiveRow::update (iterable $data)bool

Actualiza los datos de la fila de la base de datos representada por el objeto ActiveRow. Acepta un iterable con los datos que hay que actualizar (las claves son los nombres de las columnas). Para cambiar valores numéricos puede usar los operadores += y -=:

Tras realizar la actualización, el ActiveRow se recarga automáticamente de la base de datos para reflejar los cambios hechos a nivel de base de datos (p. ej. triggers). El método devuelve true solo si se produjo un cambio real de datos.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // incrementa el contador de visitas
]);
echo $article->views; // Imprime el contador de visitas actual

Este método actualiza solo una fila concreta de la base de datos. Para actualizaciones masivas de varias filas use el método Selection::update().

ActiveRow::delete(): int

Borra de la base de datos la fila representada por el objeto ActiveRow. Devuelve el número de filas borradas, que debería ser 1.

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

Este método borra solo una fila concreta de la base de datos. Para borrados masivos de varias filas use el método Selection::delete().

Relaciones entre tablas

En las bases de datos relacionales, los datos se reparten en varias tablas y se enlazan entre sí mediante claves foráneas. Nette Database Explorer ofrece una forma revolucionaria de trabajar con esas relaciones: sin escribir consultas JOIN y sin necesidad de configurar ni generar nada.

Para ilustrar el trabajo con las relaciones usaremos una base de datos de libros de ejemplo (la encontrará en GitHub). En la base de datos tenemos las 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 usada en los ejemplos

En nuestra base de datos de libros de ejemplo encontramos varios tipos de relaciones (aunque el modelo está simplificado respecto a 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 traductor; un traductor puede traducir varios libros.
  • Cero a uno (0:1): un libro puede tener una continuación.
  • Muchos a muchos (M:N): un libro puede tener varias etiquetas y una etiqueta se puede asignar a varios libros.

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

Si necesitamos listar los libros junto con los nombres de sus autores, tenemos dos opciones. O bien obtener 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 bien obtener los datos en dos pasos, primero los libros y después sus autores, y luego juntarlos en PHP:

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

El segundo enfoque es en realidad más eficiente, aunque pueda sorprender. Los datos se obtienen una sola vez y se pueden aprovechar mejor en la caché. Así es precisamente como funciona Nette Database Explorer: lo hace todo por debajo y le ofrece una API elegante:

$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;
}

Acceder a la tabla padre

Acceder a la tabla padre es sencillo. Son relaciones del tipo un libro tiene un autorun libro puede tener traductor. El registro relacionado se obtiene mediante una propiedad del objeto ActiveRow cuyo nombre corresponde al nombre de la columna de la clave foránea sin el sufijo _id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // encuentra el autor según la columna author_id
echo $book->translator?->name; // encuentra el traductor según la columna translator_id

Al acceder a la propiedad $book->author, Explorer busca en la tabla book una columna cuyo nombre contenga la cadena author (es decir, author_id). A partir del valor de esa columna carga el registro correspondiente de la tabla author y lo devuelve como ActiveRow. De forma parecida, $book->translator usa la columna translator_id. Como la columna translator_id puede contener null, en el código usamos el operador nullsafe ?->.

Un enfoque alternativo lo 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 por propiedad, por ejemplo porque la tabla contiene una columna con ese mismo nombre (es decir, author). En los demás casos se recomienda usar el acceso por propiedad, por su mejor legibilidad.

Explorer optimiza automáticamente las consultas a la base de datos. Cuando recorremos los 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 una consulta SELECT por cada tipo de relación, lo que reduce notablemente 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 ejecuta solo estas tres consultas rapidísimas a la base de datos:

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

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

Acceder a la tabla hija

Acceder a la tabla hija funciona en sentido contrario. Ahora preguntamos qué libros escribió este autorqué libros tradujo este traductor. Para este tipo de consulta usamos el método related(), que devuelve un Selection con los 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 "Wrote: $book->title";
}

// Imprime 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 unión como un solo argumento con notación de punto 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 unión correcta a partir del nombre de la tabla padre. En este caso une por la columna book.author_id, porque el nombre de la tabla de origen es author:

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

Si existen varias uniones posibles, Explorer lanzará una AmbiguousReferenceKeyException.

Naturalmente, podemos usar el método related() al recorrer varios registros en un bucle, y Explorer optimizará también en ese caso las consultas automáticamente:

$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 solo dos consultas SQL rapidísimas:

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

Relación muchos a muchos

Para una relación muchos a muchos (M:N) hace falta una tabla de unión (en nuestro caso, book_tag) que contiene dos columnas de clave foránea (book_id, tag_id). Cada una de esas columnas se refiere a la clave primaria de una de las tablas enlazadas. Para obtener los datos relacionados obtenemos primero los registros de la tabla de unión con related('book_tag') y después pasamos a 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 al revés: imprime los nombres de los libros marcados 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 en una 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

Consultar a través de tablas relacionadas

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

La notación con punto (tabla_padre.columna) se usa para las relaciones 1:N desde la perspectiva de la tabla hija:

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

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

// Ordena los libros por el nombre del autor de forma 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 con dos puntos (:tabla_hija.columna) se usa para las relaciones 1:N desde la perspectiva de la tabla padre:

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

// Encuentra los autores que escribieron 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 la notación de dos puntos (:book.title) no se indica la columna de la clave foránea. Explorer detecta automáticamente la columna correcta a partir del nombre de la tabla padre. En este caso une por la columna book.author_id, porque el nombre de la tabla de origen es author. Si existen varias uniones posibles, Explorer lanzará una AmbiguousReferenceKeyException.

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

// Encuentra los 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 datos de varias tablas:

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

Ampliar las condiciones del JOIN

El método joinWhere() amplía las condiciones indicadas al unir tablas en SQL tras la palabra clave ON.

Digamos que queremos encontrar los libros traducidos por un traductor concreto:

// Encuentra los libros traducidos por un 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 de joinWhere() puede usar las mismas construcciones que en el método where(): operadores, marcadores, arrays de valores o expresiones SQL.

Para consultas más complejas con varios JOIN puede definir alias de tablas:

$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)

Fíjese en que, mientras 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 al unir las tablas.

versión: 4.x