Raziskovalec zbirke podatkov

Z zbirko podatkov Nette lahko delate na dva načina: pustite, da se poizvedbe SQL generirajo samodejno (pristop raziskovalca), ali pa jih napišete sami(neposredni dostop). Raziskovalec bistveno poenostavi dostop do podatkov. Obravnava razmerja med tabelami, tako da se lahko osredotočite na logiko svoje aplikacije.

  • Delo s podatki je naravno in enostavno za razumevanje
  • Ustvarja optimizirane poizvedbe SQL, ki poberejo le potrebne podatke
  • Zagotavlja enostaven dostop do povezanih podatkov, ne da bi bilo treba pisati poizvedbe JOIN
  • Deluje takoj, brez kakršne koli konfiguracije ali ustvarjanja entitet

Delo z Raziskovalcem se začne s klicem metode table() na objektu Nette\Database\Explorer (za informacije o ustvarjanju povezav in konfiguraciji glejte posebno stran):

$books = $explorer->table('book'); // 'book' je ime tabele

Metoda vrne objekt Selection, ki predstavlja poizvedbo SQL. Na ta objekt je mogoče verižno povezati dodatne metode za filtriranje in razvrščanje rezultatov. Poizvedba se sestavi in izvede le, ko so podatki zahtevani, na primer z iteracijo s foreach. Vsaka vrstica je predstavljena z objektom ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // izpisi stolpca 'naslov'
	echo $book->author_id;    // izpiše stolpec 'author_id'.
}

Raziskovalec močno poenostavi delo z razmerji med tabelami. Naslednji primer prikazuje, kako preprosto lahko izpišemo podatke iz povezanih tabel (knjige in njihovi avtorji). Opazite, da ni treba pisati nobenih poizvedb JOIN; Nette jih ustvari namesto nas:

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

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // ustvari povezavo JOIN s tabelo "author".
}

Nette Database Explorer optimizira poizvedbe za največjo učinkovitost. Zgornji primer izvede le dve poizvedbi SELECT, ne glede na to, ali obdelujemo 10 ali 10.000 knjig.

Poleg tega Raziskovalec spremlja, kateri stolpci se uporabljajo v kodi, in iz zbirke podatkov pobere le te, s čimer prihrani še dodatno zmogljivost. To obnašanje je popolnoma samodejno in prilagodljivo. Če pozneje spremenite kodo in uporabite dodatne stolpce, Raziskovalec samodejno prilagodi poizvedbe. Ničesar vam ni treba konfigurirati ali razmišljati o tem, kateri stolpci bodo potrebni – to prepustite Nette.

Filtriranje in razvrščanje

Razred Selection ponuja metode za filtriranje in razvrščanje podatkov.

where($condition, ...$params) Doda pogoj WHERE. Več pogojev se združi z uporabo AND
whereOr(array $conditions) Doda skupino pogojev WHERE, združenih z uporabo OR
wherePrimary($value) Doda pogoj WHERE na podlagi primarnega ključa
order($columns, ...$params) Določi razvrščanje z ORDER BY
select($columns, ...$params) Določi, katere stolpce je treba poiskati
limit($limit, $offset = null) Omeji število vrstic (LIMIT) in po želji določi OFFSET
page($page, $itemsPerPage, &$total = null) Nastavi paginacijo
group($columns, ...$params) Združi vrstice v skupine (GROUP BY)
having($condition, ...$params) Doda pogoj HAVING za filtriranje združenih vrstic

Metode je mogoče verižiti (tako imenovani tekoči vmesnik): $table->where(...)->order(...)->limit(...).

Te metode omogočajo tudi uporabo posebnih zapisov za dostop do podatkov iz povezanih tabel.

Pobegi in identifikatorji

Metode samodejno izločijo parametre in identifikatorje citatov (imena tabel in stolpcev), kar preprečuje vbrizgavanje SQL. Za zagotovitev pravilnega delovanja je treba upoštevati nekaj pravil:

  • ključne besede, imena funkcij, postopkov itd. pišite z nadrobnimi črkami.
  • Imena stolpcev in tabel pišite z malimi črkami**.
  • Nizove vedno prenašajte z uporabo parametrov.
