Explorator de baze de date

Cu Nette Database, puteți lucra în două moduri: fie lăsați ca interogările SQL să fie generate automat (abordarea Explorer), fie le scrieți singur(acces direct). Explorer simplifică semnificativ accesul la date. Acesta gestionează relațiile dintre tabele, astfel încât să vă puteți concentra pe logica aplicației dumneavoastră.

  • Lucrul cu datele este natural și ușor de înțeles
  • Generează interogări SQL optimizate care extrag numai datele necesare
  • Oferă acces ușor la date conexe fără a fi nevoie să scrieți interogări JOIN
  • Funcționează imediat, fără nicio configurare sau generare de entități

Lucrul cu Explorer începe prin apelarea metodei table() pe obiectul Nette\Database\Explorer (pentru informații despre crearea conexiunilor și configurare, consultați pagina dedicată):

$books = $explorer->table('book'); // "book" este numele tabelului

Metoda returnează un obiect Selection, care reprezintă o interogare SQL. Metode suplimentare pot fi înlănțuite la acest obiect pentru filtrarea și sortarea rezultatelor. Interogarea este asamblată și executată numai atunci când datele sunt solicitate, de exemplu, prin iterarea cu foreach. Fiecare rând este reprezentat de un obiect ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // iese coloana "title" (titlu)
	echo $book->author_id;    // iese coloana "author_id
}

Explorer simplifică foarte mult lucrul cu relațiile dintre tabele. Următorul exemplu arată cât de ușor putem extrage date din tabelele legate (cărți și autorii acestora). Observați că nu trebuie să scrieți nicio interogare JOIN; Nette le generează pentru noi:

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

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // creează un JOIN la tabelul "autor".
}

Nette Database Explorer optimizează interogările pentru eficiență maximă. Exemplul de mai sus efectuează doar două interogări SELECT, indiferent dacă procesăm 10 sau 10.000 de cărți.

În plus, Explorer urmărește ce coloane sunt utilizate în cod și le extrage din baza de date numai pe acelea, economisind și mai multă performanță. Acest comportament este complet automat și adaptabil. Dacă modificați ulterior codul pentru a utiliza coloane suplimentare, Explorer ajustează automat interogările. Nu trebuie să configurați nimic sau să vă gândiți la coloanele care vor fi necesare – lăsați asta pe seama Nette.

Filtrare și sortare

Clasa Selection oferă metode pentru filtrarea și sortarea datelor.

where($condition, ...$params) Adaugă o condiție WHERE. Condițiile multiple sunt combinate folosind AND
whereOr(array $conditions) Adaugă un grup de condiții WHERE combinate folosind OR
wherePrimary($value) Adaugă o condiție WHERE bazată pe cheia primară
order($columns, ...$params) Setează sortarea cu ORDER BY
select($columns, ...$params) Specifică ce coloane se vor extrage
limit($limit, $offset = null) Limitează numărul de rânduri (LIMIT) și stabilește opțional OFFSET
page($page, $itemsPerPage, &$total = null) Setează paginarea
group($columns, ...$params) Grupează rândurile (GROUP BY)
having($condition, ...$params) Adaugă o condiție HAVING pentru filtrarea rândurilor grupate

Metodele pot fi înlănțuite (așa-numita interfață fluentă): $table->where(...)->order(...)->limit(...).

Aceste metode permit, de asemenea, utilizarea de notații speciale pentru accesarea datelor din tabele conexe.

Escaping și identificatori

Metodele scapă automat parametrii și identificatorii de citat (nume de tabele și coloane), împiedicând injectarea SQL. Pentru a asigura funcționarea corectă, trebuie respectate câteva reguli:

  • Scrieți cuvintele-cheie, numele funcțiilor, procedurilor etc. cu cu majuscule.
  • Scrieți numele coloanelor și tabelelor cu cifre mici.
  • Transmiteți întotdeauna șiruri de caractere folosind parametri.
where('name = ' . $name);         // **DISASTER**: vulnerabil la injectarea SQL
where('name LIKE "%search%"');    // **WRONG**: complică citarea automată
where('name LIKE ?', '%search%'); // **CORRECT**: valoare transmisă ca parametru

where('name like ?', $name);     // **WRONG**: generează: `name` `like` ?
where('name LIKE ?', $name);     // **CORECT**: generează: `name` LIKE ?
where('LOWER(name) = ?', $value);// **CORECT**: LOWER(`nume`) = ?

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

