SQL підхід
Nette Database пропонує два шляхи: ви можете писати SQL-запити самостійно (SQL підхід), або дозволити генерувати їх автоматично (див. Explorer). SQL підхід дає вам повний контроль над запитами і при цьому забезпечує їх безпечне складання.
Деталі щодо підключення та конфігурації бази даних знайдете в розділі Підключення та конфігурація.
Базові запити
Для запитів до бази даних служить метод query()
. Він повертає
об'єкт ResultSet, який
представляє результат запиту. У разі збою метод викине виняток. Результат запиту можна
перебирати за допомогою циклу foreach
, або використати одну з допоміжних функцій.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
Для безпечного вставлення значень у SQL-запити використовуємо параметризовані запити. Nette Database робить їх максимально простими – достатньо після SQL-запиту додати кому та значення:
$database->query('SELECT * FROM users WHERE name = ?', $name);
При використанні кількох параметрів у вас є два варіанти запису. Або ви можете “розбавляти” SQL-запит параметрами:
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
Або написати спочатку весь SQL-запит, а потім додати всі параметри:
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Захист від SQL injection
Чому важливо використовувати параметризовані запити? Тому що вони захищають вас від атаки під назвою SQL injection, під час якої зловмисник міг би підсунути власні SQL-команди і таким чином отримати або пошкодити дані в базі даних.
Ніколи не вставляйте змінні безпосередньо в SQL-запит! Завжди використовуйте параметризовані запити, які захистять вас від SQL injection.
// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Безпечний параметризований запит
$database->query('SELECT * FROM users WHERE name = ?', $name);
Ознайомтеся з можливими ризиками безпеки.
Техніки запитів
Умови WHERE
Умови WHERE можна записати як асоціативний масив, де ключі – це назви стовпців, а значення – дані для порівняння. Nette Database автоматично вибере найбільш підходящий SQL-оператор залежно від типу значення.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
У ключі можна також явно вказати оператор для порівняння:
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // використовує оператор >
'name LIKE' => '%John%', // використовує оператор LIKE
'email NOT LIKE' => '%example.com%', // використовує оператор NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette автоматично обробляє спеціальні випадки, такі як значення
null
або масиви.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // використовує оператор =
'category_id' => [1, 2, 3], // використовує IN
'description' => null, // використовує IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
Для негативних умов використовуйте оператор NOT
:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // використовує оператор <>
'category_id NOT' => [1, 2, 3], // використовує NOT IN
'description NOT' => null, // використовує IS NOT NULL
'id' => [], // пропускається
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
Для об'єднання умов використовується оператор AND
. Це можна
змінити за допомогою заповнювача ?or.
Правила ORDER BY
Сортування ORDER BY
можна записати за допомогою масиву. У ключах
вказуємо стовпці, а значенням буде boolean, що визначає, чи сортувати за
зростанням:
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // за зростанням
'name' => false, // за спаданням
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Вставка даних (INSERT)
Для вставки записів використовується SQL-команда INSERT
.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
Метод getInsertId()
повертає ID останнього вставленого рядка. У
деяких базах даних (наприклад, PostgreSQL) необхідно як параметр вказати
назву послідовності, з якої має генеруватися ID, за допомогою
$database->getInsertId($sequenceId)
.
Як параметри можна передавати і #спеціальні значення, такі як файли, об'єкти DateTime або перелічувані типи.
Вставка кількох записів одночасно:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
Багаторазовий INSERT набагато швидший, оскільки виконується єдиний запит до бази даних замість багатьох окремих.
Попередження щодо безпеки: Ніколи не використовуйте як
$values
невалідовані дані. Ознайомтеся з можливими ризиками.
Оновлення даних (UPDATE)
Для оновлення записів використовується SQL-команда UPDATE
.
// Оновлення одного запису
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
Кількість зачеплених рядків поверне $result->getRowCount()
.
Для UPDATE можна використовувати оператори +=
та -=
:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // інкрементація login_count
], 1);
Приклад вставки або оновлення запису, якщо він вже існує.
Використаємо техніку 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
Зверніть увагу, що Nette Database розпізнає, в якому контексті SQL-команди
вставляється параметр з масивом, і відповідно до цього складає з нього
SQL-код. Так, з першого масиву він склав (id, name, year) VALUES (123, 'Jim', 1978)
,
тоді як другий перетворив на вигляд name = 'Jim', year = 1978
. Детальніше
про це йдеться в розділі Підказки для побудови
SQL.
Видалення даних (DELETE)
Для видалення записів використовується SQL-команда DELETE
.
Приклад з отриманням кількості видалених рядків:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Підказки для побудови SQL
Підказка – це спеціальний заповнювач у SQL-запиті, який вказує, як значення параметра має бути перетворено на SQL-вираз:
Підказка | Опис | Автоматично використовується |
---|---|---|
?name |
використовується для вставки назви таблиці або стовпця | – |
?values |
генерує (key, ...) VALUES (value, ...) |
INSERT ... ? , REPLACE ... ? |
?set |
генерує присвоєння key = value, ... |
SET ? , KEY UPDATE ? |
?and |
об'єднує умови в масиві оператором AND |
WHERE ? , HAVING ? |
?or |
об'єднує умови в масиві оператором OR |
– |
?order |
генерує умову ORDER BY |
ORDER BY ? , GROUP BY ? |
Для динамічного вставлення назв таблиць та стовпців у запит служить
заповнювач ?name
. Nette Database подбає про правильну обробку
ідентифікаторів відповідно до конвенцій даної бази даних (наприклад,
взяття у зворотні апострофи в MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (у MySQL)
Попередження: символ ?name
використовуйте лише для назв
таблиць та стовпців з валідованих вхідних даних, інакше ви наражаєтеся
на ризик безпеки.
Інші підказки зазвичай не потрібно вказувати, оскільки Nette
використовує розумну автодетекцію при складанні SQL-запиту (див. третій
стовпець таблиці). Але ви можете її використати, наприклад, у ситуації,
коли хочете об'єднати умови за допомогою OR
замість 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'
Спеціальні значення
Крім звичайних скалярних типів (string, int, bool), ви можете передавати як параметри і спеціальні значення:
- файли:
fopen('image.gif', 'r')
вставить бінарний вміст файлу - дата та час: об'єкти
DateTime
перетворяться на формат бази даних - перелічувані типи: екземпляри
enum
перетворяться на їхнє значення - SQL-літерали: створені за допомогою
Connection::literal('NOW()')
вставляться безпосередньо в запит
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTime,
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
У базах даних, які не мають нативної підтримки для типу даних
datetime
(як SQLite та Oracle), DateTime
перетворюється на значення,
визначене в конфігурації бази даних
елементом formatDateTime
(значення за замовчуванням – U
– unix
timestamp).
SQL-літерали
У деяких випадках потрібно вказати як значення безпосередньо SQL-код,
який, однак, не повинен розглядатися як рядок і екрануватися. Для цього
служать об'єкти класу Nette\Database\SqlLiteral
. Їх створює метод
Connection::literal()
.
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
Або альтернативно:
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
SQL-літерали можуть містити параметри:
$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)
Завдяки чому можна створювати цікаві комбінації:
$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')
Отримання даних
Скорочення для SELECT-запитів
Для спрощення завантаження даних Connection
пропонує кілька
скорочень, які комбінують виклик query()
з наступним fetch*()
.
Ці методи приймають ті самі параметри, що й query()
, тобто SQL-запит
та необов'язкові параметри. Повний опис методів fetch*()
знайдете нижче.
fetch($sql, ...$params): ?Row |
Виконує запит і повертає перший рядок як об'єкт Row |
fetchAll($sql, ...$params): array |
Виконує запит і повертає всі рядки як масив об'єктів Row |
fetchPairs($sql, ...$params): array |
Виконує запит і повертає асоціативний масив, де перший стовпець представляє ключ, а другий – значення |
fetchField($sql, ...$params): mixed |
Виконує запит і повертає значення першого поля з першого рядка |
fetchList($sql, ...$params): ?array |
Виконує запит і повертає перший рядок як індексований масив |
Приклад:
// fetchField() - повертає значення першої комірки
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach
– ітерація по рядках
Після виконання запиту повертається об'єкт ResultSet, який дозволяє
перебирати результати кількома способами. Найпростіший спосіб
виконати запит і отримати рядки – це ітерація в циклі foreach
. Цей
спосіб є найбільш економним з точки зору пам'яті, оскільки повертає
дані поступово і не зберігає їх усі в пам'яті одночасно.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
ResultSet
можна ітерувати лише один раз. Якщо вам потрібно
ітерувати повторно, ви повинні спочатку завантажити дані в масив,
наприклад, за допомогою методу fetchAll()
.
fetch(): ?Row
Повертає рядок як об'єкт Row
. Якщо більше немає рядків, повертає
null
. Пересуває внутрішній вказівник на наступний рядок.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // читає перший рядок
if ($row) {
echo $row->name;
}
fetchAll(): array
Повертає всі рядки, що залишилися, з ResultSet
як масив об'єктів
Row
.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // читає всі рядки
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
Повертає результати як асоціативний масив. Перший аргумент визначає назву стовпця, який буде використаний як ключ у масиві, другий аргумент визначає назву стовпця, який буде використаний як значення:
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
Якщо вказати лише перший параметр, значенням буде весь рядок, тобто
об'єкт Row
:
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
У разі дублювання ключів використовується значення з останнього
рядка. При використанні null
як ключа масив буде індексовано
нумерично з нуля (тоді колізій не виникає):
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
Альтернативно, ви можете вказати як параметр колбек, який для кожного рядка повертатиме або саме значення, або пару ключ-значення.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// Callback також може повертати масив із парою ключ-значення:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Повертає значення першого поля з поточного рядка. Якщо більше немає
рядків, повертає null
. Пересуває внутрішній вказівник на
наступний рядок.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // читає ім'я з першого рядка
fetchList(): ?array
Повертає рядок як індексований масив. Якщо більше немає рядків,
повертає null
. Пересуває внутрішній вказівник на
наступний рядок.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Повертає кількість зачеплених рядків останнім запитом UPDATE
або DELETE
. Для SELECT
це кількість повернутих рядків, але вона
може бути невідомою – у такому випадку метод поверне null
.
getColumnCount(): ?int
Повертає кількість стовпців у ResultSet
.
Інформація про запити
Для цілей налагодження ми можемо отримати інформацію про останній виконаний запит:
echo $database->getLastQueryString(); // виводить SQL-запит
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // виводить SQL-запит
echo $result->getTime(); // виводить час виконання в секундах
Для відображення результату у вигляді HTML-таблиці можна використати:
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet пропонує інформацію про типи стовпців:
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column має тип $type->type"; // напр. 'id має тип int'
}
Логування запитів
Ми можемо реалізувати власне логування запитів. Подія onQuery
–
це масив колбеків, які викликаються після кожного виконаного запиту:
$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');
}
};