Eksplorator baz danych

Nette Database Explorer (dawniej Nette Database Table, NDBT) znacznie ułatwia pobieranie danych z bazy danych bez konieczności pisania zapytań SQL.

  • Zadaje skuteczne pytania
  • nie przekazuje zbędnych danych
  • ma elegancką składnię

Korzystanie z Database Explorera rozpoczyna się od tabeli poprzez wywołanie metody table() nad obiektem Nette\Database\Explorer. Najłatwiejszy sposób jej uzyskania opisany jest tutaj, ale jeśli korzystasz z Nette Database Explorera samodzielnie, możesz również utworzyć ją ręcznie.

$books = $explorer->table('book'); // nazwa tabeli to 'book'

Zwraca obiekt Selection, nad którym możemy iterować, aby przejść przez wszystkie książki. Wiersze są instancjami ActiveRow i możemy z nich bezpośrednio odczytywać dane.

foreach ($books as $book) {
	echo $book->title;
	echo $book->author_id;
}

Wybór jednego konkretnego wiersza odbywa się za pomocą metody get(), która zwraca bezpośrednio instancję ActiveRow.

$book = $explorer->table('book')->get(2); // zwróć książkę o id 2
echo $book->title;
echo $book->author_id;

Spróbujmy na prostym przykładzie. Musimy wybrać z bazy danych książki i ich autorów. Jest to prosty przykład wiązania 1:N. Często spotykanym rozwiązaniem jest wybieranie danych jednym zapytaniem SQL, łącząc tabele za pomocą JOIN. Inną opcją jest wybranie danych osobno, za pomocą jednego zapytania o książkę, a następnie dla każdej książki wybranie jej autora (np. za pomocą pętli foreach). Można to zoptymalizować do dwóch zapytań do bazy, jednego dla książek i jednego dla autorów – i tak właśnie robi to Nette Database Explorer.

W poniższych przykładach będziemy pracować ze schematem bazy danych przedstawionym na rysunku. Istnieją wiązania OneHasMany (1:N) (autor książki, author_id, oraz dowolny tłumacz, translator_id, który może mieć wartość null) oraz ManyHasMany (M:N) pomiędzy książką a jej tagami.

Możesz znaleźć przykład, w tym schemat, na GitHubie.

Struktura bazy danych dla powyższych przykładów

Poniższy kod wymienia nazwę autora każdej książki i wszystkie jej tagi. Za chwilę omówimy dokładnie jak to działa.

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

foreach ($books as $book) {
	echo 'tytuł: ' . $book->title;
	echo 'napisane przez: ' . $book->author->name; // $book->author je řádek z tabulky 'autor'

	echo 'tagi: ';
	foreach ($book->related('book_tag') as $bookTag) {
		echo $bookTag->tag->name . ', '; // $bookTag->tag je řádek z tabulky 'tag'
	}
}

Będziesz mile zaskoczony tym, jak sprawnie działa warstwa bazodanowa. Powyższy przykład wykona stałą liczbę żądań, które wyglądają tak:

SELECT * FROM `book`
SELECT * FROM `author` WHERE (`author`.`id` IN (11, 12))
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3))
SELECT * FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23))

Jeśli użyjesz pamięci podręcznej (jest ona domyślnie włączona), żadne niepotrzebne kolumny nie będą pobierane z bazy danych. Po pierwszym zapytaniu nazwy używanych kolumn będą przechowywane w pamięci podręcznej i tylko kolumny, których faktycznie używasz, będą pobierane z bazy danych:

SELECT `id`, `title`, `author_id` FROM `book`
SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (11, 12))
SELECT `book_id`, `tag_id` FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3))
SELECT `id`, `name` FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23))

Wybór

Przyjrzyjmy się opcjom filtrowania i ograniczania selekcji przy użyciu klasy Nette\Database\Table\Selection:

$table->where($where[, $param[, ...]]) Ustawia WHERE używając AND jako łącznika, gdy występuje więcej niż jeden warunek
$table->whereOr($where) Ustawia WHERE używając OR jako łącznika w więcej niż jednym warunku
$table->order($columns) Ustawia ORDER BY, może być wyrażeniem ('column DESC, id DESC')
$table->select($columns) Ustawia zwrócone kolumny, może być wyrażeniem ('col, MD5(col) AS hash')
$table->limit($limit[, $offset]) Ustawia Limit i OFFSET
$table->page($page, $itemsPerPage[, &$lastPage]) Ustaw paginację
$table->group($columns) Set GROUP BY
$table->having($having) Set HAVING

