Πρόσβαση 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 injection
$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
. Αυτό
μπορεί να αλλάξει χρησιμοποιώντας το placeholder
?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), είναι απαραίτητο να
καθορίσετε ως παράμετρο το όνομα της ακολουθίας (sequence) από την οποία θα
δημιουργηθεί το ID χρησιμοποιώντας $database->getInsertId($sequenceId)
.
Ως παραμέτρους μπορούμε επίσης να περάσουμε speciální hodnoty όπως αρχεία, αντικείμενα DateTime ή τύπους enum.
Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα:
$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
. Αυτό το
εξετάζουμε λεπτομερέστερα στην ενότητα Hints για
τη σύνταξη SQL.
Διαγραφή δεδομένων (DELETE)
Για τη διαγραφή εγγραφών, χρησιμοποιείται η εντολή SQL DELETE
.
Παράδειγμα με λήψη του αριθμού των διαγραμμένων γραμμών:
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Hints για τη σύνταξη SQL
Ένα hint είναι ένα ειδικό placeholder στο ερώτημα SQL που λέει πώς πρέπει να μεταγραφεί η τιμή της παραμέτρου σε έκφραση SQL:
Hint | Περιγραφή | Χρησιμοποιείται αυτόματα |
---|---|---|
?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 ? |
Για τη δυναμική εισαγωγή ονομάτων πινάκων και στηλών στο ερώτημα,
χρησιμοποιείται το placeholder ?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
μόνο για
ονόματα πινάκων και στηλών από επικυρωμένες εισόδους, διαφορετικά
εκτίθεστε σε κίνδυνο
ασφαλείας.
Τα υπόλοιπα hints συνήθως δεν χρειάζεται να αναφέρονται, καθώς το 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: οι παρουσίες
enum
μετατρέπονται στην τιμή τους - SQL literals: δημιουργημένα με
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 Literals
Σε ορισμένες περιπτώσεις, πρέπει να αναφέρετε απευθείας κώδικα SQL ως
τιμή, ο οποίος όμως δεν πρέπει να θεωρηθεί ως συμβολοσειρά και να
υποστεί escaping. Για αυτό χρησιμεύουν τα αντικείμενα της κλάσης
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 literals μπορούν να περιέχουν παραμέτρους:
$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
Εναλλακτικά, μπορείτε να δώσετε ως παράμετρο ένα callback, το οποίο θα επιστρέφει για κάθε γραμμή είτε την ίδια την τιμή, είτε ένα ζεύγος κλειδιού-τιμής.
$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' (id is of type int)
}
Καταγραφή ερωτημάτων
Μπορούμε να υλοποιήσουμε τη δική μας καταγραφή ερωτημάτων. Το συμβάν
onQuery
είναι ένας πίνακας callbacks που καλούνται μετά από κάθε
εκτελεσμένο ερώτημα:
$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');
}
};