where('name = ' . $name);         // **DISASTER**: ranljiv za vbrizgavanje SQL
where('name LIKE "%search%"');    // **PRAVILNO**: otežuje samodejno citiranje
where('name LIKE ?', '%search%'); // **CORRECT**: vrednost je posredovana kot parameter

where('name like ?', $name);     // **WRONG**: ustvari: ?
where('name LIKE ?', $name);     // **CORRECT**: generira: `name` LIKE ?
where('LOWER(name) = ?', $value);// **CORRECT**: LOWER(`name`) = ?

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

Filtriranje rezultatov z uporabo pogojev WHERE. Njegova prednost je v inteligentnem obravnavanju različnih vrst vrednosti in samodejnem izbiranju operatorjev SQL.

Osnovna uporaba:

$table->where('id', $value);     // Kjer `id` = 123
$table->where('id > ?', $value); // Kjer `id` > 123
$table->where('id = ? OR name = ?', $id, $name); // KJE `id` = 1 ALI `imeno` = 'Jon Snow'

Zahvaljujoč samodejnemu zaznavanju ustreznih operatorjev vam ni treba obravnavati posebnih primerov – Nette jih uredi namesto vas:

$table->where('id', 1);          // Kjer `id` = 1
$table->where('id', null);       // Kjer je `id` NULL
$table->where('id', [1, 2, 3]);  // Kjer `id` IN (1, 2, 3)
// Namestni znak ? se lahko uporablja brez operatorja:
$table->where('id ?', 1);        // Kjer `id` = 1

Metoda pravilno obravnava tudi negativne pogoje in prazne pole:

$table->where('id', []);         // Kjer `id` JE NULL IN FALSE -- ne najde ničesar
$table->where('id NOT', []);     // KER je `id` NIČ ALI PRAVDA -- najde vse
$table->where('NOT (id ?)', []); // KER NE (`id` JE NULL IN FALSE) -- najde vse
// $table->where('NOT id ?', $ids); // OPOZORILO: Ta sintaksa ni podprta

Kot parameter lahko posredujete tudi rezultat druge poizvedbe po tabeli in tako ustvarite podpovpraševanje:

// Kjer `id` IN (SELECT `id` FROM `tableName`)
$table->where('id', $explorer->table($tableName));

// Kjer `id` IN (SELECT `col` FROM `tableName`)
$table->where('id', $explorer->table($tableName)->select('col'));

Pogoje lahko posredujete tudi kot polje, pri čemer elemente združite z uporabo metode AND:

// Kjer (`Cena_končna` < `Cena_prejšnja`) IN (Število zalog` > `Min_zaloge`)
$table->where([
	'price_final &lt; price_original',
	'stock_count > min_stock',
]);

V polju se lahko uporabijo pari ključ-vrednost, Nette pa bo ponovno samodejno izbral pravilne operatorje:

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

Prav tako lahko mešamo izraze SQL z nadomestnimi znaki in več parametri. To je uporabno za zapletene pogoje z natančno določenimi operatorji:

// Kjer (`starost` > 18 let) IN (ROUND(`score`, 2) > 75,5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // dva parametra sta posredovana kot polje
]);

Več klicev na where() samodejno združi pogoje z uporabo AND.

whereOr (array $parameters)static

Podobno kot where(), vendar združuje pogoje z uporabo OR:

// Kjer (`status` = 'aktiven') ALI (`izbrisano` = 1)
$table->whereOr([
	'status' => 'active',
	'deleted' => true,
]);

Uporabljajo se lahko tudi bolj zapleteni izrazi:

// Kjer (`cena` > 1000) ALI (`cena_z_davkom` > 1500)
$table->whereOr([
	'price > ?' => 1000,
	'price_with_tax > ?' => 1500,
]);

wherePrimary (mixed $key)static

Doda pogoj za primarni ključ tabele:

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

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

Če ima tabela sestavljen primarni ključ (npr. foo_id, bar_id), ga posredujemo kot polje:

// Kjer `foo_id` = 1 IN `bar_id` = 5
$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch();

// Kjer (`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

Določa vrstni red vrnjenih vrstic. Razvrstite lahko po enem ali več stolpcih, v naraščajočem ali padajočem vrstnem redu ali z izrazom po meri:

$table->order('created');                   // NAROČI PO `ustvarjenem`
$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