Możemy użyć tak zwanego płynnego interfejsu, na przykład $table->where(...)->order(...)->limit(...). Wiele warunków where lub whereOr jest połączonych operatorem AND.

gdzie()

Nette Database Explorer automatycznie dodaje odpowiednie operatory w zależności od otrzymanych danych:

$table->where('field', $value) pole = $value
$table->where('field', null) field IS NULL
$table->where('field > ?', $val) pole > $val
$table->where('field', [1, 2]) pole IN (1, 2)
$table->where('id = ? OR name = ?', 1, $name) id = 1 OR name = ‘Jon Snow’
$table->where('field', $explorer->table($tableName)) field IN (SELECT $primary FROM $tableName)
$table->where('field', $explorer->table($tableName)->select('col')) field IN (SELECT col FROM $tableName)

Placeholder (znak zapytania) działa nawet bez operatora kolumny. Kolejne połączenia są takie same:

$table->where('id = ? OR id = ?', 1, 2);
$table->where('id ? OR id ?', 1, 2);

Dzięki temu możliwe jest wygenerowanie właściwego operatora na podstawie wartości:

$table->where('id ?', 2);    // id = 2
$table->where('id ?', null); // id IS NULL
$table->where('id', $ids);   // id IN (...)

Wybór poprawnie obsługuje warunki negatywne i może również pracować z pustymi polami:

$table->where('id', []);       // id IS NULL AND FALSE
$table->where('id NOT', []);   // id IS NULL OR TRUE
$table->where('NOT (id ?)', $ids);  // NOT (id IS NULL AND FALSE)

// powoduje to wyjątek, ta składnia nie jest obsługiwana
$table->where('NOT id ?', $ids);

gdzieOr()

Przykład użycia bez parametrów:

// WHERE (user_id IS NULL) OR (SUM(`field1`) > SUM(`field2`))
$table->whereOr([
	'user_id IS NULL',
	'SUM(field1) > SUM(field2)',
]);

Parametry użytkowe. Jeśli nie określisz operatora, Nette Database Explorer automatycznie doda odpowiedni:

// WHERE (`field1` IS NULL) OR (`field2` IN (3, 5)) OR (`amount` > 11)
$table->whereOr([
	'field1' => null,
	'field2' => [3, 5],
	'amount >' => 11,
]);

Możesz określić wyrażenie zawierające znaki zapytania wieloznaczne w kluczu, a następnie przekazać parametry w wartości:

// WHERE (`id` > 12) OR (ROUND(`id`, 5) = 3)
$table->whereOr([
	'id > ?' => 12,
	'ROUND(id, ?) = ?' => [5, 3],
]);

order()

Przykłady użycia:

$table->order('field1');               // ORDER BY `field1`
$table->order('field1 DESC, field2');  // ORDER BY `field1` DESC, `field2`
$table->order('field = ? DESC', 123);  // ORDER BY `field` = 123 DESC

wybierz()

Przykłady użycia:

$table->select('field1');                 // SELECT `field1`
$table->select('col, UPPER(col) AS abc'); // SELECT `col`, UPPER(`col`) AS abc
$table->select('SUBSTR(title, ?)', 3);    // SELECT SUBSTR(`title`, 3)

limit()

Przykłady zastosowania:

$table->limit(1);     // LIMIT 1
$table->limit(1, 10); // LIMIT 1 OFFSET 10

strona()

Alternatywny sposób ustawienia limitu i offsetu:

$page = 5;
$itemsPerPage = 10;
$table->page($page, $itemsPerPage); // LIMIT 10 OFFSET 40

Pobierz numer ostatniej strony, przekaż go do zmiennej $lastPage:

$table->page($page, $itemsPerPage, $lastPage);

group()

Przykłady użycia:

$table->group('field1');          // GROUP BY `field1`
$table->group('field1, field2');  // GROUP BY `field1`, `field2`

having()

Przykłady użycia:

$table->having('COUNT(items) >', 100);  // HAVING COUNT(`items`) > 100