Filtrează rezultatele utilizând condițiile WHERE. Punctul său forte constă în gestionarea inteligentă a diferitelor tipuri de valori și în selectarea automată a operatorilor SQL.

Utilizare de bază:

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

Datorită detectării automate a operatorilor potriviți, nu trebuie să gestionați cazuri speciale – Nette le gestionează pentru dvs:

$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)
// Placeholder-ul ? poate fi utilizat fără operator:
$table->where('id ?', 1);        // WHERE `id` = 1

De asemenea, metoda gestionează corect condițiile negative și matricele goale:

$table->where('id', []);         // WHERE `id` IS NULL AND FALSE -- nu găsește nimic
$table->where('id NOT', []);     // WHERE `id` IS NULL OR TRUE -- găsește totul
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- găsește totul
// $table->where('NOT id ?', $ids); // AVERTISMENT: Această sintaxă nu este acceptată

De asemenea, puteți trece rezultatul unei alte interogări de tabel ca parametru, creând o subinterogare:

// 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'));

Condițiile pot fi, de asemenea, transmise sub formă de array, elementele fiind combinate folosind AND:

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

În matrice, pot fi utilizate perechi cheie-valoare, iar Nette va alege din nou automat operatorii corecți:

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

De asemenea, putem amesteca expresii SQL cu marcaje și parametri multipli. Acest lucru este util pentru condiții complexe cu operatori definiți cu precizie:

// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // doi parametri sunt trecuți ca un array
]);

Apelurile multiple la where() combină automat condițiile folosind AND.

whereOr (array $parameters)static

Similar cu where(), dar combină condițiile folosind OR:

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

Pot fi utilizate și expresii mai complexe:

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

wherePrimary (mixed $key)static

Adaugă o condiție pentru cheia primară a tabelului:

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

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

Dacă tabelul are o cheie primară compusă (de exemplu, foo_id, bar_id), o transmitem sub formă de 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

Specifică ordinea în care sunt returnate rândurile. Puteți sorta după una sau mai multe coloane, în ordine crescătoare sau descrescătoare, sau după o expresie personalizată:

$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

Specifică coloanele care urmează să fie returnate din baza de date. În mod implicit, Nette Database Explorer returnează numai coloanele care sunt utilizate efectiv în cod. Utilizați metoda select() atunci când trebuie să recuperați expresii specifice:

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

Aliasurile definite folosind AS sunt apoi accesibile ca proprietăți ale obiectului ActiveRow:

foreach ($table as $row) {
	echo $row->formatted_date;   // accesați aliasul
}

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

Limitează numărul de rânduri returnate (LIMIT) și, opțional, stabilește un offset:

$table->limit(10);        // LIMIT 10 (returnează primele 10 rânduri)
$table->limit(10, 20);    // LIMIT 10 OFFSET 20

Pentru paginare, este mai adecvat să se utilizeze metoda page().

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

Simplifică paginarea rezultatelor. Acceptă numărul paginii (începând de la 1) și numărul de elemente pe pagină. Opțional, puteți trece o referință la o variabilă în care va fi stocat numărul total de pagini:

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

group (string $columns, …$parameters)static

Grupează rândurile după coloanele specificate (GROUP BY). Se utilizează de obicei în combinație cu funcții de agregare:

// Numără numărul de produse din fiecare categorie
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Stabilește o condiție pentru filtrarea rândurilor grupate (HAVING). Aceasta poate fi utilizată în combinație cu metoda group() și cu funcțiile de agregare:

// Găsește categorii cu mai mult de 100 de produse
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Citirea datelor

Pentru citirea datelor din baza de date, sunt disponibile mai multe metode utile:

foreach ($table as $key => $row) Iternează prin toate rândurile, $key este valoarea cheii primare, $row este un obiect ActiveRow
$row = $table->get($key) Returnează un singur rând după cheia primară
$row = $table->fetch() Returnează rândul curent și avansează pointerul la următorul rând
$array = $table->fetchPairs() Creează un array asociativ din rezultate
$array = $table->fetchAll() Returnează toate rândurile ca un array
count($table) Returnează numărul de rânduri din obiectul Selection

Obiectul ActiveRow este numai pentru citire. Aceasta înseamnă că nu puteți modifica valorile proprietăților sale. Această restricție asigură coerența datelor și previne efectele secundare neașteptate. Datele sunt preluate din baza de date, iar orice modificare trebuie efectuată explicit și într-un mod controlat.

