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_idbar_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 – 著者と翻訳者(列は idnamewebborn
  • book – 本(列は idauthor_idtranslator_idtitlesequel_id
  • tag – タグ(列は idname
  • book_tag – 本とタグをつなぐ中間テーブル(列は book_idtag_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->translatortranslator_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_idtag_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 の句の条件を広げることに注意してください。