Nette Documentation Preview

syntax
Database Explorer
*****************
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Database Explorer

Explorer oferă o modalitate intuitivă și eficientă de a lucra cu baza de date. Se ocupă automat de legăturile dintre tabele și de optimizarea interogărilor, astfel încât să vă puteți concentra pe aplicația dvs. Funcționează imediat fără configurare. Dacă aveți nevoie de control total asupra interogărilor SQL, puteți utiliza abordarea SQL.

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

Cu Explorer începeți prin apelarea metodei table() a obiectului Nette\Database\Explorer (detalii despre conectare găsiți în capitolul Conectare și configurare):

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

Metoda returnează obiectul Selection, care reprezintă o interogare SQL. Pe acest obiect putem înlănțui alte metode pentru filtrarea și sortarea rezultatelor. Interogarea se construiește și se execută abia în momentul în care începem să solicităm date. De exemplu, prin parcurgerea cu ciclul foreach. Fiecare rând este reprezentat de obiectul ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // afișarea coloanei 'title'
	echo $book->author_id;    // afișarea coloanei 'author_id'
}

Explorer facilitează în mod fundamental lucrul cu legăturile dintre tabele. Următorul exemplu arată cât de ușor putem afișa date din tabele legate (cărți și autorii lor). Observați că nu trebuie să scriem nicio interogare JOIN, Nette le creează pentru noi:

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

foreach ($books as $book) {
	echo 'Carte: ' . $book->title;
	echo 'Autor: ' . $book->author->name; // creează JOIN pe tabela 'author'
}

Nette Database Explorer optimizează interogările pentru a fi cât mai eficiente. Exemplul de mai sus execută 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 încarcă din baza de date doar acelea, economisind astfel performanță suplimentară. Acest comportament este complet automat și adaptiv. Dacă modificați ulterior codul și începeți să utilizați alte coloane, Explorer ajustează automat interogările. Nu trebuie să setați nimic, nici să vă gândiți ce coloane veți avea nevoie – lăsați asta pe seama Nette.

Filtrare și sortare

Clasa Selection oferă metode pentru filtrarea și sortarea selecției de date.

where($condition, ...$params) Adaugă condiția WHERE. Mai multe condiții sunt legate cu operatorul AND
whereOr(array $conditions) Adaugă un grup de condiții WHERE legate cu operatorul OR
wherePrimary($value) Adaugă condiția WHERE după cheia primară
order($columns, ...$params) Setează sortarea ORDER BY
select($columns, ...$params) Specifică coloanele care trebuie încărcate
limit($limit, $offset = null) Limitează numărul de rânduri (LIMIT) și opțional setează OFFSET
page($page, $itemsPerPage, &$total = null) Setează paginarea
group($columns, ...$params) Grupează rândurile (GROUP BY)
having($condition, ...$params) Adaugă condiția HAVING pentru filtrarea rândurilor grupate

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

În aceste metode puteți utiliza și notația specială pentru accesarea datelor din tabelele conexe.

Escapare și identificatori

Metodele escapează automat parametrii și încadrează identificatorii (numele tabelelor și coloanelor) în ghilimele, prevenind astfel SQL injection. Pentru funcționarea corectă este necesar să respectați câteva reguli:

  • Cuvintele cheie, numele funcțiilor, procedurilor etc. scrieți-le cu majuscule.
  • Numele coloanelor și tabelelor scrieți-le cu litere mici.
  • Șirurile de caractere introduceți-le întotdeauna prin parametri.
where('name = ' . $name);         // VULNERABILITATE CRITICĂ: SQL injection
where('name LIKE "%search%"');    // GREȘIT: complică încadrarea automată în ghilimele
where('name LIKE ?', '%search%'); // CORECT: valoare introdusă prin parametru

where('name like ?', $name);     // GREȘIT: generează: `name` `like` ?
where('name LIKE ?', $name);     // CORECT: generează: `name` LIKE ?
where('LOWER(name) = ?', $value);// CORECT: LOWER(`name`) = ?

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

