SQL Way

Możesz pracować z Nette Database na dwa sposoby: pisząc zapytania SQL(sposób SQL) lub pozwalając, aby SQL był generowany automatycznie(sposób Explorer). Sposób SQL pozwala na bezpieczne tworzenie zapytań przy zachowaniu pełnej kontroli nad ich strukturą.

Zobacz Połączenie i konfiguracja, aby uzyskać szczegółowe informacje na temat konfiguracji połączenia z bazą danych.

Podstawowe zapytania

Metoda query() wykonuje zapytania do bazy danych i zwraca obiekt ResultSet reprezentujący wynik. Jeśli zapytanie nie powiedzie się, metoda zgłasza wyjątek. Wynik zapytania można przeglądać za pomocą pętli foreach lub użyć jednej z funkcji pomocniczych.

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

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

Aby bezpiecznie wstawiać wartości do zapytań SQL, należy używać zapytań parametryzowanych. Nette Database sprawia, że jest to bardzo proste: wystarczy dodać przecinek i wartość do zapytania SQL.

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

W przypadku wielu parametrów można albo przeplatać zapytanie SQL z parametrami:

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

Lub najpierw napisać całe zapytanie SQL, a następnie dołączyć wszystkie parametry:

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

Ochrona przed SQL Injection

Dlaczego korzystanie z zapytań parametryzowanych jest ważne? Ponieważ chronią one przed atakami SQL injection, w których atakujący mogą wstrzykiwać złośliwe polecenia SQL w celu manipulowania danymi bazy danych lub uzyskiwania do nich dostępu.

Nigdy nie wstawiaj zmiennych bezpośrednio do zapytania SQL! Zawsze używaj sparametryzowanych zapytań, aby chronić się przed wstrzyknięciem SQL.

// ❌ NIEBEZPIECZNY KOD - podatny na wstrzyknięcie kodu SQL
$database->query("SELECT * FROM users WHERE name = '$name'");

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

Należy zapoznać się z potencjalnymi zagrożeniami bezpieczeństwa.

Techniki zapytań

Warunki WHERE

Możesz zapisać warunki WHERE jako tablicę asocjacyjną, gdzie klucze są nazwami kolumn, a wartości są danymi do porównania. Nette Database automatycznie wybiera najbardziej odpowiedni operator SQL na podstawie typu wartości.