foreach – Iterarea prin toate rândurile

Cel mai simplu mod de a executa o interogare și de a extrage rânduri este prin iterarea cu bucla foreach. Aceasta execută automat interogarea SQL.

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

get ($key): ?ActiveRow

Execută o interogare SQL și returnează un rând după cheia sa primară sau null dacă nu există.

$book = $explorer->table('book')->get(123); // returnează ActiveRow cu ID 123 sau nul
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Returnează un rând și avansează pointerul intern la următorul rând. Dacă nu mai există alte rânduri, se returnează null.

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

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

Returnează rezultatele sub forma unui array asociativ. Primul argument specifică numele coloanei care urmează să fie utilizată ca cheie a matricei, iar al doilea argument specifică numele coloanei care urmează să fie utilizată ca valoare:

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

Dacă este furnizat doar primul parametru, întregul rând va fi utilizat ca valoare, reprezentat ca un obiect ActiveRow:

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

Dacă null este furnizat ca cheie, matricea va fi indexată numeric începând de la zero:

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

fetchPairs (Closure $callback)array

Alternativ, puteți trece un callback ca parametru. Callback-ul va fi aplicat fiecărui rând pentru a returna fie o singură valoare, fie o pereche cheie-valoare.

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

// Callback-ul poate returna și un array care conține o pereche cheie-valoare:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Prima carte' => 'Jan Novak', ...]

fetchAll(): array

Returnează toate rândurile ca o matrice asociativă de obiecte ActiveRow, unde cheile sunt valorile cheii primare.

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

count(): int

Metoda count() fără parametri returnează numărul de rânduri din obiectul Selection:

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

Notă: count() cu un parametru efectuează funcția de agregare COUNT în baza de date, astfel cum este descris mai jos.

ActiveRow::toArray(): array

Convertește obiectul ActiveRow într-un array asociativ în care cheile sunt numele coloanelor, iar valorile sunt datele corespunzătoare.

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

Agregare

Clasa Selection oferă metode pentru realizarea cu ușurință a funcțiilor de agregare (COUNT, SUM, MIN, MAX, AVG, etc.).

count($expr) Numără numărul de rânduri
min($expr) Returnează valoarea minimă dintr-o coloană
max($expr) Returnează valoarea maximă a unei coloane
sum($expr) Returnează suma valorilor dintr-o coloană
aggregation($function) Permite orice funcție de agregare, cum ar fi AVG() sau GROUP_CONCAT()

count (string $expr): int

Execută o interogare SQL cu funcția COUNT și returnează rezultatul. Această metodă este utilizată pentru a determina câte rânduri corespund unei anumite condiții:

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

Notă: count() fără un parametru returnează pur și simplu numărul de rânduri din obiectul Selection.

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

Metodele min() și max() returnează valorile minimă și maximă din coloana sau expresia specificată:

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

sum (string $expr): int

Returnează suma valorilor din coloana sau expresia specificată:

// SELECT SUM(`preț` * `articole_în_stock`) FROM `produse` WHERE `activ` = 1
$totalPrice = $products->where('active', true)
	->sum('price * items_in_stock');

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

Permite executarea oricărei funcții de agregare.

// Calculează prețul mediu al produselor dintr-o categorie
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// Combină etichetele produselor într-un singur șir
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Dacă trebuie să agregăm rezultate care sunt ele însele rezultatul unei agregări și grupări (de exemplu, SUM(value) peste rânduri grupate), specificăm funcția de agregare care urmează să fie aplicată acestor rezultate intermediare ca al doilea argument:

// Calculează prețul total al produselor din stoc pentru fiecare categorie, apoi însumează aceste prețuri
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
	->group('category_id')
	->aggregation('SUM(category_total)', 'SUM');

În acest exemplu, calculăm mai întâi prețul total al produselor din fiecare categorie (SUM(price * stock) AS category_total) și grupăm rezultatele prin category_id. Utilizăm apoi aggregation('SUM(category_total)', 'SUM') pentru a însuma aceste subtotaluri. Al doilea argument 'SUM' specifică funcția de agregare care se aplică rezultatelor intermediare.

Inserare, actualizare și ștergere

Nette Database Explorer simplifică inserarea, actualizarea și ștergerea datelor. Toate metodele menționate aruncă un Nette\Database\DriverException în cazul unei erori.

Selection::insert (iterable $data)static

Introduce înregistrări noi într-un tabel.

Inserarea unei singure înregistrări:

Noua înregistrare este transmisă sub forma unui array asociativ sau a unui obiect iterabil (cum ar fi ArrayHash utilizat în formulare), unde cheile corespund numelor coloanelor din tabel.

Dacă tabelul are o cheie primară definită, metoda returnează un obiect ActiveRow, care este reîncărcat din baza de date pentru a reflecta orice modificări efectuate la nivelul bazei de date (de exemplu, declanșatoare, valori implicite ale coloanelor sau calcule de autoincrementare). Acest lucru asigură coerența datelor, iar obiectul conține întotdeauna datele actuale din baza de date. Dacă nu este definită în mod explicit o cheie primară, metoda returnează datele de intrare sub forma unui array.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row este o instanță de ActiveRow care conține datele complete ale rândului inserat,
// inclusiv ID-ul autogenerat și orice modificări efectuate de declanșatoare
echo $row->id;          // Afișează ID-ul utilizatorului nou introdus
echo $row->created_at;  // Afișează ora creării, dacă a fost stabilită de un declanșator

Inserarea simultană a mai multor înregistrări:

Metoda insert() vă permite să introduceți mai multe înregistrări cu o singură interogare SQL. În acest caz, se returnează numărul de rânduri introduse.

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

De asemenea, puteți trece ca parametru un obiect Selection cu o selecție de date.

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

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

Inserarea de valori speciale:

Valorile pot include fișiere, obiecte DateTime sau literale SQL:

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // convertește în formatul bazei de date
	'avatar' => fopen('image.jpg', 'rb'),   // inserează conținutul binar al fișierului
	'uuid' => $explorer::literal('UUID()'), // apelează la funcția UUID()
]);

Selection::update (iterable $data)int

Actualizează rândurile dintr-un tabel pe baza unui filtru specificat. Returnează numărul de rânduri efectiv modificate.

Coloanele care urmează să fie actualizate sunt transmise ca un array asociativ sau un obiect iterabil (cum ar fi ArrayHash utilizat în formulare), unde cheile corespund numelor coloanelor din tabel:

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

Pentru a modifica valorile numerice, puteți utiliza operatorii += și -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // crește valoarea coloanei "puncte" cu 1
		'coins-=' => 1,   // scade valoarea coloanei "monede" cu 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Șterge rânduri dintr-un tabel pe baza unui filtru specificat. Returnează numărul de rânduri șterse.

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

Atunci când apelați update() sau delete(), asigurați-vă că utilizați where() pentru a specifica rândurile care urmează să fie actualizate sau șterse. Dacă nu se utilizează where(), operațiunea va fi efectuată pe întregul tabel!

ActiveRow::update (iterable $data)bool

Actualizează datele dintr-un rând din baza de date reprezentat de obiectul ActiveRow. Acesta acceptă date iterabile ca parametru, unde cheile sunt nume de coloane. Pentru a modifica valorile numerice, puteți utiliza operatorii += și -=:

După efectuarea actualizării, ActiveRow este reîncărcat automat din baza de date pentru a reflecta orice modificări efectuate la nivelul bazei de date (de exemplu, declanșatoare). Metoda returnează true numai dacă a avut loc o modificare reală a datelor.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // incrementează numărul de vizualizări
]);
echo $article->views; // Afișează numărul curent de vizualizări

Această metodă actualizează doar un anumit rând din baza de date. Pentru actualizări masive ale mai multor rânduri, utilizați metoda Selection::update().

ActiveRow::delete()

Șterge un rând din baza de date care este reprezentat de obiectul ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // Șterge cartea cu ID 1

Această metodă șterge numai un anumit rând din baza de date. Pentru ștergerea în bloc a mai multor rânduri, utilizați metoda Selection::delete().

Relații între tabele

În bazele de date relaționale, datele sunt împărțite în mai multe tabele și conectate prin chei străine. Nette Database Explorer oferă un mod revoluționar de a lucra cu aceste relații – fără a scrie interogări JOIN sau a necesita vreo configurare sau generare de entități.

Pentru demonstrație, vom utiliza example database(disponibilă pe GitHub). Baza de date include următoarele tabele:

  • author – autori și traducători (coloanele id, name, web, born)
  • book – cărți (coloanele id, author_id, translator_id, title, sequel_id)
  • tag – etichete (coloanele id, name)
  • book_tag – tabel de legături între cărți și etichete (coloanele book_id, tag_id)

Structura bazei de date

