Direct SQL

Z Nette Database można pracować na dwa sposoby: pisząc zapytania SQL bezpośrednio(Direct Access) lub pozwalając na automatyczne generowanie SQL(Explorer Access). Direct Access pozwala na bezpieczne tworzenie zapytań przy zachowaniu pełnej kontroli nad ich strukturą.

Informacje na temat tworzenia połączenia i jego konfiguracji można znaleźć na osobnej stronie.

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