$database->query('SELECT * FROM users WHERE', [
	'name' => 'John',
	'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1

Można również jawnie określić operator w kluczu:

$database->query('SELECT * FROM users WHERE', [
	'age >' => 25,           // używa operatora >
	'name LIKE' => '%John%', // używa operatora LIKE
	'email NOT LIKE' => '%example.com%', // używa operatora NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'

Specjalne przypadki, takie jak wartości null lub tablice, są obsługiwane automatycznie:

$database->query('SELECT * FROM products WHERE', [
	'name' => 'Laptop',         // używa operatora =
	'category_id' => [1, 2, 3], // używa IN
	'description' => null,      // uses IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL

W przypadku warunków ujemnych należy użyć operatora NOT:

$database->query('SELECT * FROM products WHERE', [
	'name NOT' => 'Laptop',         // używa operatora <>
	'category_id NOT' => [1, 2, 3], // używa NOT IN
	'description NOT' => null,      // używa IS NOT NULL
	'id' => [],                     // skipped
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL

Domyślnie warunki są łączone za pomocą operatora AND. Możesz zmienić to zachowanie za pomocą symbolu zastępczego ?or.

Reguły ORDER BY

Klauzula ORDER BY może być zdefiniowana jako tablica, w której klucze reprezentują kolumny, a wartości są wartościami logicznymi wskazującymi kolejność rosnącą:

$database->query('SELECT id FROM author ORDER BY', [
	'id' => true,  // rosnąco
	'name' => false, // malejąco
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC

Wstawianie danych (INSERT)

Aby wstawić rekordy, należy użyć instrukcji SQL INSERT.

$values = [
	'name' => 'John Doe',
	'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();

Metoda getInsertId() zwraca ID ostatniego wstawionego wiersza. W przypadku niektórych baz danych (np. PostgreSQL) należy określić nazwę sekwencji za pomocą $database->getInsertId($sequenceId).

Jako parametry można również przekazywać wartości specjalne, takie jak pliki, obiekty DateTime lub typy wyliczeniowe.

Wstawianie wielu rekordów jednocześnie:

$database->query('INSERT INTO users ?', [
	['name' => 'User 1', 'email' => 'user1@mail.com'],
	['name' => 'User 2', 'email' => 'user2@mail.com'],
]);

Wykonanie wsadowego INSERT jest znacznie szybsze, ponieważ wykonywane jest tylko jedno zapytanie do bazy danych zamiast wielu pojedynczych zapytań.

Uwaga dotycząca bezpieczeństwa: Nigdy nie używaj niezwalidowanych danych jako $values. Zapoznaj się z możliwymi zagrożeniami.

Aktualizacja danych (UPDATE)

Aby zaktualizować rekordy, należy użyć instrukcji SQL UPDATE.

// Aktualizacja pojedynczego rekordu
$values = [
	'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);

Liczbę wierszy, których to dotyczy, można sprawdzić za pomocą $result->getRowCount().

Można użyć operatorów += i -= w UPDATE:

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

Aby wstawić lub zaktualizować rekord, jeśli już istnieje, użyj techniki ON DUPLICATE KEY UPDATE:

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

Należy zauważyć, że Nette Database rozpoznaje kontekst polecenia SQL, w którym używany jest parametr z tablicą i odpowiednio generuje kod SQL. Na przykład skonstruował (id, name, year) VALUES (123, 'Jim', 1978) z pierwszej tablicy, podczas gdy drugą przekonwertował na name = 'Jim', year = 1978. Zostało to omówione bardziej szczegółowo w sekcji Wskazówki dotyczące konstruowania kodu SQL.

Usuwanie danych (DELETE)

Aby usunąć rekordy, należy użyć instrukcji SQL DELETE. Przykład z liczbą usuniętych wierszy:

$count = $database->query('DELETE FROM users WHERE id = ?', 1)
	->getRowCount();

Wskazówki dotyczące budowy SQL

Symbole zastępcze SQL pozwalają kontrolować sposób, w jaki wartości parametrów są włączane do wyrażeń SQL:

Wskazówka Opis Automatycznie używane dla
?name Używane dla nazw tabel lub kolumn
?values Generuje (key, ...) VALUES (value, ...) INSERT ... ?, REPLACE ... ?
?set Generuje przypisania key = value, ... SET ?, KEY UPDATE ?
?and Łączy warunki w tablicy z AND WHERE ?, HAVING ?
?or Łączy warunki w tablicy z OR
?order Generuje klauzulę ORDER BY ORDER BY ?, GROUP BY ?

Do dynamicznego wstawiania nazw tabel lub kolumn należy użyć symbolu zastępczego ?name. Nette Database zapewnia prawidłowe uciekanie zgodnie z konwencjami bazy danych (np. zamykanie w backticks dla MySQL).

$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (w MySQL)

Ostrzeżenie: Używaj tylko symbolu zastępczego ?name dla zweryfikowanych nazw tabel i kolumn. W przeciwnym razie istnieje ryzyko wystąpienia luk w zabezpieczeniach.

Inne podpowiedzi zwykle nie są konieczne do określenia, ponieważ Nette używa inteligentnego automatycznego wykrywania podczas konstruowania zapytań SQL (patrz trzecia kolumna tabeli). Można ich jednak użyć w sytuacjach, gdy chcemy połączyć warunki za pomocą OR zamiast AND:

$database->query('SELECT * FROM users WHERE ?or', [
	'name' => 'John',
	'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'

Wartości specjalne

Oprócz standardowych typów skalarnych (np. string, int, bool), można również przekazywać wartości specjalne jako parametry:

  • Pliki: Użyj fopen('file.png', 'r') aby wstawić binarną zawartość pliku.
  • Date and Time: obiekty DateTime są automatycznie konwertowane do formatu daty bazy danych.
  • Enum Values: Instancje enum są konwertowane na odpowiadające im wartości.
  • SQL Literals: Tworzone za pomocą Connection::literal('NOW()'), są wstawiane bezpośrednio do zapytania.
$database->query('INSERT INTO articles ?', [
	'title' => 'My Article',
	'published_at' => new DateTime,
	'content' => fopen('image.png', 'r'),
	'state' => Status::Draft,
]);

W przypadku baz danych, które nie obsługują natywnie typu datetime (np. SQLite i Oracle), wartości DateTime są konwertowane zgodnie z opcją konfiguracji formatDateTime (domyślnie: U dla uniksowego znacznika czasu).

Literały SQL

W niektórych przypadkach może być konieczne wstawienie nieprzetworzonego kodu SQL jako wartości bez traktowania go jako ciągu znaków lub ucieczki. W tym celu należy użyć obiektów klasy Nette\Database\SqlLiteral, które można utworzyć za pomocą metody Connection::literal().

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

Alternatywnie:

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

Literały SQL mogą również zawierać parametry:

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

Pozwala to na elastyczne kombinacje:

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

Pobieranie danych

Skróty dla zapytań SELECT

Aby uprościć pobieranie danych, klasa Connection udostępnia kilka skrótów, które łączą wywołanie query() z kolejnym wywołaniem fetch*(). Metody te akceptują te same parametry co query(), tj. zapytanie SQL i opcjonalne parametry. Szczegółowy opis metod fetch*() można znaleźć poniżej.

fetch($sql, ...$params): ?Row Wykonuje zapytanie i pobiera pierwszy wiersz jako obiekt Row.
fetchAll($sql, ...$params): array Wykonuje zapytanie i pobiera wszystkie wiersze jako tablicę obiektów Row.
fetchPairs($sql, ...$params): array Wykonuje zapytanie i pobiera tablicę asocjacyjną, w której pierwsza kolumna jest kluczem, a druga wartością.
fetchField($sql, ...$params): mixed Wykonuje zapytanie i pobiera wartość pierwszej komórki w pierwszym wierszu.
fetchList($sql, ...$params): ?array Wykonuje zapytanie i pobiera pierwszy wiersz jako tablicę indeksowaną.

Przykład:

// fetchField() - zwraca wartość pierwszej komórki
$count = $database->query('SELECT COUNT(*) FROM articles')
	->fetchField();

foreach – Iteracja po wierszach

Po wykonaniu zapytania zwracany jest obiekt ResultSet, który umożliwia iterację po wynikach na różne sposoby. Najprostszym i najbardziej wydajnym pamięciowo sposobem pobierania wierszy jest iteracja w pętli foreach. Metoda ta przetwarza wiersze pojedynczo i pozwala uniknąć przechowywania wszystkich danych w pamięci jednocześnie.

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

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

Pętla ResultSet może być iterowana tylko raz. Jeśli konieczne jest wielokrotne iterowanie, należy najpierw załadować dane do tablicy, na przykład za pomocą metody fetchAll().

fetch(): ?Row

Wykonuje zapytanie i pobiera pojedynczy wiersz jako obiekt Row. Jeśli nie ma więcej wierszy, zwraca null. Ta metoda przesuwa wewnętrzny wskaźnik do następnego wiersza.

$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // pobiera pierwszy wiersz
if ($row) {
	echo $row->name;
}

fetchAll(): array

Pobiera wszystkie pozostałe wiersze z ResultSet jako tablicę obiektów Row.

$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // pobiera wszystkie wiersze
foreach ($rows as $row) {
	echo $row->name;
}

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

Pobiera wyniki jako tablicę asocjacyjną. Pierwszy argument określa kolumnę, która ma być użyta jako klucz, a drugi określa kolumnę, która ma być użyta jako wartość:

$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => "John Doe", 2 => "Jane Doe", ...].

Jeśli podano tylko pierwszy parametr, wartością będzie cały wiersz (jako obiekt Row ):

$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...].

Jeśli null zostanie przekazany jako klucz, tablica będzie indeksowana numerycznie, zaczynając od zera:

$names = $result->fetchPairs(null, 'name');
// [0 => "John Doe", 1 => "Jane Doe", ...].

fetchPairs (Closure $callback)array

Alternatywnie można podać wywołanie zwrotne, które określa pary klucz-wartość lub wartości dla każdego wiersza.

$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...].

// Wywołanie zwrotne może również zwrócić tablicę z parą klucz i wartość:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...].

fetchField(): mixed

Pobiera wartość pierwszej komórki w bieżącym wierszu. Jeśli nie ma więcej wierszy, zwraca null. Ta metoda przesuwa wewnętrzny wskaźnik do następnego wiersza.

$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // pobiera nazwę z pierwszego wiersza

fetchList(): ?array

Pobiera wiersz jako indeksowaną tablicę. Jeśli nie ma więcej wierszy, zwraca null. Ta metoda przesuwa wewnętrzny wskaźnik do następnego wiersza.

$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ["John", "john@example.com"].

getRowCount(): ?int

Zwraca liczbę wierszy, których dotyczyło ostatnie zapytanie UPDATE lub DELETE. W przypadku zapytań SELECT zwraca liczbę pobranych wierszy, ale nie zawsze jest ona znana – w takich przypadkach zwraca null.

getColumnCount(): ?int

Zwraca liczbę kolumn w pliku ResultSet.

Informacje o zapytaniu

Aby uzyskać szczegółowe informacje na temat ostatnio wykonanego zapytania, należy użyć opcji

echo $database->getLastQueryString(); // wyprowadza zapytanie SQL

$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString();    // wyprowadza zapytanie SQL
echo $result->getTime();           // wyświetla czas wykonania w sekundach

Aby wyświetlić wynik jako tabelę HTML, użyj:

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

Można również pobrać informacje o typach kolumn z ResultSet:

$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();

foreach ($types as $column => $type) {
	echo "$column is of type $type->type"; // np. "id jest typu int
}

Rejestrowanie zapytań

Można zaimplementować niestandardowe rejestrowanie zapytań. Zdarzenie onQuery to tablica wywołań zwrotnych, które są wywoływane po każdym wykonaniu zapytania:

$database->onQuery[] = function ($database, $result) use ($logger) {
	$logger->info('Query: ' . $result->getQueryString());
	$logger->info('Time: ' . $result->getTime());

	if ($result->getRowCount() > 1000) {
		$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
	}
};
wersja: 4.0