În acest exemplu de bază de date cu cărți, găsim mai multe tipuri de relații (simplificate față de realitate):

  • One-to-many (1:N) – Fiecare carte are un autor; un autor poate scrie mai multe cărți.
  • Zero-to-many (0:N) – O carte poate avea un traducător; un traducător poate traduce multiple cărți.
  • Zero-to-one (0:1) – O carte poate avea o continuare.
  • Many-to-many (M:N) – O carte poate avea mai multe etichete, iar o etichetă poate fi atribuită la mai multe cărți.

În aceste relații, există întotdeauna un tabel parent și un tabel child. De exemplu, în relația dintre autori și cărți, tabelul author este părintele, iar tabelul book este copilul – vă puteți gândi la aceasta ca la o carte care “aparține” întotdeauna unui autor. Acest lucru se reflectă și în structura bazei de date: tabelul copil book conține cheia externă author_id, care face trimitere la tabelul părinte author.

Dacă dorim să afișăm cărțile împreună cu numele autorilor lor, avem două opțiuni. Fie recuperăm datele utilizând o singură interogare SQL cu un JOIN:

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

Fie recuperăm datele în două etape – mai întâi cărțile, apoi autorii acestora – și le asamblăm în PHP:

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

A doua abordare este, surprinzător, mai eficientă. Datele sunt preluate o singură dată și pot fi utilizate mai bine în cache. Acesta este exact modul în care funcționează Nette Database Explorer – gestionează totul sub capotă și vă oferă un API curat:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'title: ' . $book->title;
	echo 'written by: ' . $book->author->name; // $book->author este o înregistrare din tabelul "author
	echo 'translated by: ' . $book->translator?->name;
}

Accesarea tabelului părinte

Accesarea tabelului părinte este simplă. Acestea sunt relații precum o carte are un autor sau o carte poate avea un traducător. Înregistrarea conexă poate fi accesată prin intermediul proprietății obiectului ActiveRow – numele proprietății corespunde numelui coloanei cheii străine fără sufixul id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // găsește autorul prin intermediul coloanei "author_id
echo $book->translator?->name; // găsește traducătorul prin coloana "translator_id".

Atunci când accesează proprietatea $book->author, Explorer caută o coloană în tabelul book care conține șirul author (de exemplu, author_id). Pe baza valorii din această coloană, Explorer extrage înregistrarea corespunzătoare din tabelul author și o returnează ca obiect ActiveRow. În mod similar, $book->translator utilizează coloana translator_id. Deoarece coloana translator_id poate conține null, se utilizează operatorul ?->.

O abordare alternativă este oferită de metoda ref(), care acceptă două argumente – numele tabelului țintă și coloana de legătură – și returnează o instanță ActiveRow sau null:

echo $book->ref('author', 'author_id')->name;      // link către autor
echo $book->ref('author', 'translator_id')->name;  // link către traducător

Metoda ref() este utilă în cazul în care accesul bazat pe proprietăți nu poate fi utilizat, de exemplu, atunci când tabelul conține o coloană cu același nume ca și proprietatea (author). În alte cazuri, este recomandată utilizarea accesului bazat pe proprietăți pentru o mai bună lizibilitate.

Explorer optimizează automat interogările în baza de date. Atunci când iterați prin cărți și accesați înregistrările aferente acestora (autori, traducători), Explorer nu generează o interogare pentru fiecare carte în parte. În schimb, acesta execută doar o singură interogare SELECT pentru fiecare tip de relație, reducând semnificativ încărcarea bazei de date. De exemplu:

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

Acest cod va executa doar trei interogări optimizate ale bazei de date:

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

Logica pentru identificarea coloanei de legătură este definită de implementarea Conventions. Vă recomandăm să utilizați DiscoveredConventions, care analizează cheile străine și vă permite să lucrați fără probleme cu relațiile existente între tabele.

Accesarea tabelului copil

Accesarea tabelului copilului funcționează în direcția opusă. Acum întrebăm ce cărți a scris acest autor sau ce cărți a tradus acest traducător. Pentru acest tip de interogare, folosim metoda related(), care returnează un obiect Selection cu înregistrările aferente. Iată un exemplu:

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

// Afișează toate cărțile scrise de autor
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Afișează toate cărțile traduse de autor
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

Metoda related() acceptă descrierea relației ca un singur argument folosind notația punct sau ca două argumente separate:

$author->related('book.translator_id');    // un singur argument
$author->related('book', 'translator_id'); // două argumente