Določa stolpce, ki se vrnejo iz podatkovne zbirke. Privzeto Nette Database Explorer vrne samo stolpce, ki se dejansko uporabljajo v kodi. Metodo select() uporabite, kadar želite pridobiti določene izraze:

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

Vzdevki, določeni z uporabo AS, so nato dostopni kot lastnosti predmeta ActiveRow:

foreach ($table as $row) {
	echo $row->formatted_date;   // dostop do vzdevka
}

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

Omeji število vrnjenih vrstic (LIMIT) in po želji določi odmik:

$table->limit(10);        // LIMIT 10 (vrne prvih 10 vrstic)
$table->limit(10, 20);    // LIMIT 10 OFFSET 20

Za paginacijo je primerneje uporabiti metodo page().

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

Poenostavi paginiranje rezultatov. Sprejme številko strani (od 1 naprej) in število elementov na strani. Po želji lahko posredujete sklic na spremenljivko, v kateri bo shranjeno skupno število strani:

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

group (string $columns, …$parameters)static

Poveže vrstice po določenih stolpcih (GROUP BY). Običajno se uporablja v kombinaciji z združevalnimi funkcijami:

// Šteje število izdelkov v vsaki kategoriji.
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Določi pogoj za filtriranje združenih vrstic (HAVING). Uporablja se lahko v kombinaciji z metodo group() in združevalnimi funkcijami:

// Poišče kategorije z več kot 100 izdelki
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Branje podatkov

Za branje podatkov iz podatkovne zbirke je na voljo več uporabnih metod:

foreach ($table as $key => $row) Iterira skozi vse vrstice, $key je vrednost primarnega ključa, $row je objekt ActiveRow
$row = $table->get($key) Vrne posamezno vrstico po primarnem ključu
$row = $table->fetch() Vrne trenutno vrstico in premakne kazalec na naslednjo
$array = $table->fetchPairs() Iz rezultatov ustvari asociativno polje
$array = $table->fetchAll() Vrne vse vrstice kot polje
count($table) Vrne število vrstic v objektu Izbor

Objekt ActiveRow je namenjen samo branju. To pomeni, da ne morete spreminjati vrednosti njegovih lastnosti. Ta omejitev zagotavlja doslednost podatkov in preprečuje nepričakovane stranske učinke. Podatki se pridobivajo iz podatkovne zbirke, zato je treba vse spremembe izrecno in nadzorovano izvesti.

foreach – Iteriranje skozi vse vrstice

Najlažji način za izvajanje poizvedbe in pridobivanje vrstic je iteracija z zanko foreach. Ta samodejno izvede poizvedbo SQL.

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

get ($key): ?ActiveRow

Izvrši poizvedbo SQL in vrne vrstico po njenem primarnem ključu ali null, če ta ne obstaja.

$book = $explorer->table('book')->get(123); // vrne vrstico ActiveRow z ID 123 ali null
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Vrne eno vrstico in premakne notranji kazalec na naslednjo. Če ni več vrstic, vrne null.

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

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

Rezultate vrne kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ polja, drugi argument pa ime stolpca, ki se uporabi kot vrednost:

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

Če je podan samo prvi parameter, se kot vrednost uporabi celotna vrstica, ki je predstavljena kot objekt ActiveRow:

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

Če je kot ključ naveden null, se polje številčno indeksira od nič:

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

fetchPairs (Closure $callback)array

Kot parameter lahko posredujete tudi povratni klic. Povratni klic se uporabi za vsako vrstico, da se vrne ena vrednost ali par ključ-vrednost.

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

// Povratni klic lahko vrne tudi polje, ki vsebuje par ključ-vrednost:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['First Book' => 'Jan Novak', ...]

fetchAll(): array

Vrne vse vrstice kot asociativno polje objektov ActiveRow, kjer so ključi vrednosti primarnih ključev.

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

count(): int

Metoda count() brez parametrov vrne število vrstic v objektu Selection:

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

Opomba: metoda count() s parametrom izvede funkcijo združevanja COUNT v zbirki podatkov, kot je opisano spodaj.

ActiveRow::toArray(): array

Objekt ActiveRow pretvori v asociativno polje, kjer so ključi imena stolpcev, vrednosti pa ustrezni podatki.

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

Agregacija

Razred Selection ponuja metode za enostavno izvajanje funkcij združevanja (COUNT, SUM, MIN, MAX, AVG itd.).

