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 NOT' => [],                 // 飛ばされます
]);
// WHERE `name` != 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL

既定では条件は AND 演算子でつながれます。これは ?or のプレースホルダで変えられます。

ORDER BY の規則

ORDER BY の句は配列で書けます。キーに列を指定し、真偽値で昇順(true)か降順(false)かを示します。

$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 オブジェクト、enum 型も渡せます。

複数のレコードを一度に挿入します。

$database->query('INSERT INTO users ?', [
	['name' => 'User 1', 'email' => 'user1@mail.com'],
	['name' => 'User 2', 'email' => 'user2@mail.com'],
]);

複数レコードの INSERT はずっと速くなります。多くの個別のクエリの代わりに、データベースのクエリが 1 つだけ実行されるからです。

セキュリティに関する注意: 検証していないデータを $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) を組み立て、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') はファイルの中身をバイナリとして入れます
  • 日付と時刻: DateTimeInterface のオブジェクトはデータベースの書式に変換されます
  • enum 型: enum のインスタンスはその値に変換されます
  • SQL のリテラル: Connection::literal('NOW()') で作られ、そのままクエリに入ります
$database->query('INSERT INTO articles ?', [
	'title' => 'My Article',
	'published_at' => new DateTimeImmutable, // または new DateTime
	'content' => fopen('image.png', 'r'),
	'state' => Status::Draft,
]);

datetime のデータ型を本来は持たないデータベース(SQLite や Oracle など)では、DateTimeDateTimeImmutable のオブジェクトは、データベースの設定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 オブジェクトとして、なければ null を返します。
fetchAll($sql, ...$params): array クエリを実行し、すべての行を Row オブジェクトの配列として返します。
fetchPairs($sql, ...$params): array クエリを実行し、連想配列(キー ⇒ 値の組)を返します。
fetchField($sql, ...$params): mixed クエリを実行し、最初の行の最初の列の値を返します。
fetchList($sql, ...$params): ?array クエリを実行し、最初の行を添字の配列として、なければ null を返します。

例です。

// fetchField() - 最初のセルの値を返します
$count = $database->query('SELECT COUNT(*) FROM articles')
	->fetchField();

foreach – 行を順に回す

クエリを実行すると ResultSetオブジェクトが返され、結果をいくつかの方法で回せます。クエリを実行して行を取り出すいちばん簡単な方法は、foreach のループで回すことです。この方法はメモリをもっとも節約します。データを 1 行ずつ取り出し、結果全体を一度にメモリへ読み込まないからです。

$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

結果を連想配列として返します。第 1 引数はキーとして使う列、第 2 引数は値として使う列を指定します。

$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

第 1 パラメータ($key)だけを渡すと、行全体(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(); // 最初の行から name を読み込みます

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"; // たとえば 'id is of type 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');
	}
};