Wybór według wartości z innej tabeli

Często potrzebujemy filtrować wyniki za pomocą warunku, który dotyczy innej tabeli bazy danych. Tego typu warunek wymaga łączenia tabel, ale dzięki Nette Database Explorer nigdy nie musimy pisać ich ręcznie.

Załóżmy, że chcemy wybrać wszystkie książki napisane przez autora o nazwie Jon. Musimy jedynie wpisać nazwę klucza sesji join oraz nazwę kolumny połączonej tabeli. Klucz join pochodzi od nazwy kolumny, która odnosi się do tabeli, z którą chcemy się połączyć. W naszym przykładzie (patrz schemat bazy danych) jest to kolumna author_id, z której wystarczy użyć części – author. name to nazwa kolumny w tabeli author. Możemy również stworzyć warunek dla tłumacza książek, do którego dołączamy kolumnę translator_id.

$books = $explorer->table('book');
$books->where('author.name LIKE ?', '%Jon%');
$books->where('translator.name', 'David Grudl');

Logika tworzenia klucza join jest podana przez implementację Conventions. Zalecamy użycie DiscoveredConventions, który parsuje klucze obce i pozwala łatwo pracować z relacjami między tabelami.

Relacja między książką a autorem wynosi 1:N. Możliwa jest również relacja odwrotna, nazywamy ją backjoin. Rozważmy następujący przykład. Chcemy wybrać wszystkich autorów, którzy napisali więcej niż trzy książki. Do stworzenia relacji odwrotnej używamy : (dvojtečku). Dvojtečka znamená, že jde o vztah hasMany (a je to logické, dvě tečky jsou více než jedna). Bohužel třída Selection není dostatečně chytrá a musíme mu pomoci s agregací výsledků a předat mu část GROUP BY, również warunek musi być zapisany jako HAVING.

$authors = $explorer->table('author');
$authors->group('author.id')
	->having('COUNT(:book.id) > 3');

Być może zauważyłeś, że wyrażenie join odwołuje się do book, ale nie jest jasne, czy łączymy się przez author_id czy translator_id. W powyższym przykładzie Selection łączy się przez kolumnę author_id, ponieważ znaleziono dopasowanie do nazwy tabeli źródłowej – tabeli author. Gdyby nie było dopasowania i istniało wiele możliwości, Nette rzuciłoby AmbiguousReferenceKeyException.

Aby dołączyć poprzez translator_id, wystarczy dodać opcjonalny parametr do wyrażenia join.

$authors = $explorer->table('author');
$authors->group('author.id')
	->having('COUNT(:book(translator).id) > 3');

Przyjrzyjmy się teraz bardziej złożonemu przykładowi łączenia tabel.

Chcemy wybrać wszystkich autorów, którzy napisali coś o PHP. Wszystkie książki mają etykiety, więc chcemy wybrać wszystkich autorów, którzy napisali książkę z etykietą “PHP”.

$authors = $explorer->table('author');
$authors->where(':book:book_tags.tag.name', 'PHP')
	->group('author.id')
	->having('COUNT(:book:book_tags.tag.id) > 0');

Agregacja wyników

$table->count('*') Zwraca liczbę wierszy
$table->count("DISTINCT $column") Zwraca liczbę różnych wartości.
$table->min($column) Zwraca wartość minimalną
$table->max($column) Zwraca maksymalną wartość
$table->sum($column) Zwraca sumę wszystkich wartości
$table->aggregation("GROUP_CONCAT($column)") Dla każdej innej funkcji agregacji

Metoda count() bez określonego parametru wybiera wszystkie rekordy i zwraca rozmiar tablicy, co jest bardzo nieefektywne. Na przykład, jeśli potrzebujesz policzyć liczbę wierszy dla paginacji, zawsze podaj pierwszy argument.

Ucieczka i cytaty

Database Explorer potrafi sprytnie wymykać się parametrom i identyfikatorom. Jednak dla prawidłowej funkcjonalności należy przestrzegać kilku zasad:

  • wielkie litery w słowach kluczowych, nazwach funkcji, nazwach procedur itp.
  • pisać nazwy kolumn i tabel małymi literami
  • ustawianie wartości za pomocą parametrów
->where('name like ?', 'John'); // ZŁY! generuje: `name` `like` ?
->where('name LIKE ?', 'John'); // PRAWDA