count($expr) Šteje število vrstic
min($expr) Vrne najmanjšo vrednost v stolpcu
max($expr) Vrne največjo vrednost v stolpcu
sum($expr) Vrne vsoto vrednosti v stolpcu
aggregation($function) Omogoča katero koli funkcijo združevanja, kot sta AVG() ali GROUP_CONCAT()

count (string $expr): int

Izvede poizvedbo SQL s funkcijo COUNT in vrne rezultat. Ta metoda se uporablja za ugotavljanje, koliko vrstic ustreza določenemu pogoju:

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

Opomba: funkcija count() brez parametra preprosto vrne število vrstic v objektu Selection.

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

Metodi min() in max() vrneta najmanjšo in največjo vrednost v navedenem stolpcu ali izrazu:

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

sum (string $expr): int

Vrne vsoto vrednosti v določenem stolpcu ali izrazu:

// SELECT SUM(`cena` * `predmeti na zalogi`) FROM `izdelki` WHERE `active` = 1
$totalPrice = $products->where('active', true)
	->sum('price * items_in_stock');

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

Omogoča izvajanje katere koli funkcije združevanja.

// Izračuna povprečno ceno izdelkov v kategoriji.
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// združuje oznake izdelkov v en sam niz
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Če moramo združiti rezultate, ki so sami rezultat združevanja in grupiranja (npr. SUM(value) nad grupiranimi vrsticami), kot drugi argument navedemo funkcijo združevanja, ki se uporabi za te vmesne rezultate:

// Izračuna skupno ceno izdelkov na zalogi za vsako kategorijo, nato pa te cene sešteje.
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
	->group('category_id')
	->aggregation('SUM(category_total)', 'SUM');

V tem primeru najprej izračunamo skupno ceno izdelkov v vsaki kategoriji (SUM(price * stock) AS category_total) in rezultate združimo po category_id. Nato za seštevanje teh vmesnih vsot uporabimo aggregation('SUM(category_total)', 'SUM'). Drugi argument 'SUM' določa funkcijo združevanja, ki se uporabi za vmesne rezultate.

Vstavljanje, posodabljanje in brisanje

Nette Database Explorer poenostavlja vstavljanje, posodabljanje in brisanje podatkov. Vse omenjene metode v primeru napake zavržejo sporočilo Nette\Database\DriverException.

Selection::insert (iterable $data)static

Vnese nove zapise v tabelo.

Vstavljanje posameznega zapisa:

Nov zapis se posreduje kot asociativno polje ali iterabilni objekt (kot je ArrayHash, ki se uporablja v obrazcih), kjer se ključi ujemajo z imeni stolpcev v tabeli.

Če ima tabela določen primarni ključ, metoda vrne objekt ActiveRow, ki se ponovno naloži iz podatkovne zbirke, da odraža vse spremembe na ravni podatkovne zbirke (npr. sprožilce, privzete vrednosti stolpcev ali izračune samodejnega povečanja). S tem je zagotovljena doslednost podatkov, objekt pa vedno vsebuje trenutne podatke iz zbirke podatkov. Če primarni ključ ni izrecno določen, metoda vrne vhodne podatke kot polje.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row je primerek ActiveRow, ki vsebuje celotne podatke vstavljene vrstice,
// vključno s samodejno ustvarjenim ID in vsemi spremembami, ki so jih naredili sprožilci
echo $row->id;          // izpiše ID novo vstavljenega uporabnika
echo $row->created_at;  // izpiše čas ustvarjanja, če ga je določil sprožilec

Vstavljanje več zapisov naenkrat:

Metoda insert() omogoča vstavljanje več zapisov z eno samo poizvedbo SQL. V tem primeru vrne število vstavljenih vrstic.

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

Kot parameter lahko posredujete tudi objekt Selection z izbranimi podatki.

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

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

Vstavljanje posebnih vrednosti:

Vrednosti lahko vključujejo datoteke, predmete DateTime ali literale SQL:

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // pretvori v obliko podatkovne zbirke
	'avatar' => fopen('image.jpg', 'rb'),   // vstavi vsebino binarne datoteke
	'uuid' => $explorer::literal('UUID()'), // pokliče funkcijo UUID()
]);

Selection::update (iterable $data)int