Filtrează rezultatele folosind condiții WHERE. Punctul său forte este lucrul inteligent cu diferite tipuri de valori și alegerea 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ă ne ocupăm de diverse cazuri speciale. Nette le rezolvă pentru noi:

$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)
// se poate utiliza și semnul de întrebare substituent fără operator:
$table->where('id ?', 1);        // WHERE `id` = 1

Metoda procesează corect și condițiile negative și array-urile 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 tot
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- găsește tot
// $table->where('NOT id ?', $ids);  Atenție - această sintaxă nu este suportată

Ca parametru putem transmite și rezultatul dintr-o altă tabelă – se va crea 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 le putem transmite și ca array, ale cărui elemente se vor uni cu AND:

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

În array putem folosi perechi cheie ⇒ valoare și Nette alege din nou automat operatorii corecți:

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

În array putem combina expresii SQL cu semne de întrebare substituente și mai mulți parametri. Acest lucru este potrivit pentru condiții complexe cu operatori definiți precis:

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

Apelurile multiple ale where() leagă automat condițiile cu AND.

whereOr(array $parameters)static

Similar cu where(), adaugă condiții, dar cu diferența că le leagă cu OR:

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

Și aici putem folosi expresii mai complexe:

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

wherePrimary(mixed $key)static

Adaugă condiția pentru cheia primară a tabelei:

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

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

Dacă tabela are o cheie primară compozită (de ex. foo_id, bar_id), o transmitem ca 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

Determină ordinea în care vor fi returnate rândurile. Putem sorta după una sau mai multe coloane, în ordine descrescătoare sau crescătoare, sau după o expresie proprie:

$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 trebuie returnate din baza de date. În mod implicit, Nette Database Explorer returnează doar acele coloane care sunt utilizate efectiv în cod. Metoda select() o folosim deci în cazurile în care avem nevoie să returnăm 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 cu AS sunt apoi disponibile ca proprietăți ale obiectului ActiveRow:

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

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

Limitează numărul de rânduri returnate (LIMIT) și opțional permite setarea unui offset:

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

Pentru paginare este mai potrivită utilizarea metodei page().

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

Facilitează paginarea rezultatelor. Acceptă numărul paginii (numărat de la 1) și numărul de elemente pe pagină. Opțional, se poate transmite o referință la o variabilă în care se va stoca numărul total de pagini:

$numOfPages = null;
$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages);
echo "Total pagini: $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:

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

having(string $having, …$parameters)static

Setează condiția pentru filtrarea rândurilor grupate (HAVING). Poate fi utilizată în combinație cu metoda group() și funcții de agregare:

// Găsește categoriile care au 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 avem la dispoziție câteva metode utile:

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

Obiectul ActiveRow este destinat doar citirii. Acest lucru înseamnă că nu se pot modifica valorile proprietăților sale. Această limitare asigură consistența datelor și previne efectele secundare neașteptate. Datele sunt încărcate din baza de date și orice modificare ar trebui efectuată explicit și controlat.

foreach – iterare peste toate rândurile

Cel mai simplu mod de a executa o interogare și de a obține rândurile este iterarea într-un ciclu foreach. Lansează automat interogarea SQL.

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

get($key): ?ActiveRow

Execută interogarea SQL și returnează rândul după cheia primară, sau null, dacă nu există.

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

fetch(): ?ActiveRow

Returnează rândul și mută pointerul intern la următorul. Dacă nu mai există alte rânduri, 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 ca array asociativ. Primul argument specifică numele coloanei care se va utiliza ca cheie în array, al doilea argument specifică numele coloanei care se va utiliza ca valoare:

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

Dacă specificăm doar primul parametru, valoarea va fi întregul rând, adică obiectul ActiveRow:

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

În cazul cheilor duplicate, se va utiliza valoarea din ultimul rând. La utilizarea null ca cheie, array-ul va fi indexat numeric de la zero (atunci nu apar coliziuni):

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

fetchPairs(Closure $callback)array

Alternativ, puteți specifica ca parametru un callback, care va returna pentru fiecare rând fie valoarea însăși, fie perechea cheie-valoare.

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