Explorer poate detecta automat coloana de legătură corectă pe baza numelui tabelului părinte. În acest caz, se face legătura prin coloana book.author_id deoarece numele tabelului sursă este author:

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

Dacă există mai multe conexiuni posibile, Explorer va arunca o excepție AmbiguousReferenceKeyException.

Putem, desigur, să folosim metoda related() și atunci când iterăm prin mai multe înregistrări într-o buclă, iar Explorer va optimiza automat interogările și în acest caz:

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

Acest cod generează doar două interogări SQL eficiente:

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

Relația Many-to-Many

Pentru o relație mulți-la-mulți (M:N), este necesar un ** tabel de joncțiune** (în cazul nostru, book_tag). Acest tabel conține două coloane de chei străine (book_id, tag_id). Fiecare coloană face trimitere la cheia primară a unuia dintre tabelele conectate. Pentru a extrage datele legate, mai întâi extragem înregistrările din tabelul de legătură utilizând related('book_tag'), apoi continuăm cu datele țintă:

$book = $explorer->table('book')->get(1);
// Scoate numele etichetelor atribuite cărții
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name; // extrage numele etichetei prin intermediul tabelului de legături
}

$tag = $explorer->table('tag')->get(1);
// Direcția opusă: afișează titlurile cărților cu această etichetă
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // extrage titlul cărții
}

Explorer optimizează din nou interogările SQL într-o formă eficientă:

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

În metodele where(), select(), order(), și group(), puteți utiliza notații speciale pentru a accesa coloane din alte tabele. Explorer creează automat JOIN-urile necesare.

Notația Dot (parent_table.column) este utilizată pentru relațiile 1:N, văzute din perspectiva tabelului părinte:

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

// Găsește cărți ale căror nume de autori încep cu "Jon
$books->where('author.name LIKE ?', 'Jon%');

// Sortează cărțile după numele autorului descrescător
$books->order('author.name DESC');

// Scoate titlul cărții și numele autorului
$books->select('book.title, author.name');

Notația coloanelor se utilizează pentru relațiile 1:N din perspectiva tabelului părinte:

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

// Găsește autorii care au scris o carte cu "PHP" în titlu
$authors->where(':book.title LIKE ?', '%PHP%');

// Numără numărul de cărți pentru fiecare autor
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

În exemplul de mai sus cu notația două puncte (:book.title), coloana cheie externă nu este specificată explicit. Explorer detectează automat coloana corectă pe baza numelui tabelului părinte. În acest caz, se conectează prin coloana book.author_id deoarece numele tabelului sursă este author. Dacă există mai multe conexiuni posibile, Explorer aruncă excepția AmbiguousReferenceKeyException.

Coloana de legătură poate fi specificată explicit în paranteze:

// Găsește autori care au tradus o carte cu "PHP" în titlu
$authors->where(':book(translator).title LIKE ?', '%PHP%');

Notațiile pot fi înlănțuite pentru a accesa date din mai multe tabele:

// Găsește autori de cărți etichetate cu "PHP
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Extinderea condițiilor pentru JOIN

Metoda joinWhere() adaugă condiții suplimentare la îmbinările de tabele în SQL după cuvântul-cheie ON.

De exemplu, să spunem că dorim să găsim cărți traduse de un anumit traducător:

// Găsește cărți traduse de un traducător numit "David
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David')

În condiția joinWhere(), puteți utiliza aceleași construcții ca și în metoda where() – operatori, marcatori de poziție, matrici de valori sau expresii SQL.

Pentru interogări mai complexe cu JOIN-uri multiple, se pot defini aliasuri de tabel:

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

Rețineți că, în timp ce metoda where() adaugă condiții la clauza WHERE, metoda joinWhere() extinde condițiile din clauza ON în timpul îmbinărilor de tabele.

Crearea manuală a exploratorului

Dacă nu utilizați containerul Nette DI, puteți crea manual o instanță a Nette\Database\Explorer:

use Nette\Database;

// $storage implementează Nette\Caching\Storage, de ex:
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir');
// conexiune la baza de date
$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password');
// gestionează reflectarea structurii bazei de date
$structure = new Database\Structure($connection, $storage);
// definește reguli pentru maparea numelor de tabele, coloanelor și cheilor străine
$conventions = new Database\Conventions\DiscoveredConventions($structure);
$explorer = new Database\Explorer($connection, $structure, $conventions, $storage);
versiune: 4.0