->where('KEY = ?', $value); // PRAWDA! KEY jest słowem kluczowym
->where('key = ?', $value); // PRAWDA. generuje: `key` = ?

->where('name = ' . $name); // ZŁY! sql injection!
->where('name = ?', $name); // PRAWDA

->select('DATE_FORMAT(created, "%d.%m.%Y")'); // ZŁY! wartości są wstawiane przez parametr
->select('DATE_FORMAT(created, ?)', '%d.%m.%Y'); // PRAWDA

Niewłaściwe użycie może prowadzić do powstania luk w zabezpieczeniach aplikacji.

Dane do odczytu

foreach ($table as $id => $row) Iteruje przez wszystkie wiersze wyniku
$row = $table->get($id) Zwraca jeden wiersz o ID $id
$row = $table->fetch() Zwraca następny wiersz wyniku.
$array = $table->fetchPairs($key, $value) Zwraca wszystkie wyniki jako tablicę asocjacyjną
$array = $table->fetchPairs($key) Zwraca wszystkie wiersze jako tablicę asocjacyjną
count($table) Zwraca liczbę wierszy w wyniku.

Wstawianie, aktualizacja i usuwanie

Metoda insert() akceptuje tablice lub obiekty Traversable (na przykład ArrayHash, z którym pracują formularze):

$row = $explorer->table('users')->insert([
	'name' => $name,
	'year' => $year,
]);
// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978)

Jeśli tabela ma zdefiniowany klucz główny, zwraca nowy wiersz jako obiekt ActiveRow.

Wkładka wielokrotna:

$explorer->table('users')->insert([
	[
		'name' => 'Jim',
		'year' => 1978,
	], [
		'name' => 'Jack',
		'year' => 1987,
	],
]);
// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987)

Jako parametry możemy również przekazywać pliki lub obiekty DateTime:

$explorer->table('users')->insert([
	'name' => $name,
	'created' => new DateTime, // nebo $explorer::literal('NOW()')
	'avatar' => fopen('image.gif', 'r'), // vloží soubor
]);

Edycja rekordów (zwraca liczbę zmienionych wierszy):

$count = $explorer->table('users')
	->where('id', 10) //  musí se volat před update()
	->update([
		'name' => 'Ned Stark'
	]);
// UPDATE `users` SET `name`='Ned Stark' WHERE (`id` = 10)

Do aktualizacji możemy użyć operatorów += i -=:

$explorer->table('users')
	->update([
		'age+=' => 1, // všimněte si +=
	]);
// UPDATE users SET `age` = `age` + 1

Usuń rekordy (zwraca liczbę usuniętych wierszy):

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

Wiązania między tabelami

Sesja Ma jeden

Bardzo często spotykana jest sesja has one. Książka ma jednego autora. Książka ma jednego tłumacza. Wiersz, który znajduje się w relacji has one uzyskujemy za pomocą metody ref() Przyjmuje ona dwa argumenty: nazwę tabeli docelowej oraz nazwę kolumny złączenia. Zobacz przykład:

$book = $explorer->table('book')->get(1);
$book->ref('author', 'author_id');

W powyższym przykładzie wybieramy powiązanego autora z tabeli author. Klucz główny tabeli author jest wyszukiwany przez kolumnę book.author_id. Metoda ref() zwraca instancję ActiveRow lub null, jeśli poszukiwany rekord nie istnieje. Zwrócony wiersz jest instancją ActiveRow, więc możemy z nim pracować tak jak z rekordem książki.

$author = $book->ref('author', 'author_id');
$author->name;
$author->born;

// nebo přímo
$book->ref('author', 'author_id')->name;
$book->ref('author', 'author_id')->born;

Książka ma też jednego tłumacza, łatwo możemy poznać jego nazwisko.

$book->ref('author', 'translator_id')->name

Takie podejście jest funkcjonalne, ale wciąż nieco uciążliwe, nie sądzisz? Baza danych zawiera już definicje kluczy obcych, więc dlaczego nie używać ich automatycznie. Spróbujmy.