// Callback-ul poate returna și un array cu perechea cheie & valoare:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Prima carte' => 'Jan Novák', ...]

fetchAll(): array

Returnează toate rândurile ca array asociativ de obiecte ActiveRow, unde cheile sunt valorile cheilor primare.

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

count(): int

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

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

Atenție, count() cu parametru execută funcția de agregare COUNT în baza de date.

ActiveRow::toArray(): array

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

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

Agregace

Clasa Selection oferă metode pentru executarea ușoară 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ă din coloană
max($expr) Returnează valoarea maximă din coloană
sum($expr) Returnează suma valorilor din coloană
aggregation($function) Permite executarea oricărei funcții de agregare. De ex. AVG()GROUP_CONCAT()

count(string $expr): int

Execută interogarea SQL cu funcția COUNT și returnează rezultatul. Metoda se utilizează pentru a afla 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`

Atenție, count() fără parametru returnează doar numărul de rânduri din obiectul Selection.

min(string $expr) și max(string $expr)

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

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

sum(string $expr)

Returnează suma valorilor din coloana sau expresia specificată:

// 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 executarea oricărei funcții de agregare.

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

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

Dacă avem nevoie să agregăm rezultate care deja provin dintr-o funcție de agregare și grupare (de ex. SUM(valoare) peste rândurile grupate), ca al doilea argument specificăm funcția de agregare care trebuie aplicată acestor rezultate intermediare:

// Calculează prețul total al produselor din stoc pentru fiecare categorie și apoi adună 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, mai întâi calculăm prețul total al produselor din fiecare categorie (SUM(price * stock) AS category_total) și grupăm rezultatele după category_id. Apoi folosim aggregation('SUM(category_total)', 'SUM') pentru a aduna aceste sume intermediare category_total. Al doilea argument 'SUM' spune că funcția SUM trebuie aplicată rezultatelor intermediare.

Insert, Update & Delete

Nette Database Explorer simplifică inserarea, actualizarea și ștergerea datelor. Toate metodele menționate aruncă excepția Nette\Database\DriverException în caz de eroare.

Selection::insert(iterable $data)

Inserează înregistrări noi în tabelă.

Inserarea unei singure înregistrări:

Transmitem noua înregistrare ca array asociativ sau obiect iterabil (de exemplu, ArrayHash utilizat în formulare), unde cheile corespund numelor coloanelor din tabelă.

Dacă tabela are o cheie primară definită, metoda returnează un obiect ActiveRow, care este reîncărcat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (triggere, valori implicite ale coloanelor, calcule ale coloanelor auto-increment). Astfel se asigură consistența datelor și obiectul conține întotdeauna datele actuale din baza de date. Dacă nu are o cheie primară unică, returnează datele transmise sub formă de array.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row este o instanță ActiveRow și conține datele complete ale rândului inserat,
// inclusiv ID-ul generat automat și eventualele modificări efectuate de triggere
echo $row->id; // Afișează ID-ul utilizatorului nou inserat
echo $row->created_at; // Afișează timpul creării, dacă este setat de un trigger

Inserarea mai multor înregistrări deodată:

Metoda insert() permite inserarea mai multor înregistrări printr-o singură interogare SQL. În acest caz, returnează numărul de rânduri inserate.

$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

Ca parametru se poate transmite și un obiect Selection cu selecția de date.

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

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

Inserarea valorilor speciale:

Ca valori putem transmite și fișiere, obiecte DateTime sau literali SQL:

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

Selection::update(iterable $data)int

Actualizează rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri efectiv modificate.

Coloanele modificate le transmitem ca array asociativ sau obiect iterabil (de exemplu, 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 modificarea valorilor numerice putem folosi operatorii += și -=:

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

Selection::delete(): int

Șterge rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri șterse.

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

La apelarea update() și delete(), nu uitați să specificați rândurile care trebuie modificate/șterse folosind where(). Dacă nu utilizați where(), operația se va efectua pe întreaga tabelă!

ActiveRow::update(iterable $data)bool

Actualizează datele din rândul bazei de date reprezentat de obiectul ActiveRow. Ca parametru acceptă un iterabil cu datele care trebuie actualizate (cheile sunt numele coloanelor). Pentru modificarea valorilor numerice putem folosi operatorii += și -=:

După efectuarea actualizării, ActiveRow se reîncarcă automat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (de ex. triggere). Metoda returnează true doar dacă a avut loc o modificare efectivă a datelor.

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

Această metodă actualizează doar un singur rând specific din baza de date. Pentru actualizarea în masă a mai multor rânduri, utilizați metoda Selection::update().

ActiveRow::delete()

Șterge rândul 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 doar un singur rând specific din baza de date. Pentru ștergerea în masă 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 interconectate prin chei străine. Nette Database Explorer aduce o modalitate revoluționară de a lucra cu aceste legături – fără a scrie interogări JOIN și fără a fi nevoie să configurați sau să generați ceva.

Pentru a ilustra lucrul cu legăturile, vom folosi exemplul bazei de date de cărți (îl găsiți pe GitHub). În baza de date avem tabelele:

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

Structura bazei de date folosită în exemple

În exemplul nostru de bază de date de cărți găsim mai multe tipuri de relații (deși modelul este simplificat față de realitate):

  • One-to-many 1:N – fiecare carte are un autor, autorul poate scrie mai multe cărți
  • Zero-to-many 0:N – cartea poate avea un traducător, traducătorul poate traduce mai multe cărți
  • Zero-to-one 0:1 – cartea poate avea o continuare
  • Many-to-many M:N – cartea poate avea mai multe etichete și o etichetă poate fi atribuită mai multor cărți

În aceste relații există întotdeauna o tabelă părinte și una copil. De exemplu, în relația dintre autor și carte, tabela author este părinte și book este copil – ne putem imagina că o carte „aparține“ întotdeauna unui autor. Acest lucru se reflectă și în structura bazei de date: tabela copil book conține cheia străină author_id, care face referire la tabela părinte author.

Dacă avem nevoie să afișăm cărțile inclusiv numele autorilor lor, avem două opțiuni. Fie obținem datele printr-o singură interogare SQL folosind JOIN:

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

Fie încărcăm datele în doi pași – mai întâi cărțile și apoi autorii lor – și apoi le asamblăm în PHP:

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3);  -- id-urile autorilor cărților obținute

A doua abordare este de fapt mai eficientă, deși poate fi surprinzător. Datele sunt încărcate o singură dată și pot fi utilizate mai bine în cache. Exact în acest mod lucrează Nette Database Explorer – rezolvă totul sub capotă și vă oferă o API elegantă:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'titlu: ' . $book->title;
	echo 'scris de: ' . $book->author->name; // $book->author este înregistrarea din tabela 'author'
	echo 'tradus de: ' . $book->translator?->name;
}

Accesul la tabela părinte

Accesul la tabela părinte este direct. Este vorba despre relații precum cartea are un autor sau cartea poate avea un traducător. Obținem înregistrarea asociată prin proprietatea obiectului ActiveRow – numele său corespunde numelui coloanei cu cheia străină fără _id:

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

Când accesăm proprietatea $book->author, Explorer caută în tabela book o coloană al cărei nume conține șirul author (adică author_id). După valoarea din această coloană, încarcă înregistrarea corespunzătoare din tabela author și o returnează ca ActiveRow. Similar funcționează și $book->translator, care utilizează coloana translator_id. Deoarece coloana translator_id poate conține null, folosim în cod operatorul ?->.

O cale alternativă o oferă metoda ref(), care acceptă doi argumente, numele tabelei țintă și numele coloanei de legătură, și returnează o instanță ActiveRow sau null:

echo $book->ref('author', 'author_id')->name;      // legătura cu autorul
echo $book->ref('author', 'translator_id')->name;  // legătura cu traducătorul

Metoda ref() este utilă dacă nu se poate utiliza accesul prin proprietate, deoarece tabela conține o coloană cu același nume (adică author). În celelalte cazuri, se recomandă utilizarea accesului prin proprietate, care este mai lizibil.

Explorer optimizează automat interogările bazei de date. Când parcurgem cărțile într-un ciclu și accesăm înregistrările lor asociate (autori, traducători), Explorer nu generează o interogare pentru fiecare carte în parte. În schimb, execută doar un singur SELECT pentru fiecare tip de legătură, reducând semnificativ sarcina 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 apelează doar aceste trei interogări fulgerătoare în baza de date:

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id din coloana author_id a cărților selectate
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- id din coloana translator_id a cărților selectate

Logica de căutare a coloanei de legătură este dată de implementarea Conventions. Recomandăm utilizarea DiscoveredConventions, care analizează cheile străine și permite lucrul simplu cu relațiile existente între tabele.

Accesul la tabela copil

Accesul la tabela copil funcționează în direcția opusă. Acum întrebăm ce cărți a scris acest autor sau a tradus acest traducător. Pentru acest tip de interogare folosim metoda related(), care returnează Selection cu înregistrările asociate. Să vedem un exemplu:

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

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

// Afișează toate cărțile pe care autorul le-a tradus
foreach ($author->related('book.translator_id') as $book) {
	echo "A tradus: $book->title";
}

Metoda related() acceptă descrierea legăturii ca un singur argument cu notație cu punct sau ca doi argumente separate:

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

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

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

Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția AmbiguousReferenceKeyException.

Metoda related() o putem folosi, desigur, și la parcurgerea mai multor înregistrări într-un ciclu și Explorer optimizează automat interogările și în acest caz:

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

Acest cod generează doar două interogări SQL fulgerătoare:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id-urile autorilor selectați

Legătura Many-to-many

Pentru legătura many-to-many (M:N) este necesară existența unei tabele de legătură (în cazul nostru book_tag), care conține două coloane cu chei străine (book_id, tag_id). Fiecare dintre aceste coloane face referire la cheia primară a uneia dintre tabelele legate. Pentru a obține datele asociate, mai întâi obținem înregistrările din tabela de legătură folosind related('book_tag') și apoi continuăm către datele țintă:

$book = $explorer->table('book')->get(1);
// afișează numele etichetelor atribuite cărții
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name;  // afișează numele etichetei prin tabela de legătură
}

$tag = $explorer->table('tag')->get(1);
// sau invers: afișează numele cărților etichetate cu această etichetă
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // afișează numele 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, ...));  -- id-urile cărților selectate
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- id-urile etichetelor găsite în book_tag

Interogarea prin tabele asociate

În metodele where(), select(), order() și group() putem folosi notații speciale pentru accesarea coloanelor din alte tabele. Explorer creează automat JOIN-urile necesare.

Notația cu punct (tabela_parinte.coloana) se utilizează pentru relația 1:N din perspectiva tabelei copil:

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

// Găsește cărțile al căror autor are numele începând cu 'Jon'
$books->where('author.name LIKE ?', 'Jon%');

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

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

Notația cu două puncte (:tabela_copil.coloana) se utilizează pentru relația 1:N din perspectiva tabelei 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%');

// Calculează 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 cu două puncte (:book.title), nu este specificată coloana cu cheia străină. Explorer detectează automat coloana corectă pe baza numelui tabelei părinte. În acest caz, se leagă prin coloana book.author_id, deoarece numele tabelei sursă este author. Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția AmbiguousReferenceKeyException.

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

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

Notațiile pot fi înlănțuite pentru accesul prin mai multe tabele:

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

Extinderea condițiilor pentru JOIN

Metoda joinWhere() extinde condițiile care se specifică la legarea tabelelor în SQL după cuvântul cheie ON.

Să presupunem că dorim să găsim cărțile traduse de un anumit traducător:

// Găsește cărțile traduse de traducătorul 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() putem folosi aceleași construcții ca în metoda where() – operatori, semne de întrebare substituente, array-uri de valori sau expresii SQL.

Pentru interogări mai complexe cu mai multe JOIN-uri, putem defini aliasuri pentru tabele:

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

Observați că, în timp ce metoda where() adaugă condiții în clauza WHERE, metoda joinWhere() extinde condițiile în clauza ON la legarea tabelelor.