Baza danych Rdzeń

Nette Database Core to warstwa bazowa dostępu do bazy danych, tzw. warstwa abstrakcji bazy danych.

Instalacja

Pobierz i zainstaluj bibliotekę za pomocą Composera:

composer require nette/database

Podłączenie i konfiguracja

Aby połączyć się z bazą danych, wystarczy stworzyć instancję klasy Nette\Database\Connection:

$database = new Nette\Database\Connection($dsn, $user, $password);

Parametr $dsn (nazwa źródła danych) jest taki sam jak ten używany przez PDO, np. host=127.0.0.1;dbname=test. W przypadku niepowodzenia rzuca wyjątek Nette\Database\ConnectionException.

Bardziej poręczny sposób oferuje jednak konfiguracja aplikacji, gdzie wystarczy dodać sekcję database, a ona sama utworzy potrzebne obiekty, a także pasek bazy danych w pasku Tracy.

database:
	dsn: 'mysql:host=127.0.0.1;dbname=test'
	user: root
	password: password

Następnie otrzymujemy obiekt połączenia jako usługę z kontenera DI, na przykład:

class Model
{
	// pro práci s vrstvou Database Explorer si předáme Nette\Database\Explorer
	public function __construct(
		private Nette\Database\Connection $database,
	) {
	}
}

Więcej informacji na temat konfiguracji bazy danych.

Pytania

Zapytania do bazy danych są zadawane za pomocą metody query(), która zwraca ResultSet.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
}

echo $result->getRowCount(); // zwraca liczbę wierszy wyniku, jeśli jest znana

Możesz iterować nad ResultSet tylko raz, jeśli potrzebujesz iterować więcej niż raz, musisz przekonwertować wynik na tablicę za pomocą metody fetchAll().

Bardzo łatwo jest również dodać parametry do zapytania, zauważ znak zapytania:

$database->query('SELECT * FROM users WHERE name = ?', $name);

$database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active);

$database->query('SELECT * FROM users WHERE id IN (?)', $ids); // $ids je pole

OSTRZEŻENIE, nigdy nie komponuj zapytań jako ciągów znaków, to stworzyłoby lukę w SQL injection

$db->query('SELECT * FROM users WHERE name = ' . $name); // ŠPATNĚ!!!

Jeśli query() nie powiedzie się, rzuci albo Nette\Database\DriverException albo jeden z jego potomków:

Oprócz strony query() znajdują się na niej inne przydatne funkcje:

// zwróć tablicę asocjacyjną id => nazwa
$pairs = $database->fetchPairs('SELECT id, name FROM users');

// zwraca wszystkie rekordy jako tablicę
$rows = $database->fetchAll('SELECT * FROM users');

// zwraca pojedynczy rekord
$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id);

// zwraca bezpośrednio wartość komórki
$name = $database->fetchField('SELECT name FROM users WHERE id = ?', $id);

W przypadku niepowodzenia, wszystkie te metody rzucą Nette\Database\DriverException.

Wstawianie, aktualizacja i usuwanie

Parametr, który wstawiamy do zapytania SQL może być również tablicą (w tym przypadku możliwe jest również użycie placeholdera ? vynechat), což se hodí třeba pro sestavení příkazu INSERT:

$database->query('INSERT INTO users ?', [ // możemy tutaj pominąć znak zapytania
	'name' => $name,
	'year' => $year,
]);
// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978)

$id = $database->getInsertId(); // zwróć auto-increment wstawionego rekordu

$id = $database->getInsertId($sequence); // lub wartość sekwencji

Multiple INSERT:

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

Jako parametry mogą być przekazywane pliki, obiekty DateTime lub typy wyliczeniowe:

$database->query('INSERT INTO users', [
	'name' => $name,
	'created' => new DateTime, // nebo $database::literal('NOW()')
	'avatar' => fopen('image.gif', 'r'), // vloží soubor
	'status' => State::New, // enum State
]);

Edycja zapisów:

$result = $database->query('UPDATE users SET', [
	'name' => $name,
	'year' => $year,
], 'WHERE id = ?', $id);
// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123

echo $result->getRowCount(); // zwraca liczbę dotkniętych wierszy

Dla UPDATE możemy użyć operatorów += i -=:

$database->query('UPDATE users SET', [
	'age+=' => 1, // uwaga +=
], 'WHERE id = ?', $id);
// UPDATE users SET `age` = `age` + 1

Usuwanie:

$result = $database->query('DELETE FROM users WHERE id = ?', $id);
echo $result->getRowCount(); // zwraca liczbę dotkniętych wierszy

Pytania zaawansowane

Wstawianie lub edycja rekordu, jeśli już istnieje:

$database->query('INSERT INTO users', [
	'id' => $id,
	'name' => $name,
	'year' => $year,
], 'ON DUPLICATE KEY UPDATE', [
	'name' => $name,
	'year' => $year,
]);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
//   ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978

Zauważ, że Nette Database rozpoznaje kontekst instrukcji SQL, w której wstawiony jest parametr z polem i odpowiednio buduje kod SQL. Zbudował więc (id, name, year) VALUES (123, 'Jim', 1978) z pierwszego pola, jednocześnie przekształcając drugie pole do postaci name = 'Jim', year = 1978.

Możemy również wpłynąć na sortowanie za pomocą tablicy, w kluczach podamy kolumny, a wartością będzie boolean określający czy sortować w porządku rosnącym:

$database->query('SELECT id FROM author ORDER BY', [
	'id' => true, // vzestupně
	'name' => false, // sestupně
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC

Jeśli wykrywanie nie działa dla nietypowej konstrukcji, możesz określić formę budowy przez symbol wieloznaczny ?, po którym następuje podpowiedź. Obsługiwane są następujące wskazówki:

(klucz1, klucz2, …) VALUES (wartość1, wartość2, …)
?set key1 = value1, key2 = value2, …
?and key1 = value1 AND key2 = value2 …
lub key1 = value1 OR key2 = value2 …
?order key1 ASC, key2 DESC

Klauzula WHERE używa operatora ?and, więc warunki są połączone operatorem AND:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	'year' => $year,
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978

Które możemy łatwo zmienić na OR podając placeholder ?or:

$result = $database->query('SELECT * FROM users WHERE ?or', [
	'name' => $name,
	'year' => $year,
]);
// SELECT * FROM users WHERE `name` = 'Jim' OR `year` = 1978

W warunkach możemy używać operatorów:

$result = $database->query('SELECT * FROM users WHERE', [
	'name <>' => $name,
	'year >' => $year,
]);
// SELECT * FROM users WHERE `name` <> 'Jim' AND `year` > 1978

A także wyliczenia:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => ['Jim', 'Jack'],
	'role NOT IN' => ['admin', 'owner'], // výčet + operátor NOT IN
]);
// SELECT * FROM users WHERE
//   `name` IN ('Jim', 'Jack') AND `role` NOT IN ('admin', 'owner')

Możemy również wstawić do warunku fragment własnego kodu SQL, używając tzw. literału SQL:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())

Lub alternatywnie:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())

Literał SQL może mieć również parametry:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)

Co pozwala nam na tworzenie ciekawych kombinacji:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('?or', [
		'active' => true,
		'role' => $role,
	]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')

Nazwa zmiennej

Istnieje również placeholder ?name, którego używasz, jeśli nazwa tabeli lub kolumny jest zmienną. (Uważaj, aby nie pozwolić użytkownikowi manipulować zawartością takiej zmiennej):

$table = 'blog.users';
$column = 'name';
$database->query('SELECT * FROM ?name WHERE ?name = ?', $table, $column, $name);
// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim'

Transakcja

Istnieją trzy metody pracy z transakcjami:

$database->beginTransaction(); // rozpoczęcie transakcji

$database->commit(); // commit

$database->rollback(); // rollback

Metoda transaction() zapewnia elegancki sposób przekazywania wywołania zwrotnego, które jest wykonywane w transakcji. Jeśli podczas wykonywania zostanie rzucony wyjątek, transakcja jest odrzucana; jeśli wszystko pójdzie dobrze, transakcja jest popełniana.

$id = $database->transaction(function ($database) {
	$database->query('DELETE FROM ...');
	$database->query('INSERT INTO ...');
	// ...
	return $database->getInsertId();
});

Jak widać, metoda transaction() zwraca wartość zwrotną wywołania zwrotnego.

Callback transaction() może być również zagnieżdżony, co upraszcza implementację niezależnych repozytoriów.

Refleksja

Nette Database zapewnia narzędzia do introspekcji struktury bazy danych poprzez klasę Nette\Database\Reflection. Klasa ta umożliwia pobieranie informacji o tabelach, kolumnach, indeksach i kluczach obcych. Refleksji można używać do generowania schematów, tworzenia elastycznych aplikacji współpracujących z bazami danych lub tworzenia ogólnych narzędzi bazodanowych.

Obiekt refleksji można uzyskać z instancji połączenia z bazą danych:

$reflection = $database->getReflection();

Praca z tabelami

Używając refleksji, można iterować po wszystkich tabelach w bazie danych:

// Lista nazw wszystkich tabel
foreach ($reflection->tables as $tableName => $table) {
    echo $tableName . "\n";
}

// Sprawdzenie, czy tabela istnieje
if ($reflection->hasTable('users')) {
    echo "The 'users' table exists";
}

// Pobieranie określonej tabeli
$table = $reflection->getTable('users');

Informacje o kolumnie

Dla każdej tabeli można uzyskać szczegółowe informacje o jej kolumnach:

// Iteracja po wszystkich kolumnach
foreach ($table->columns as $column) {
    echo "Column: " . $column->name . "\n";
    echo "Type: " . $column->nativeType . "\n";
    echo "Nullable: " . ($column->nullable ? 'Yes': 'No') . "\n";
    echo "Default value: " . ($column->default ?? 'None') . "\n";
    echo "Primary key: " . ($column->primary ? 'Yes': 'No') . "\n";
    echo "Auto-increment: " . ($column->autoIncrement ? 'Yes': 'No') . "\n";
}

// Pobieranie określonej kolumny
$idColumn = $table->getColumn('id');

Indeksy i klucze podstawowe

Refleksja dostarcza informacji na temat indeksów i kluczy głównych:

$listColumnNames = fn(array $columns) => implode(', ', array_map(fn($col) => $col->name, $columns));

// Lista wszystkich indeksów
foreach ($table->indexes as $index) {
    echo "Index: " . ($index->name ?? 'Unnamed') . "\n";
    echo "Columns: " . $listColumnNames($index->columns) . "\n";
    echo "Unique: " . ($index->unique ? 'Yes': 'No') . "\n";
    echo "Primary key: " . ($index->primary ? 'Yes': 'No') . "\n";
}

// Pobieranie klucza głównego
if ($table->primaryKey) {
    echo "Primary key: " . $listColumnNames($table->primaryKey->columns) . "\n";
}

Klucze obce

Można również uzyskać informacje na temat kluczy obcych:

foreach ($table->foreignKeys as $fk) {
    echo "Foreign key: " . ($fk->name ?? 'Unnamed') . "\n";
    echo "Local columns: " . $listColumnNames($fk->localColumns) . "\n";
    echo "References table: {$fk->foreignTable->name}\n";
    echo "References columns: " . $listColumnNames($fk->foreignColumns) . "\n";
}
wersja: 4.0