Jeśli uzyskamy dostęp do zmiennej członkowskiej, która nie istnieje, ActiveRow spróbuje użyć nazwy tej zmiennej dla sesji “ma jedną”. Odczytanie tej zmiennej jest takie samo jak wywołanie metody ref() z tylko jednym parametrem. Parametr ten będziemy nazywać kluczem. Ten klucz zostanie użyty do znalezienia klucza obcego w tabeli. Przekazany klucz jest porównywany z kolumnami, a jeśli pasuje do reguł, klucz obcy na tej kolumnie jest używany do odczytu danych z powiązanej tabeli. Zobacz przykład:

$book->author->name;
// to samo co
$book->ref('author')->name;

Instancja ActiveRow nie ma kolumny author. Wszystkie kolumny tabeli book są skanowane w poszukiwaniu dopasowania do klucza. Dopasowanie w tym przypadku oznacza, że nazwa kolumny musi zawierać klucz. W powyższym przykładzie kolumna author_id zawiera ciąg “autor”, a zatem pasuje do klucza “autor”. Jeśli chcemy uzyskać dostęp do rekordu tłumacza, używamy klucza ‘translator’ w podobny sposób, ponieważ będzie on pasował do kolumny translator_id Więcej o logice dopasowywania kluczy można przeczytać w Łączenie wyrażeń.

echo $book->title . ': ';
echo $book->author->name;
if ($book->translator) {
	echo ' (translated by ' . $book->translator->name . ')';
}

Jeśli chcemy zdobyć autora wielu książek, stosujemy to samo podejście. Nette Database Explorer pobierze dla nas rekordy autora i tłumacza dla wszystkich książek naraz.

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

Ten kod wywoła tylko te trzy zapytania do bazy danych:

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id ze sloupce author_id vybraných knih
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- id ze sloupce translator_id vybraných knih

Sesja Ma wiele

Sesja “ma wielu” jest po prostu odwrotnością sesji “ma jednego”. Autor napisał kilka (wiele) książek. Autorka przetłumaczyła kilka (wiele) książek. Ten typ relacji jest trudniejszy, ponieważ relacja jest nazwana (“napisał”, “przetłumaczył”). ActiveRow posiada metodę related(), która zwraca tablicę powiązanych rekordów. Rekordy są ponownie instancjami ActiveRow. Zobacz przykład:

$author = $explorer->table('author')->get(11);
echo $author->name . ' napsal:';

foreach ($author->related('book.author_id') as $book) {
	echo $book->title;
}

echo 'a přeložil:';
foreach ($author->related('book.translator_id') as $book) {
	echo $book->title;
}

Metoda related() przyjmuje opis połączenia jako dwa argumenty lub jako pojedynczy argument połączony kropką. Pierwszy argument to tabela docelowa, drugi to kolumna.

$author->related('book.translator_id');
// je stejné jako
$author->related('book', 'translator_id');

Możemy wykorzystać heurystykę Nette Database Explorer opartą na kluczach obcych i użyć tylko klucza. Klucz zostanie porównany z kluczami obcymi, które odwołują się do bieżącej tabeli (tabela author). Jeśli zostanie znalezione dopasowanie, Nette Database Explorer użyje tego klucza obcego, w przeciwnym razie rzuci Nette\InvalidArgumentException lub AmbiguousReferenceKeyException. Więcej o logice dopasowania kluczy można przeczytać w Łączenie wyrażeń.

Oczywiście metodę related() można wywołać na wszystkich wyszukanych autorach, a Nette Database Explorer załaduje wszystkie pasujące książki jednocześnie.

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

W powyższym przykładzie uruchomiono tylko te dwa zapytania do bazy danych:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id vybraných autorů

Ręczny Eksplorator Tworzenia

Jeśli utworzyliśmy połączenie z bazą danych za pomocą konfiguracji aplikacji, nie musimy się o nic martwić. Stworzyliśmy również usługę taką jak Nette\Database\Explorer, którą możemy przekazać za pomocą DI.

Jeśli jednak korzystamy z Nette Database Explorer osobno, musimy ręcznie utworzyć instancję Nette\Database\Explorer.

// $storage obsahuje implementację Nette\Caching\Storage, např..:
$storage = new Nette\Caching\Storages\FileStorage($tempDir);
$connection = new Nette\Database\Connection($dsn, $user, $password);
$structure = new Nette\Database\Structure($connection, $storage);
$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure);
$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage);
wersja: 4.0