Posodobi vrstice v tabeli na podlagi določenega filtra. Vrne število dejansko spremenjenih vrstic.

Stolpci, ki jih je treba posodobiti, se posredujejo kot asociativno polje ali iterabilni objekt (kot je ArrayHash, ki se uporablja v obrazcih), kjer se ključi ujemajo z imeni stolpcev v tabeli:

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

Za spreminjanje številskih vrednosti lahko uporabite operatorja += in -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // poveča vrednost stolpca "točke" za 1.
		'coins-=' => 1,   // zmanjša vrednost stolpca "kovanci" za 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Iz tabele izbriše vrstice na podlagi določenega filtra. Vrne število izbrisanih vrstic.

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

Ko kličete update() ali delete(), ne pozabite uporabiti where() za določitev vrstic, ki jih je treba posodobiti ali izbrisati. Če where() ni uporabljen, bo operacija izvedena za celotno tabelo!

ActiveRow::update (iterable $data)bool

Posodobi podatke v vrstici zbirke podatkov, ki jo predstavlja objekt ActiveRow. Kot parameter sprejme iterabilne podatke, pri čemer so ključi imena stolpcev. Za spreminjanje številskih vrednosti lahko uporabite operatorja += in -=:

Po izvedeni posodobitvi se objekt ActiveRow samodejno ponovno naloži iz podatkovne zbirke, da odraža vse spremembe, izvedene na ravni podatkovne zbirke (npr. sprožilci). Metoda vrne true le, če je prišlo do dejanske spremembe podatkov.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // poveča število ogledov
]);
echo $article->views; // izpiše trenutno število ogledov

Ta metoda posodobi samo eno določeno vrstico v zbirki podatkov. Za množične posodobitve več vrstic uporabite metodo Selection::update().

ActiveRow::delete()

Izbriše vrstico iz zbirke podatkov, ki jo predstavlja objekt ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // izbriše knjigo z ID 1

Ta metoda izbriše samo eno določeno vrstico v zbirki podatkov. Za množično brisanje več vrstic uporabite metodo Selection::delete().

Razmerja med tabelami

V relacijskih podatkovnih zbirkah so podatki razdeljeni v več tabel in povezani s tujimi ključi. Nette Database Explorer ponuja revolucionaren način dela s temi razmerji – brez pisanja poizvedb JOIN ali zahteve po konfiguraciji ali ustvarjanju entitet.

Za demonstracijo bomo uporabili podatkovno zbirko primer(na voljo na GitHubu). Podatkovna baza vsebuje naslednje tabele:

  • author – avtorji in prevajalci (stolpci id, name, web, born)
  • book – knjige (stolpci id, author_id, translator_id, title, sequel_id)
  • tag – oznake (stolpci id, name)
  • book_tag – preglednica povezav med knjigami in oznakami (stolpci book_id, tag_id)

Struktura podatkovne zbirke

V tem primeru zbirke podatkov o knjigah najdemo več vrst povezav (poenostavljeno v primerjavi z resničnostjo):

  • Vsaka knjiga ima enega avtorja; avtor lahko napiše več knjig.
  • Zero-to-many (0:N) – Knjiga ima lahko prevajalca; prevajalec lahko prevede več knjig.
  • Zero-to-one (0:1) – Knjiga ima lahko** nadaljevanje.
  • Mnogo-več (M:N) – Knjiga ima lahko več** značk in ena značka je lahko dodeljena več knjigam.

V teh razmerjih vedno obstajata starševska tabela in družinska tabela. Na primer, v razmerju med avtorji in knjigami je tabela author starš, tabela book pa otrok – to si lahko predstavljate tako, da knjiga vedno “pripada” avtorju. To se odraža tudi v strukturi zbirke podatkov: podrejena tabela book vsebuje tuj ključ author_id, ki se sklicuje na nadrejeno tabelo author.

Če želimo prikazati knjige skupaj z imeni njihovih avtorjev, imamo dve možnosti. Podatke lahko pridobimo z eno samo poizvedbo SQL s povezavo JOIN:

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

ali pa podatke pridobimo v dveh korakih – najprej knjige, nato njihove avtorje – in jih sestavimo v PHP:

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

