Database Explorer
Explorer はデータベースを直感的に、しかも効率よく扱う方法を提供します。テーブルどうしの関係を自動的に扱い、クエリを最適化するので、あなたはアプリケーションの論理に集中できます。設定なしですぐに動きます。SQL のクエリを完全に思いどおりにしたいなら、SQL アプローチを使えます。
- データの扱いが自然で分かりやすい
- 必要なデータだけを取ってくる最適化された SQL のクエリを生成する
- JOIN のクエリを書かずに関連するデータへ簡単にアクセスできる
- 設定もエンティティの生成もなしにすぐ動く
Explorer を使う仕事は、Nette\Database\Explorerオブジェクトの
table() メソッドを呼ぶところから始まります(データベース接続の設定については 接続と設定をご覧ください)。
$books = $explorer->table('book'); // 'book' はテーブルの名前です
このメソッドは SQL のクエリを表す Selectionオブジェクトを返します。このオブジェクトにはさらにメソッドをつないで、結果を絞り込んだり並べ替えたりできます。クエリが組み立てられて実行されるのは、たとえば
foreach で回すなど、データが実際に求められたときだけです。それぞれの行は ActiveRowオブジェクトが表します。
foreach ($books as $book) {
echo $book->title; // 'title' 列を出力します
echo $book->author_id; // 'author_id' 列を出力します
}
Explorer はテーブルどうしの関係を扱う仕事を大いに簡単にします。次の例は、関連するテーブル(本とその著者)のデータをどれだけ簡単に出力できるかを示します。JOIN のクエリを書く必要がないことに注目してください。Nette がそれを生成してくれます。
$books = $explorer->table('book');
foreach ($books as $book) {
echo 'Book: ' . $book->title;
echo 'Author: ' . $book->author->name; // 'author' テーブルへの JOIN を作ります
}
Nette Database Explorer は効率が最大になるようにクエリを最適化します。上の例は、本を 10 冊扱おうと 10,000 冊扱おうと、SELECT のクエリを 2 つしか実行しません。
さらに Explorer は、コードでどの列が使われているかを追い、データベースからはそれだけを取ってきて、いっそう性能を節約します。この振る舞いは完全に自動で、状況に合わせて変わります。あとからコードを変えて別の列を使うようにすれば、Explorer はクエリを自動的に合わせます。何かを設定したり、どの列が要るかを考えたりする必要はありません。それは Nette に任せてください。
絞り込みと並べ替え
Selection
クラスは、選び出すデータを絞り込んだり並べ替えたりするメソッドを提供します。
where($condition, ...$params) |
WHERE の条件を足します。複数の条件は AND でつながれます |
whereOr(array $conditions) |
OR でつながれる WHERE の条件のまとまりを足します |
wherePrimary($value) |
主キーをもとにした WHERE の条件を足します |
order($columns, ...$params) |
ORDER BY による並べ替えを設定します |
select($columns, ...$params) |
どの列を取ってくるかを指定します |
limit($limit, $offset = null) |
行の数を制限し(LIMIT)、必要なら OFFSET も設定します |
page($page, $itemsPerPage, &$numOfPages = null) |
ページ分けを設定します |
group($columns, ...$params) |
行をまとめます(GROUP BY) |
having($condition, ...$params) |
まとめた行を絞り込む HAVING の条件を足します |
メソッドはつなげて書けます(いわゆる fluent
インターフェース)。$table->where(...)->order(...)->limit(...) のようにです。
これらのメソッドでは、関連するテーブルのデータにアクセスする特別な書き方も使えます。
エスケープと識別子
これらのメソッドはパラメータを自動的にエスケープし、識別子(テーブル名と列名)を引用符で囲むので、SQL インジェクションを防ぎます。正しく動くようにするには、いくつかの決まりを守る必要があります。
- キーワード、関数名、プロシージャ名などは大文字で書きます。
- 列名とテーブル名は小文字で書きます。
- 文字列はいつもパラメータとして渡します。
where('name = ' . $name); // 致命的な弱点: SQL インジェクション
where('name LIKE "%search%"'); // 誤り: 自動的な引用を難しくします
where('name LIKE ?', '%search%'); // 正しい: 値をパラメータとして渡します
where('name like ?', $name); // 誤り: `name` `like` ? を生成します
where('name LIKE ?', $name); // 正しい: `name` LIKE ? を生成します
where('LOWER(name) = ?', $value);// 正しい: LOWER(`name`) = ?
where (string|array $condition, …$parameters): static
WHERE の条件で結果を絞り込みます。その強みは、さまざまな型の値を賢く扱い、ふさわしい SQL の演算子を自動的に選ぶところにあります。
基本の使い方です。
$table->where('id', $value); // WHERE `id` = 123
$table->where('id > ?', $value); // WHERE `id` > 123
$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow'
ふさわしい演算子が自動的に見分けられるので、さまざまな特別な場合を自分で扱う必要はありません。Nette が片付けてくれます。
$table->where('id', 1); // WHERE `id` = 1
$table->where('id', null); // WHERE `id` IS NULL
$table->where('id', [1, 2, 3]); // WHERE `id` IN (1, 2, 3)
// 演算子なしのプレースホルダ ? も使えます:
$table->where('id ?', 1); // WHERE `id` = 1
このメソッドは否定の条件と空の配列も正しく扱います。
$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- 何も見つかりません
$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- すべてが見つかります
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- すべてが見つかります
// $table->where('NOT id ?', $ids); // 注意: この書き方には対応していません
パラメータとして別のテーブルのクエリの結果を渡して、副問い合わせを作ることもできます。
// WHERE `id` IN (SELECT `id` FROM `tableName`)
$table->where('id', $explorer->table($tableName));
// WHERE `id` IN (SELECT `col` FROM `tableName`)
$table->where('id', $explorer->table($tableName)->select('col'));
条件は配列でも渡せます。その要素は AND でつながれます。
// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`)
$table->where([
'price_final < price_original',
'stock_count > min_stock',
]);
配列では キー ⇒ 値 の組も使え、Nette はやはり正しい演算子を自動的に選びます。
// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3))
$table->where([
'status' => 'active',
'id' => [1, 2, 3],
]);
配列の中では、SQL の式をプレースホルダや複数のパラメータと組み合わせられます。演算子をきっちり定めたい込み入った条件に向いています。
// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
'age > ?' => 18,
'ROUND(score, ?) > ?' => [2, 75.5], // 2 つのパラメータを配列で渡します
]);
where() を何度も呼ぶと、条件は自動的に AND でつながれます。
whereOr (array $parameters): static
where() と同じように条件を足しますが、OR でつなぎます。
// WHERE (`status` = 'active') OR (`deleted` = 1)
$table->whereOr([
'status' => 'active',
'deleted' => true,
]);
ここでもより込み入った式を使えます。
// WHERE (`price` > 1000) OR (`price_with_tax` > 1500)
$table->whereOr([
'price > ?' => 1000,
'price_with_tax > ?' => 1500,
]);
wherePrimary (mixed $key): static
テーブルの主キーに対する条件を足します。
// WHERE `id` = 123
$table->wherePrimary(123);
// WHERE `id` IN (1, 2, 3)
$table->wherePrimary([1, 2, 3]);
テーブルが複合主キー(たとえば
foo_id、bar_id)を持つなら、配列で渡します。
// WHERE `foo_id` = 1 AND `bar_id` = 5
$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch();
// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3))
$table->wherePrimary([
['foo_id' => 1, 'bar_id' => 5],
['foo_id' => 2, 'bar_id' => 3],
])->fetchAll();
order (string $columns, …$parameters): static
行が返される順序を指定します。ひとつまたは複数の列で、昇順または降順に、あるいは独自の式に従って並べ替えられます。
$table->order('created'); // ORDER BY `created`
$table->order('created DESC'); // ORDER BY `created` DESC
$table->order('priority DESC, created'); // ORDER BY `priority` DESC, `created`
$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC
select (string $columns, …$parameters): static
データベースから返される列を指定します。既定では、Nette Database Explorer
はコードで実際に使われている列だけを返します。特定の式を取り出す必要があるときに
select() メソッドを使います。
// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date`
$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y');
AS で定義した別名は、そのあと ActiveRow
オブジェクトのプロパティとして使えます。
foreach ($table as $row) {
echo $row->formatted_date; // 別名にアクセスします
}
limit (?int $limit, ?int $offset = null): static
返される行の数を制限し(LIMIT)、必要なら開始位置も設定できます。
$table->limit(10); // LIMIT 10(最初の 10 行を返します)
$table->limit(10, 20); // LIMIT 10 OFFSET 20
ページ分けには page() メソッドのほうが向いています。
page (int $page, int $itemsPerPage, &$numOfPages = null): static
結果のページ分けを楽にします。ページ番号(1 から始まります)と 1 ページあたりの項目の数を受け取ります。必要なら、ページの総数が入る変数への参照も渡せます。
$numOfPages = null;
$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages);
echo "Total pages: $numOfPages";
group (string $columns, …$parameters): static
指定した列に従って行をまとめます(GROUP BY)。ふつうは集約関数と組み合わせて使います。
// カテゴリごとの商品の数を数えます
$table->select('category_id, COUNT(*) AS count')
->group('category_id');
having (string $having, …$parameters): static
まとめた行を絞り込む条件を設定します(HAVING)。group()
メソッドや集約関数と組み合わせて使えます。
// 商品が 100 を超えるカテゴリを見つけます
$table->select('category_id, COUNT(*) AS count')
->group('category_id')
->having('count > ?', 100);
データの読み出し
データベースからデータを読み出すのに、役に立つメソッドがいくつかあります。
foreach ($table as $key => $row) |
すべての行を回します。$key は主キーの値、$row は ActiveRow
オブジェクトです |
$row = $table->get($key) |
主キーでひとつの行を返します |
$row = $table->fetch() |
今の行を返し、ポインタを次へ進めます |
$array = $table->fetchPairs() |
結果から連想配列を作ります |
$array = $table->fetchAll() |
すべての行を配列として返します |
count($table) |
Selection オブジェクトの行の数を返します |
ActiveRowオブジェクトは読み取り専用です。つまりそのプロパティの値は変えられません。この制限はデータの整合性を守り、思いがけない副作用を防ぎます。データはデータベースから読み込まれるものであり、変更ははっきりと、しかも制御された形で行うべきだからです。
foreach – すべての行を回す
クエリを実行して行を取り出すいちばん簡単な方法は、foreach
のループで回すことです。SQL のクエリは自動的に実行されます。
$books = $explorer->table('book');
foreach ($books as $key => $book) {
// $key は主キーの値、$book は ActiveRow です
echo "$book->title ({$book->author->name})";
}
get ($key): ?ActiveRow
SQL のクエリを実行し、主キーで行を返します。存在しなければ null を返します。
$book = $explorer->table('book')->get(123); // ID 123 の ActiveRow か null を返します
if ($book) {
echo $book->title;
}
fetch(): ?ActiveRow
今の行を返し、内部のポインタを次へ進めます。行がもうなければ null
を返します。
$books = $explorer->table('book');
while ($book = $books->fetch()) {
$this->processBook($book);
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
結果を連想配列として返します。第 1 引数は配列のキーとして使う列の名前、第 2 引数は値として使う列の名前を指定します。
$authors = $explorer->table('author')->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
第 1 パラメータだけを渡すと、値は行全体、つまり ActiveRow
オブジェクトになります。
$authors = $explorer->table('author')->fetchPairs('id');
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]
キーが重なった場合は最後の行の値が使われます。キーに null
を使うと、配列はゼロから始まる添字になります(そうすれば衝突は起きません)。
$authors = $explorer->table('author')->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
代わりにパラメータとしてコールバックを渡せます。それは行ごとにひとつの値か、キーと値の組を返します。
$titles = $explorer->table('book')
->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// ['First Book (John Novak)', ...]
// コールバックはキーと値の組の配列を返すこともできます:
$titles = $explorer->table('book')
->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['First Book' => 'John Novak', ...]
fetchAll(): array
すべての行を ActiveRow
オブジェクトの連想配列として返します。キーは主キーの値です。
$allBooks = $explorer->table('book')->fetchAll();
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]
count(): int
パラメータなしの count() メソッドは Selection
オブジェクトの行の数を返します。
$table->where('category', 1);
$count = $table->count();
$count = count($table); // 別の書き方
注意: パラメータ付きの count() はデータベースで COUNT
の集約関数を実行します。下をご覧ください。
ActiveRow::toArray(): array
ActiveRow
オブジェクトを連想配列に変えます。キーは列の名前、値は対応するデータです。
$book = $explorer->table('book')->get(1);
$bookArray = $book->toArray();
// $bookArray は ['id' => 1, 'title' => '...', 'author_id' => ..., ...] になります
集約
Selection クラスは、集約関数(COUNT、SUM、MIN、MAX、AVG
など)を簡単に実行するメソッドを提供します。
count($expr) |
行の数を数えます |
min($expr) |
列の最小値を返します |
max($expr) |
列の最大値を返します |
sum($expr) |
列の値の合計を返します |
aggregation($function) |
AVG() や GROUP_CONCAT() など、任意の集約関数を使えます |
count (string $expr): int
COUNT 関数を使う SQL のクエリを実行し、その結果を返します。ある条件に合う行がいくつあるかを調べるのに使います。
$count = $table->count('*'); // SELECT COUNT(*) FROM `table`
$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table`
注意: パラメータなしの count()は Selection
オブジェクトの行の数を返すだけです。
min (string $expr) と max(string $expr)
min() と max() メソッドは、指定した列や式の最小値と最大値を返します。
// SELECT MAX(`price`) FROM `products` WHERE `active` = 1
$maxPrice = $products->where('active', true)
->max('price');
sum (string $expr): mixed
指定した列や式の値の合計を返します。
// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1
$totalPrice = $products->where('active', true)
->sum('price * items_in_stock');
aggregation (string $function, ?string $groupFunction = null): mixed
任意の集約関数を実行できます。
// カテゴリの商品の平均価格
$avgPrice = $products->where('category_id', 1)
->aggregation('AVG(price)');
// 商品のタグをひとつの文字列につなぎます
$tags = $products->where('id', 1)
->aggregation('GROUP_CONCAT(tag.name) AS tags')
->fetch()
->tags;
すでに何らかの集約関数とまとめの結果になっているもの(たとえばまとめた行に対する
SUM(value))をさらに集約する必要があるなら、その中間の結果に当てる集約関数を第 2
引数で指定します。
// カテゴリごとに在庫の商品の合計金額を計算し、それらの金額を足し合わせます。
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
->group('category_id')
->aggregation('SUM(category_total)', 'SUM');
この例では、まずカテゴリごとに商品の合計金額を計算し(SUM(price * stock) AS category_total)、結果を
category_id でまとめます。そのあと aggregation('SUM(category_total)', 'SUM')
でこれらの中間の合計 category_total を足し合わせます。第 2 引数の 'SUM'
は、中間の結果に SUM 関数を当てることを指定しています。
挿入、更新、削除
Nette Database Explorer
はデータの挿入、更新、削除を簡単にします。ここで挙げるメソッドはすべて、エラーの場合に
Nette\Database\DriverException を投げます。
Selection::insert (iterable $data)
テーブルに新しいレコードを挿入します。
ひとつのレコードを挿入する:
新しいレコードを連想配列か、反復できるオブジェクト(フォームで使われる ArrayHash
など)として渡します。キーはテーブルの列の名前に対応します。
テーブルに主キーが定義されていれば、このメソッドは ActiveRow
オブジェクトを返します。それはデータベースの側で起きた変更(トリガー、列の既定値、自動採番の計算)を映すために、データベースから読み直されます。これでデータの整合性が保たれ、オブジェクトはいつもデータベースの今のデータを持ちます。テーブルに主キーがなければ、行を特定できないのでこのメソッドは
null を返します。
$row = $explorer->table('users')->insert([
'name' => 'John Doe',
'email' => 'john.doe@example.com',
]);
// $row は ActiveRow のインスタンスで、挿入された行の完全なデータを持ちます。
// 自動的に生成された ID や、トリガーによる変更も含みます
echo $row->id; // 新しく挿入されたユーザーの ID を出力します
echo $row->created_at; // トリガーで設定されていれば作成時刻を出力します
複数のレコードを一度に挿入する:
insert() メソッドは、ひとつの SQL
のクエリで複数のレコードを挿入できます。その場合は挿入された行の数を返します。
$insertedRows = $explorer->table('users')->insert([
[
'name' => 'John',
'year' => 1994,
],
[
'name' => 'Jack',
'year' => 1995,
],
]);
// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995)
// $insertedRows は 2 になります
パラメータとして、データを選び出した Selection オブジェクトも渡せます。
$newUsers = $explorer->table('potential_users')
->where('approved', 1)
->select('name, email');
$insertedRows = $explorer->table('users')->insert($newUsers);
特別な値を挿入する:
値としてファイル、DateTime オブジェクト、SQL のリテラルも渡せます。
$explorer->table('users')->insert([
'name' => 'John',
'created_at' => new DateTime, // データベースの書式に変換します
'avatar' => fopen('image.jpg', 'rb'), // ファイルの中身をバイナリとして入れます
'uuid' => $explorer::literal('UUID()'), // UUID() 関数を呼びます
]);
Selection::update (iterable $data): int
指定した絞り込みに従ってテーブルの行を更新します。実際に変わった行の数を返します。
変える列を連想配列か、反復できるオブジェクト(フォームで使われる ArrayHash
など)として渡します。キーはテーブルの列の名前に対応します。
$affected = $explorer->table('users')
->where('id', 10)
->update([
'name' => 'John Smith',
'year' => 1994,
]);
// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10
数値を変えるには += と -= の演算子を使えます。
$explorer->table('users')
->where('id', 10)
->update([
'points+=' => 1, // 'points' 列の値を 1 増やします
'coins-=' => 1, // 'coins' 列の値を 1 減らします
]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10
Selection::delete(): int
指定した絞り込みに従ってテーブルの行を削除します。削除された行の数を返します。
$count = $explorer->table('users')
->where('id', 10)
->delete();
// DELETE FROM `users` WHERE `id` = 10
update() や delete() を呼ぶときは、変更/削除する行を
where() で指定するのを忘れないでください。where()
を使わないと、その操作はテーブル全体に対して行われます。
ActiveRow::update (iterable $data): bool
ActiveRow
オブジェクトが表すデータベースの行のデータを更新します。更新するデータを持つ反復できるものを受け取ります(キーは列の名前です)。数値を変えるには
+= と -= の演算子を使えます。
更新のあと、ActiveRow
はデータベースの側で起きた変更(トリガーなど)を映すために自動的に読み直されます。このメソッドは、実際にデータが変わったときにだけ
true を返します。
$article = $explorer->table('article')->get(1);
$article->update([
'views += 1', // 閲覧数を増やします
]);
echo $article->views; // 今の閲覧数を出力します
このメソッドはデータベースの特定の 1 行だけを更新します。複数の行をまとめて更新するには Selection::update()メソッドを使ってください。
ActiveRow::delete(): int
ActiveRow
オブジェクトが表す行をデータベースから削除します。削除された行の数を返します。それは
1 のはずです。
$book = $explorer->table('book')->get(1);
$book->delete(); // ID 1 の本を削除します
このメソッドはデータベースの特定の 1 行だけを削除します。複数の行をまとめて削除するには Selection::delete()メソッドを使ってください。
テーブルどうしの関係
リレーショナルデータベースでは、データは複数のテーブルに分けられ、外部キーで結び付けられます。Nette Database Explorer は、この関係を扱う画期的な方法を提供します。JOIN のクエリを書く必要も、何かを設定したり生成したりする必要もありません。
関係の扱いを説明するために、本のデータベースの例を使います(GitHub で見られます)。データベースには次のテーブルがあります。
author– 著者と翻訳者(列はid、name、web、born)book– 本(列はid、author_id、translator_id、title、sequel_id)tag– タグ(列はid、name)book_tag– 本とタグをつなぐ中間テーブル(列はbook_id、tag_id)
この本のデータベースの例には、いくつかの種類の関係があります(現実に比べると簡略にしてあります)。
- 1 対多(1:N) – それぞれの本には著者がひとりいます。著者は複数の本を書けます。
- 0 対多(0:N) – 本には翻訳者がいることがあります。翻訳者は複数の本を訳せます。
- 0 対 1(0:1) – 本には続編があることがあります。
- 多対多(M:N) – 本にはいくつかのタグを付けられ、タグはいくつもの本に付けられます。
こうした関係にはいつも親テーブルと子テーブルがあります。たとえば著者と本の関係では、author
テーブルが親で book
テーブルが子です。本はいつも著者に「属している」と考えると分かりやすいでしょう。これはデータベースの構造にも表れています。子テーブル
book は、親テーブル author を参照する外部キー
author_id を持ちます。
本を著者の名前とともに並べたいなら、やり方は 2 つあります。JOIN を使ってひとつの SQL のクエリでデータを取ってくるか、
SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id;
あるいは 2 段階でデータを取ってきて、まず本、次にその著者を取り、そのあと PHP で組み合わせるかです。
SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3); -- 選ばれた本の著者の ID
意外に思えるかもしれませんが、2 つめのやり方のほうが実は効率的です。データは一度しか取られず、キャッシュもよりよく活かせます。Nette Database Explorer はまさにこのように働きます。すべてを裏で片付けて、あなたには優雅な API を差し出します。
$books = $explorer->table('book');
foreach ($books as $book) {
echo 'title: ' . $book->title;
echo 'written by: ' . $book->author->name; // $book->author は 'author' テーブルのレコードです
echo 'translated by: ' . $book->translator?->name;
}
親テーブルへのアクセス
親テーブルへのアクセスは分かりやすいものです。本には著者がいる、本には翻訳者がいることがある
といった関係です。関連するレコードは ActiveRow
オブジェクトのプロパティで取り出せます。その名前は、外部キーの列の名前から _id
の接尾辞を取ったものに対応します。
$book = $explorer->table('book')->get(1);
echo $book->author->name; // author_id 列をもとに著者を見つけます
echo $book->translator?->name; // translator_id 列をもとに翻訳者を見つけます
$book->author プロパティにアクセスすると、Explorer は book
テーブルの中から、名前に author という文字列を含む列(つまり
author_id)を探します。その列の値をもとに author
テーブルの対応するレコードを読み込み、ActiveRow として返します。同じように
$book->translator は translator_id 列を使います。translator_id 列は
null を持てるので、コードでは nullsafe 演算子 ?-> を使っています。
別のやり方として ref() メソッドがあります。これは 2
つの引数、つまり対象のテーブルの名前とつなぐ列の名前を受け取り、ActiveRow
のインスタンスか null を返します。
echo $book->ref('author', 'author_id')->name; // 著者への関係
echo $book->ref('author', 'translator_id')->name; // 翻訳者への関係
ref() メソッドは、たとえばテーブルに同じ名前の列(つまり
author)があってプロパティによるアクセスが使えない場合に役立ちます。そのほかの場合は、読みやすさのためにプロパティによるアクセスをおすすめします。
Explorer はデータベースのクエリを自動的に最適化します。ループで本を回しながら関連するレコード(著者、翻訳者)にアクセスしても、Explorer は本ごとにクエリを作ったりしません。代わりに関係の種類ごとに SELECT のクエリをひとつだけ実行し、データベースの負荷を大きく減らします。たとえば次のようにです。
$books = $explorer->table('book');
foreach ($books as $book) {
echo $book->title . ': ';
echo $book->author->name;
echo $book->translator?->name;
}
このコードはデータベースに対して、次の 3 つのごく速いクエリだけを実行します。
SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- 選ばれた本の author_id 列の ID
SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- 選ばれた本の translator_id 列の ID
つなぐ列を見つける論理は Conventionsの実装が決めます。外部キーを解析し、テーブルどうしの既存の関係を簡単に扱える DiscoveredConventionsをおすすめします。
子テーブルへのアクセス
子テーブルへのアクセスは逆向きに働きます。今度は
この著者はどの本を書いたか、この翻訳者はどの本を訳したか
と尋ねます。この種の問い合わせには related()
メソッドを使います。これは関連するレコードを持つ Selection
を返します。例を見てみましょう。
$author = $explorer->table('author')->get(1);
// その著者のすべての本を出力します
foreach ($author->related('book.author_id') as $book) {
echo "Wrote: $book->title";
}
// その著者が訳したすべての本を出力します
foreach ($author->related('book.translator_id') as $book) {
echo "Translated: $book->title";
}
related()
メソッドは、つなぎ方の指定をドットの書き方でひとつの引数として、あるいは 2
つの別々の引数として受け取ります。
$author->related('book.translator_id'); // 引数ひとつ
$author->related('book', 'translator_id'); // 引数 2 つ
Explorer
は親テーブルの名前をもとに、正しいつなぎの列を自動的に見つけられます。この場合、もとのテーブルの名前が
author なので book.author_id 列でつなぎます。
$author->related('book'); // book.author_id を使います
つなぎ方の候補が複数あると、Explorer は AmbiguousReferenceKeyExceptionを投げます。
もちろん related()
メソッドは、ループで複数のレコードを回しながらも使えます。その場合も Explorer
はクエリを自動的に最適化します。
$authors = $explorer->table('author');
foreach ($authors as $author) {
echo $author->name . ' wrote:';
foreach ($author->related('book') as $book) {
echo $book->title;
}
}
このコードはごく速い SQL のクエリを 2 つだけ生みます。
SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- 選ばれた著者の ID
多対多の関係
多対多(M:N)の関係には、2
つの外部キーの列(book_id、tag_id)を持つ中間テーブル(ここでは
book_tag)が要ります。これらの列はそれぞれ、結び付けられるテーブルの一方の主キーを指します。関連するデータを取り出すには、まず
related('book_tag')
で中間テーブルのレコードを取り、そこから目当てのデータへ進みます。
$book = $explorer->table('book')->get(1);
// その本に付けられたタグの名前を出力します
foreach ($book->related('book_tag') as $bookTag) {
echo $bookTag->tag->name; // 中間テーブル経由でタグの名前を出力します
}
$tag = $explorer->table('tag')->get(1);
// あるいは逆向きに: そのタグが付いた本の名前を出力します
foreach ($tag->related('book_tag') as $bookTag) {
echo $bookTag->book->title; // 本のタイトルを出力します
}
Explorer はここでも SQL のクエリを効率のよい形に最適化します。
SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- 選ばれた本の ID
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- book_tag で見つかったタグの ID
関連するテーブルを通した問い合わせ
where()、select()、order()、group()
メソッドでは、ほかのテーブルの列にアクセスする特別な書き方を使えます。Explorer は必要な
JOIN を自動的に作ります。
ドットの書き方(親テーブル.列)は、子テーブルから見た 1:N
の関係に使います。
$books = $explorer->table('book');
// 著者の名前が 'Jon' で始まる本を見つけます
$books->where('author.name LIKE ?', 'Jon%');
// 本を著者の名前の降順に並べます
$books->order('author.name DESC');
// 本のタイトルと著者の名前を出力します
$books->select('book.title, author.name');
コロンの書き方(:子テーブル.列)は、親テーブルから見た 1:N
の関係に使います。
$authors = $explorer->table('author');
// タイトルに 'PHP' を含む本を書いた著者を見つけます
$authors->where(':book.title LIKE ?', '%PHP%');
// 著者ごとの本の数を数えます
$authors->select('*, COUNT(:book.id) AS book_count')
->group('author.id');
上のコロンの書き方の例(:book.title)では、外部キーの列を指定していません。Explorer
は親テーブルの名前をもとに正しい列を自動的に見つけます。この場合、もとのテーブルの名前が
author なので book.author_id
列でつなぎます。つなぎ方の候補が複数あると、Explorer は AmbiguousReferenceKeyExceptionを投げます。
つなぐ列はかっこの中ではっきり指定できます。
// タイトルに 'PHP' を含む本を訳した著者を見つけます
$authors->where(':book(translator_id).title LIKE ?', '%PHP%');
書き方はつなげられるので、複数のテーブルをまたいでデータにアクセスできます。
// 'PHP' のタグが付いた本の著者を見つけます
$authors->where(':book:book_tag.tag.name', 'PHP')
->group('author.id');
JOIN の条件を広げる
joinWhere() メソッドは、SQL でテーブルをつなぐときの ON
キーワードのうしろの条件を広げます。
ある翻訳者が訳した本を見つけたいとしましょう。
// 'David' という名前の翻訳者が訳した本を見つけます
$books = $explorer->table('book')
->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David')
joinWhere() の条件では、where()
メソッドと同じ書き方、つまり演算子、プレースホルダ、値の配列、SQL の式を使えます。
JOIN が複数ある込み入ったクエリでは、テーブルの別名を定義できます。
$tags = $explorer->table('tag')
->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950)
->alias(':book_tag.book.author', 'book_author');
// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id`
// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id`
// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id`
// AND (`book_author`.`born` < 1950)
where() メソッドが WHERE の句に条件を足すのに対し、joinWhere()
メソッドはテーブルをつなぐときの ON
の句の条件を広げることに注意してください。