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 < 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 (coloaneleid
,name
,web
,born
)book
– cărți (coloaneleid
,author_id
,translator_id
,title
,sequel_id
)tag
– etichete (coloaneleid
,name
)book_tag
– tabel de legături între cărți și etichete (coloanelebook_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
Interogarea prin tabele conexe
Î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 < ?', 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);