Drugi pristop je presenetljivo učinkovitejši. Podatki se pridobijo samo enkrat in jih je mogoče bolje uporabiti v predpomnilniku. Natanko tako deluje Nette Database Explorer – za vse poskrbi pod pokrovom in vam ponudi čist API:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'title: ' . $book->title;
	echo 'written by: ' . $book->author->name; // $book->author je zapis iz tabele 'author'
	echo 'translated by: ' . $book->translator?->name;
}

Dostop do matične tabele

Dostop do nadrejene tabele je preprost. To so razmerja, kot sta knjiga ima avtorja ali knjiga ima lahko prevajalca. Do povezanega zapisa lahko dostopamo prek lastnosti predmeta ActiveRow – ime lastnosti se ujema z imenom stolpca tujega ključa brez končnice id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // poišče avtorja prek stolpca 'author_id'.
echo $book->translator?->name; // poišče prevajalca prek stolpca "translator_id".

Pri dostopu do lastnosti $book->author Raziskovalec išče stolpec v tabeli book, ki vsebuje niz author (npr. author_id). Na podlagi vrednosti v tem stolpcu pridobi ustrezen zapis iz tabele author in ga vrne kot objekt ActiveRow. Podobno $book->translator uporablja stolpec translator_id. Ker lahko stolpec translator_id vsebuje null, se uporabi operator ?->.

Alternativni pristop ponuja metoda ref(), ki sprejme dva argumenta – ime ciljne tabele in povezovalni stolpec – in vrne primerek ActiveRow ali null:

echo $book->ref('author', 'author_id')->name;      // povezava do avtorja
echo $book->ref('author', 'translator_id')->name;  // povezava do prevajalca

Metoda ref() je uporabna, če ni mogoče uporabiti dostopa na podlagi lastnosti, na primer kadar tabela vsebuje stolpec z enakim imenom kot lastnost (author). V drugih primerih je zaradi boljše berljivosti priporočljiva uporaba dostopa na podlagi lastnosti.

Raziskovalec samodejno optimizira poizvedbe po zbirki podatkov. Pri iteraciji skozi knjige in dostopu do njihovih povezanih zapisov (avtorji, prevajalci) Raziskovalec ne ustvari poizvedbe za vsako knjigo posebej. Namesto tega za vsako vrsto povezave** izvede le **eno poizvedbo SELECT, s čimer znatno zmanjša obremenitev podatkovne zbirke. Na primer:

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

Ta koda bo izvedla le tri optimizirane poizvedbe po zbirki podatkov:

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

Logika za identifikacijo povezovalnega stolpca je opredeljena z implementacijo Conventions. Priporočamo uporabo DiscoveredConventions, ki analizira tuje ključe in omogoča nemoteno delo z obstoječimi povezavami tabel.

Dostop do podrejene tabele

Dostop do podrejene tabele deluje v nasprotni smeri. Zdaj vprašamo katero knjigo je napisal ta avtor ali katero knjigo je prevedel ta prevajalec. Za takšno poizvedbo uporabimo metodo related(), ki vrne objekt Selection s povezanimi zapisi. Tukaj je primer:

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

// Izpiše vse knjige, ki jih je napisal avtor
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Izhodi vse knjige, ki jih je avtor prevedel
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

Metoda related() sprejme opis povezave kot en sam argument z uporabo pike ali kot dva ločena argumenta:

$author->related('book.translator_id');    // en sam argument
$author->related('book', 'translator_id'); // dva argumenta

Raziskovalec lahko samodejno zazna pravilen povezovalni stolpec na podlagi imena nadrejene tabele. V tem primeru se poveže prek stolpca book.author_id, ker je ime izvorne tabele author:

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

Če obstaja več možnih povezav, bo Raziskovalec vrgel izjemo AmbiguousReferenceKeyException.

Seveda lahko metodo related() uporabimo tudi pri iteraciji skozi več zapisov v zanki in Raziskovalec bo tudi v tem primeru samodejno optimiziral poizvedbe:

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

Ta koda ustvari le dve učinkoviti poizvedbi SQL:

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

Razmerje veliko-več (Many-to-Many)

Za razmerje mnogo-več (M:N) je potrebna skupna tabela (v našem primeru book_tag). Ta tabela vsebuje dva stolpca tujih ključev (book_id, tag_id). Vsak stolpec se sklicuje na primarni ključ ene od povezanih tabel. Če želimo pridobiti povezane podatke, najprej pridobimo zapise iz povezovalne tabele z uporabo related('book_tag'), nato pa nadaljujemo do ciljnih podatkov:

$book = $explorer->table('book')->get(1);
// Izpiše imena oznak, dodeljenih knjigi
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name; // pridobi ime oznake prek preglednice povezav
}

$tag = $explorer->table('tag')->get(1);
// Nasprotna smer: izpiše naslove knjig s to oznako
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // pridobi naslov knjige
}

Raziskovalec ponovno optimizira poizvedbe SQL v učinkovito obliko:

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

V metodah where(), select(), order() in group() lahko uporabite posebne zapise za dostop do stolpcev iz drugih tabel. Raziskovalec samodejno ustvari potrebne povezave JOIN.

Zaznamek s piko (parent_table.column) se uporablja za razmerja 1:N, gledano z vidika nadrejene tabele:

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

// poišče knjige, katerih imena avtorjev se začnejo z 'Jon'
$books->where('author.name LIKE ?', 'Jon%');

// razvrsti knjige po imenu avtorja padajoče.
$books->order('author.name DESC');

// izpiše naslov knjige in ime avtorja
$books->select('book.title, author.name');

Za razmerja 1:N z vidika nadrejene tabele se uporablja zapis v stolpcu:

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

// Poišče avtorje, ki so napisali knjigo z besedo 'PHP' v naslovu
$authors->where(':book.title LIKE ?', '%PHP%');

// šteje število knjig za vsakega avtorja
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

V zgornjem primeru z zapisom v dvopičju (:book.title) stolpec tujega ključa ni izrecno določen. Raziskovalec samodejno zazna pravilen stolpec na podlagi imena nadrejene tabele. V tem primeru se poveže prek stolpca book.author_id, ker je ime izvorne tabele author. Če obstaja več možnih povezav, Raziskovalec vrže izjemo AmbiguousReferenceKeyException.

Povezovalni stolpec je lahko izrecno naveden v oklepajih:

// Poišče avtorje, ki so prevedli knjigo z besedo 'PHP' v naslovu
$authors->where(':book(translator).title LIKE ?', '%PHP%');

Za dostop do podatkov v več tabelah je mogoče zapise verižno povezati:

// Poišče avtorje knjig z oznako 'PHP'
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Razširitev pogojev za JOIN

Metoda joinWhere() dodaja dodatne pogoje za združevanje tabel v jeziku SQL za ključno besedo ON.

Recimo, da želimo poiskati knjige, ki jih je prevedel določen prevajalec:

// Poišče knjige, ki jih je prevedel prevajalec z imenom 'David'
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEVA povezava z avtorjem prevajalcem ON book.translator_id = translator.id AND (translator.name = 'David')

V pogoju joinWhere() lahko uporabite enake konstrukcije kot v metodi where() – operatorje, nadomestne znake, polja vrednosti ali izraze SQL.

Za bolj zapletene poizvedbe z več povezavami JOIN lahko določite namizne vzdevke:

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born &lt; ?', 1950)
	->alias(':book_tag.book.author', 'book_author');
// LEVO VEZI `book_tag` ON `tag`.`id` = `book_tag`.`tag_id`
// LEVI JOIN `knjiga` ON `knjiga_tag`.`knjiga_id` = `knjiga`.`id`
// LEVI JOIN `autor` `book_author` ON `book`.`author_id` = `book_author`.`id`
//   AND (`book_author`.`born` < 1950)

Upoštevajte, da metoda where() dodaja pogoje v stavek WHERE, metoda joinWhere() pa razširja pogoje v stavku ON med združevanjem tabel.

Ročno ustvarjanje raziskovalca

Če ne uporabljate vsebnika Nette DI, lahko primerek Nette\Database\Explorer ustvarite ročno:

use Nette\Database;

// $storage implementira Nette\Caching\Storage, npr.:
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir');
// povezava s podatkovno bazo
$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password');
// upravlja odsev strukture podatkovne zbirke
$structure = new Database\Structure($connection, $storage);
// določa pravila za preslikavo imen tabel, stolpcev in tujih ključev
$conventions = new Database\Conventions\DiscoveredConventions($structure);
$explorer = new Database\Explorer($connection, $structure, $conventions, $storage);
različica: 4.0