SQL pristop
Nette Database ponuja dve poti: lahko pišete SQL poizvedbe sami (SQL pristop) ali pa jih pustite samodejno generirati (glej Explorer). SQL pristop vam daje popoln nadzor nad poizvedbami in hkrati zagotavlja njihovo varno sestavljanje.
Podrobnosti o povezavi in konfiguraciji baze podatkov najdete v poglavju Povezava in konfiguracija.
Osnovno poizvedovanje
Za poizvedovanje v bazi podatkov služi metoda query()
. Ta vrne objekt ResultSet, ki predstavlja rezultat poizvedbe.
V primeru napake metoda vrže izjemo. Rezultat poizvedbe lahko
prehajamo z zanko foreach
ali uporabimo katero od pomožnih funkcij.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
Za varno vstavljanje vrednosti v SQL poizvedbe uporabljamo parametrizirane poizvedbe. Nette Database jih naredi maksimalno preproste – zadostuje, da za SQL poizvedbo dodamo vejico in vrednost:
$database->query('SELECT * FROM users WHERE name = ?', $name);
Pri več parametrih imate dve možnosti zapisa. Lahko SQL poizvedbo “prepletate” s parametri:
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
Ali pa najprej napišete celotno SQL poizvedbo in nato priključite vse parametre:
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Zaščita pred SQL injection
Zakaj je pomembno uporabljati parametrizirane poizvedbe? Ker vas ščitijo pred napadom, imenovanim SQL injection, pri katerem bi napadalec lahko podtaknil lastne SQL ukaze in s tem pridobil ali poškodoval podatke v bazi podatkov.
Nikoli ne vstavljajte spremenljivk neposredno v SQL poizvedbo! Vedno uporabljajte parametrizirane poizvedbe, ki vas ščitijo pred SQL injection.
// ❌ NEVARNA KODA - ranljiva za SQL injection
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Varna parametrizirana poizvedba
$database->query('SELECT * FROM users WHERE name = ?', $name);
Seznanite se z možnimi varnostnimi tveganji.
Tehnike poizvedovanja
Pogoji WHERE
Pogoje WHERE lahko zapišete kot asociativno polje, kjer so ključi imena stolpcev in vrednosti podatki za primerjavo. Nette Database samodejno izbere najprimernejši SQL operator glede na tip vrednosti.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
V ključu lahko tudi eksplicitno določite operator za primerjavo:
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // uporabi operator >
'name LIKE' => '%John%', // uporabi operator LIKE
'email NOT LIKE' => '%example.com%', // uporabi operator NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette samodejno obravnava posebne primere, kot so null
vrednosti ali polja.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // uporabi operator =
'category_id' => [1, 2, 3], // uporabi IN
'description' => null, // uporabi IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
Za negativne pogoje uporabite operator NOT
:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // uporabi operator <>
'category_id NOT' => [1, 2, 3], // uporabi NOT IN
'description NOT' => null, // uporabi IS NOT NULL
'id' => [], // izpusti se
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
Za združevanje pogojev se uporablja operator AND
. To lahko spremenite z uporabo nadomestnega znaka ?or.
Pravila ORDER BY
Razvrščanje ORDER BY
lahko zapišemo z uporabo polja. V ključih navedemo stolpce, vrednost pa bo boolean, ki
določa, ali razvrščati naraščajoče:
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // naraščajoče
'name' => false, // padajoče
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Vstavljanje podatkov (INSERT)
Za vstavljanje zapisov se uporablja SQL ukaz INSERT
.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
Metoda getInsertId()
vrne ID zadnje vstavljene vrstice. Pri nekaterih bazah podatkov (npr. PostgreSQL) je treba
kot parameter določiti ime sekvence, iz katere naj se ID generira z uporabo
$database->getInsertId($sequenceId)
.
Kot parametre lahko posredujemo tudi speciální hodnoty kot so datoteke, objekti DateTime ali naštevni tipi.
Vstavljanje več zapisov hkrati:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
Večkratni INSERT je veliko hitrejši, ker se izvede ena sama poizvedba baze podatkov namesto mnogih posameznih.
Varnostno opozorilo: Nikoli ne uporabljajte kot $values
nevalidiranih podatkov. Seznanite se z možnimi tveganji.
Posodabljanje podatkov (UPDATE)
Za posodabljanje zapisov se uporablja SQL ukaz UPDATE
.
// Posodobitev enega zapisa
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
Število prizadetih vrstic vrne $result->getRowCount()
.
Za UPDATE lahko uporabimo operatorja +=
in -=
:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // inkrementacija login_count
], 1);
Primer vstavljanja ali urejanja zapisa, če že obstaja. Uporabimo tehniko 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
Opazite, da Nette Database prepozna, v kakšnem kontekstu SQL ukaza vstavljamo parameter s poljem in glede na to iz njega
sestavi SQL kodo. Tako je iz prvega polja sestavil (id, name, year) VALUES (123, 'Jim', 1978)
, medtem ko je drugega
pretvoril v obliko name = 'Jim', year = 1978
. Podrobneje se temu posvečamo v delu Namigi za sestavljanje SQL.
Brisanje podatkov (DELETE)
Za brisanje zapisov se uporablja SQL ukaz DELETE
. Primer s pridobivanjem števila izbrisanih vrstic:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Namigi za sestavljanje SQL
Namig je poseben nadomestni znak v SQL poizvedbi, ki pove, kako naj se vrednost parametra prepiše v SQL izraz:
Namig | Opis | Samodejno se uporabi |
---|---|---|
?name |
uporabi za vstavljanje imena tabele ali stolpca | – |
?values |
generira (key, ...) VALUES (value, ...) |
INSERT ... ? , REPLACE ... ? |
?set |
generira prirejanje key = value, ... |
SET ? , KEY UPDATE ? |
?and |
združi pogoje v polju z operatorjem AND |
WHERE ? , HAVING ? |
?or |
združi pogoje v polju z operatorjem OR |
– |
?order |
generira klavzulo ORDER BY |
ORDER BY ? , GROUP BY ? |
Za dinamično vstavljanje imen tabel in stolpcev v poizvedbo služi nadomestni znak ?name
. Nette Database poskrbi
za pravilno obdelavo identifikatorjev glede na konvencije dane baze podatkov (npr. zapiranje v povratne narekovaje v MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (v MySQL)
Opozorilo: simbol ?name
uporabljajte samo za imena tabel in stolpcev iz validiranih vnosov, sicer se
izpostavljate varnostnemu tveganju.
Drugih namigov običajno ni treba navajati, saj Nette pri sestavljanju SQL poizvedbe uporablja pametno samodejno zaznavanje
(glej tretji stolpec tabele). Lahko pa ga uporabite na primer v situaciji, ko želite združiti pogoje z OR
namesto
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'
Posebne vrednosti
Poleg običajnih skalarnih tipov (string, int, bool) lahko kot parametre posredujete tudi posebne vrednosti:
- datoteke:
fopen('image.gif', 'r')
vstavi binarno vsebino datoteke - datum in čas: objekti
DateTime
se pretvorijo v format baze podatkov - naštevni tipi: instance
enum
se pretvorijo v njihovo vrednost - SQL literali: ustvarjeni z
Connection::literal('NOW()')
se vstavijo neposredno v poizvedbo
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTime,
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
Pri bazah podatkov, ki nimajo nativne podpore za podatkovni tip datetime
(kot SQLite in Oracle), se
DateTime
pretvori v vrednost, določeno v konfiguraciji
baze podatkov z vnosom formatDateTime
(privzeta vrednost je U
– unix timestamp).
SQL literali
V nekaterih primerih morate kot vrednost navesti neposredno SQL kodo, ki pa se ne sme razumeti kot niz in ubežati. Za to
služijo objekti razreda Nette\Database\SqlLiteral
. Ustvarja jih metoda Connection::literal()
.
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
Ali alternativno:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
SQL literali lahko vsebujejo parametre:
$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)
Zaradi česar lahko ustvarjamo zanimive kombinacije:
$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')
Pridobivanje podatkov
Bližnjice za SELECT poizvedbe
Za poenostavitev nalaganja podatkov Connection
ponuja več bližnjic, ki kombinirajo klic query()
z naslednjim fetch*()
. Te metode sprejemajo enake parametre kot query()
, torej SQL poizvedbo in
neobvezne parametre. Popoln opis metod fetch*()
najdete spodaj.
fetch($sql, ...$params): ?Row |
Izvede poizvedbo in vrne prvo vrstico kot objekt Row |
fetchAll($sql, ...$params): array |
Izvede poizvedbo in vrne vse vrstice kot polje objektov Row |
fetchPairs($sql, ...$params): array |
Izvede poizvedbo in vrne asociativno polje, kjer prvi stolpec predstavlja ključ in drugi vrednost |
fetchField($sql, ...$params): mixed |
Izvede poizvedbo in vrne vrednost prvega polja iz prve vrstice |
fetchList($sql, ...$params): ?array |
Izvede poizvedbo in vrne prvo vrstico kot indeksirano polje |
Primer:
// fetchField() - vrne vrednost prve celice
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach
– iteracija čez vrstice
Po izvedbi poizvedbe se vrne objekt ResultSet, ki omogoča prehajanje rezultatov na
več načinov. Najlažji način za izvedbo poizvedbe in pridobitev vrstic je iteracija v zanki foreach
. Ta način je
pomnilniško najbolj varčen, saj vrača podatke postopoma in jih ne shranjuje vseh hkrati v pomnilnik.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
ResultSet
je mogoče iterirati samo enkrat. Če potrebujete iterirati večkrat, morate najprej
naložiti podatke v polje, na primer z metodo fetchAll()
.
fetch(): ?Row
Vrne vrstico kot objekt Row
. Če ni več vrstic, vrne null
. Premakne notranji kazalec na naslednjo
vrstico.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // naloži prvo vrstico
if ($row) {
echo $row->name;
}
fetchAll(): array
Vrne vse preostale vrstice iz ResultSet
kot polje objektov Row
.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // naloži vse vrstice
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
Vrne rezultate kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument določa ime stolpca, ki se uporabi kot vrednost:
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
Če navedemo samo prvi parameter, bo vrednost celotna vrstica, torej objekt Row
:
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
V primeru podvojenih ključev se uporabi vrednost iz zadnje vrstice. Pri uporabi null
kot ključa bo polje
indeksirano numerično od nič (potem do kolizij ne pride):
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
Alternativno lahko kot parameter navedete povratni klic (callback), ki bo za vsako vrstico vrnil bodisi samo vrednost ali par ključ-vrednost.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// Callback lahko vrne tudi polje s parom ključ & vrednost:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Vrne vrednost prvega polja iz trenutne vrstice. Če ni več vrstic, vrne null
. Premakne notranji kazalec na
naslednjo vrstico.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // naloži ime iz prve vrstice
fetchList(): ?array
Vrne vrstico kot indeksirano polje. Če ni več vrstic, vrne null
. Premakne notranji kazalec na naslednjo
vrstico.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Vrne število prizadetih vrstic zadnje poizvedbe UPDATE
ali DELETE
. Za SELECT
je to
število vrnjenih vrstic, vendar to morda ni znano – v takem primeru metoda vrne null
.
getColumnCount(): ?int
Vrne število stolpcev v ResultSet
.
Informacije o poizvedbah
Za namene razhroščevanja lahko pridobimo informacije o zadnji izvedeni poizvedbi:
echo $database->getLastQueryString(); // izpiše SQL poizvedbo
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // izpiše SQL poizvedbo
echo $result->getTime(); // izpiše čas izvedbe v sekundah
Za prikaz rezultata kot HTML tabele lahko uporabimo:
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet ponuja informacije o tipih stolpcev:
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column je tipa $type->type"; // npr. 'id je tipa int'
}
Dnevniško beleženje poizvedb
Lahko implementiramo lastno dnevniško beleženje poizvedb. Dogodek onQuery
je polje povratnih klicev (callback),
ki se pokličejo po vsaki izvedeni poizvedbi:
$database->onQuery[] = function ($database, $result) use ($logger) {
$logger->info('Poizvedba: ' . $result->getQueryString());
$logger->info('Čas: ' . $result->getTime());
if ($result->getRowCount() > 1000) {
$logger->warning('Velik nabor rezultatov: ' . $result->getRowCount() . ' vrstic');
}
};