SQLアクセス
Nette Databaseは2つの方法を提供します: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);
複数のパラメータがある場合、2つの記述方法があります。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条件
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
ソートは配列を使用して記述できます。キーにカラムを指定し、値は昇順でソートするかどうかを示すブール値になります:
$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)では、$database->getInsertId($sequenceId)
を使用してIDを生成するシーケンス名をパラメータとして指定する必要があります。
パラメータとして、ファイル、DateTimeオブジェクト、または列挙型などのspeciální hodnotyを渡すこともできます。
複数のレコードを一度に挿入する:
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
複数INSERTは、多くの個別のクエリではなく単一のデータベースクエリが実行されるため、はるかに高速です。
セキュリティ警告:
$values
として検証されていないデータを使用しないでください。潜在的なリスクについて理解してください。
データの更新 (UPDATE)
レコードを更新するには、SQLコマンド UPDATE
を使用します。
// 1つのレコードの更新
$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)
を構築し、2番目の配列を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クエリを構築する際に賢い自動検出を使用するため(表の3番目の列を参照)、指定する必要はありません。ただし、たとえば
AND
の代わりに OR
を使用して条件を結合したい場合などに使用できます:
$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タイムスタンプ)に変換されます。
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 |
クエリを実行し、最初のカラムがキー、2番目のカラムが値である連想配列を返します |
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
結果を連想配列として返します。最初の引数は配列のキーとして使用されるカラム名を指定し、2番目の引数は値として使用されるカラム名を指定します:
$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 は型 $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');
}
};