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クエリのショートカット

データ取得を簡略化するために、Connectionquery()の呼び出しとそれに続く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');
	}
};
バージョン: 4.0