Прямий SQL
Ви можете працювати з базою даних Nette двома способами: безпосередньо писати SQL-запити (прямий доступ) або дозволити SQL генеруватися автоматично(Explorer Access). Direct Access дозволяє безпечно створювати запити, зберігаючи повний контроль над їх структурою.
Інформацію про створення з'єднання та його налаштування дивіться на окремій сторінці.
Базові запити
Метод 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-ін'єкцій
Чому важливо використовувати параметризовані запити? Тому що вони захищають вас від атак SQL-ін'єкцій, коли зловмисники можуть вставляти шкідливі SQL-команди для маніпулювання або доступу до даних бази даних.
**Ніколи не вставляйте змінні безпосередньо в SQL-запит! Завжди використовуйте параметризовані запити, щоб захиститися від SQL-ін'єкцій.
// НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкцій
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Безпечний параметризований запит
$database->query('SELECT * FROM users WHERE name = ?', $name);
Обов'язково ознайомтеся з потенційними ризиками безпеки.
Методи запитів
Де Умови
Ви можете записати умови 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%'
Особливі випадки, такі як null
значення або масиви, обробляються
автоматично:
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // використовує оператор =
'category_id' => [1, 2, 3], // використовує IN
'description' => null, // uses IS NULL
]);
// WHERE `name` = 'Ноутбук' 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, // uses IS NOT NULL
'id' => [], // пропущено
]);
// WHERE `name` <> 'Ноутбук' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
За замовчуванням умови об'єднуються за допомогою оператора AND
.
Ви можете змінити цю поведінку за допомогою заповнювача ?or.
Впорядкувати за правилами
Речення ORDER BY
можна визначити як масив, де ключами є стовпці, а
значеннями – булеві числа, що вказують на порядок зростання:
$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()
повертає ідентифікатор останнього вставленого
рядка. Для деяких баз даних (наприклад, PostgreSQL) ви повинні вказати ім'я
послідовності за допомогою $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)
// ПРИ ОНОВЛЕННІ ДВІЧНОГО КЛЮЧА `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('file.png', '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).
Літерали 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', ...]
// Зворотний виклик також може повертати масив з парою ключ & значення:
$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 is of type $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');
}
};