Files
yii2/docs/guide-ja/db-query-builder.md
2014-12-04 22:28:27 +09:00

18 KiB

クエリビルダとクエリ

Note|注意: この節はまだ執筆中です。

データベースの基礎 の節で説明したように、Yii は基本的なデータベースアクセスレイヤを提供します。 このデータベースアクセスレイヤは、データベースと相互作用するための低レベルな方法を提供するものです。 それが有用な状況もありますが、生の SQL を書くことは面倒くさく、間違いを生じやすいものでもあります。 これに取って代る方法の一つがクエリビルダを使用することです。 クエリビルダは、実行すべきクエリを生成するためのオブジェクト指向の手法です。

クエリビルダの典型的な使用例は以下のようなものです。

$rows = (new \yii\db\Query())
    ->select('id, name')
    ->from('user')
    ->limit(10)
    ->all();

// これは下記のコードと等価

$query = (new \yii\db\Query())
    ->select('id, name')
    ->from('user')
    ->limit(10);

// コマンドを作成。$command->sql で実際の SQL を取得できる
$command = $query->createCommand();

// コマンドを実行
$rows = $command->queryAll();

クエリメソッド

ご覧のように、yii\db\Query が、あなたが扱わねばならない主役のオブジェクトです。 舞台裏では、Query は、実際には、さまざまなクエリ情報を表現する役目を負っているに過ぎません。 実際のクエリ構築のロジックは、createCommand() コマンドを呼んだときに、yii\db\QueryBuilder によって実行され、クエリの実行は yii\db\Command によって実行されます。

便宜上の理由から、yii\db\Query が、よく使われる一連のクエリメソッド (クエリを構築し、実行して、結果を返すメソッド) を提供しています。 例えば、

  • yii\db\Query::all(): クエリを構築し、実行して、全ての結果を配列として返します。
  • yii\db\Query::one(): 結果の最初の行を返します。
  • yii\db\Query::column(): 結果の最初のカラムを返します。
  • yii\db\Query::scalar(): 結果の最初の行の最初のカラムを返します。
  • yii\db\Query::exists(): 何らかのクエリ結果が有るかどうかを返します。
  • yii\db\Query::count(): COUNT クエリの結果を返します。 他の似たようなメソッドに、sum($q)average($q)max($q)min($q) があり、いわゆる統計データクエリをサポートしています。 これらのメソッドでは $q パラメータは必須であり、カラム名または式を取ります。

クエリを構築する

以下に、SQL 文の中のさまざまな句を組み立てる方法を説明します。 話を単純にするために、$query という変数を使って yii\db\Query オブジェクトを表すものとします。

SELECT

基本的な SELECT クエリを組み立てるためには、どのテーブルからどのカラムをセレクトするかを指定する必要があります。

$query->select('id, name')
    ->from('user');

セレクトのオプションは、上記のように、カンマで区切られた文字列で指定することも出来ますが、配列によって指定することも出来ます。 配列を使う構文は、セレクトを動的に組み立てる場合に、特に有用です。

$query->select(['id', 'name'])
    ->from('user');

Info|情報: SELECT 句が SQL 式を含む場合は、常に配列形式を使うべきです。 これは、CONCAT(first_name, last_name) AS full_name のように、SQL 式がカンマを含みうるからです。 そういう式を他のカラムと一緒に文字列の中に含めると、式がカンマによっていくつかの部分に分離されるおそれがあります。 それはあなたの意図するところではないでしょう。

カラムを指定するときは、例えば user.iduser.id AS user_id などのように、テーブル接頭辞やカラムエイリアスを含めることが出来ます。 カラムを指定するのに配列を使っている場合は、例えば ['user_id' => 'user.id', 'user_name' => 'user.name'] のように、配列のキーを使ってカラムエイリアスを指定することも出来ます。

バージョン 2.0.1 以降では、サブクエリをカラムとしてセレクトすることも出来ます。例えば、

$subQuery = (new Query)->select('COUNT(*)')->from('user');
$query = (new Query)->select(['id', 'count' => $subQuery])->from('post');
// $query は次の SQL を表現する
// SELECT `id`, (SELECT COUNT(*) FROM `user`) AS `count` FROM `post`

重複行を除外して取得したい場合は、次のように、distinct() を呼ぶことが出来ます。

$query->select('user_id')->distinct()->from('post');

FROM

どのテーブルからデータを取得するかを指定するために from() を呼びます。

$query->select('*')->from('user');

カンマ区切りの文字列または配列を使って、複数のテーブルを指定することが出来ます。 テーブル名は、スキーマ接頭辞 (例えば 'public.user')、 および/または、テーブルエイリアス (例えば、'user u') を含んでも構いません。 テーブル名が何らかの括弧を含んでいる場合 (すなわち、テーブルがサブクエリまたは DB 式で与えられていることを意味します) を除いて、メソッドが自動的にテーブル名を引用符で囲みます。 例えば、

$query->select('u.*, p.*')->from(['user u', 'post p']);

テーブルが配列として指定されている場合は、配列のキーをテーブルエイリアスとして使うことも出来ます。 (テーブルにエイリアスが必要でない場合は、文字列のキーを使わないでください。) 例えば、

$query->select('u.*, p.*')->from(['u' => 'user', 'p' => 'post']);

Query オブジェクトを使ってサブクエリを指定することが出来ます。 この場合、対応する配列のキーがサブクエリのエイリアスとして使われます。

$subQuery = (new Query())->select('id')->from('user')->where('status=1');
$query->select('*')->from(['u' => $subQuery]);

WHERE

通常、データは何らかの基準に基づいて選択されます。 クエリビルダはその基準を指定するための有用なメソッドをいくつか持っていますが、その中で最も強力なものが where です。 これは多様な方法で使うことが出来ます。

条件を適用するもっとも簡単な方法は文字列を使うことです。

$query->where('status=:status', [':status' => $status]);

文字列を使うときは、文字列の結合によってクエリを作るのではなく、必ずクエリパラメータをバインドするようにしてください。 上記の手法は使っても安全ですが、下記の手法は安全ではありません。

$query->where("status=$status"); // 危険!

status の値をただちにバインドするのでなく、params または addParams を使ってそうすることも出来ます。

$query->where('status=:status');
$query->addParams([':status' => $status]);

ハッシュ形式 を使って、複数の条件を同時に where にセットすることが出来ます。

$query->where([
    'status' => 10,
    'type' => 2,
    'id' => [4, 8, 15, 16, 23, 42],
]);

上記のコードは次の SQL を生成します。

WHERE (`status` = 10) AND (`type` = 2) AND (`id` IN (4, 8, 15, 16, 23, 42))

NULL はデータベースでは特別な値です。クエリビルダはこれを賢く処理します。例えば、

$query->where(['status' => null]);

これは次の WHERE 句になります。

WHERE (`status` IS NULL)

次のように Query オブジェクトを使ってサブクエリを作ることも出来ます。

$userQuery = (new Query)->select('id')->from('user');
$query->where(['id' => $userQuery]);

これは次の SQL を生成します。

WHERE `id` IN (SELECT `id` FROM `user`)

このメソッドを使うもう一つの方法は、[演算子, オペランド1, オペランド2, ...] という演算形式です。 Another way to use the method is the operand format which is [operator, operand1, operand2, ...].

Operator can be one of the following (see also yii\db\QueryInterface::where()):

  • and: the operands should be concatenated together using AND. For example, ['and', 'id=1', 'id=2'] will generate id=1 AND id=2. If an operand is an array, it will be converted into a string using the rules described here. For example, ['and', 'type=1', ['or', 'id=1', 'id=2']] will generate type=1 AND (id=1 OR id=2). The method will NOT do any quoting or escaping.

  • or: similar to the and operator except that the operands are concatenated using OR.

  • between: operand 1 should be the column name, and operand 2 and 3 should be the starting and ending values of the range that the column is in. For example, ['between', 'id', 1, 10] will generate id BETWEEN 1 AND 10.

  • not between: similar to between except the BETWEEN is replaced with NOT BETWEEN in the generated condition.

  • in: operand 1 should be a column or DB expression. Operand 2 can be either an array or a Query object. It will generate an IN condition. If Operand 2 is an array, it will represent the range of the values that the column or DB expression should be; If Operand 2 is a Query object, a sub-query will be generated and used as the range of the column or DB expression. For example, ['in', 'id', [1, 2, 3]] will generate id IN (1, 2, 3). The method will properly quote the column name and escape values in the range. The in operator also supports composite columns. In this case, operand 1 should be an array of the columns, while operand 2 should be an array of arrays or a Query object representing the range of the columns.

  • not in: similar to the in operator except that IN is replaced with NOT IN in the generated condition.

  • like: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing the values that the column or DB expression should be like. For example, ['like', 'name', 'tester'] will generate name LIKE '%tester%'. When the value range is given as an array, multiple LIKE predicates will be generated and concatenated using AND. For example, ['like', 'name', ['test', 'sample']] will generate name LIKE '%test%' AND name LIKE '%sample%'. You may also provide an optional third operand to specify how to escape special characters in the values. The operand should be an array of mappings from the special characters to their escaped counterparts. If this operand is not provided, a default escape mapping will be used. You may use false or an empty array to indicate the values are already escaped and no escape should be applied. Note that when using an escape mapping (or the third operand is not provided), the values will be automatically enclosed within a pair of percentage characters.

    Note: When using PostgreSQL you may also use ilike instead of like for case-insensitive matching.

  • or like: similar to the like operator except that OR is used to concatenate the LIKE predicates when operand 2 is an array.

  • not like: similar to the like operator except that LIKE is replaced with NOT LIKE in the generated condition.

  • or not like: similar to the not like operator except that OR is used to concatenate the NOT LIKE predicates.

  • exists: requires one operand which must be an instance of yii\db\Query representing the sub-query. It will build a EXISTS (sub-query) expression.

  • not exists: similar to the exists operator and builds a NOT EXISTS (sub-query) expression.

Additionally you can specify anything as operator:

$userQuery = (new Query)->select('id')->from('user');
$query->where(['>=', 'id', 10]);

It will result in:

SELECT id FROM user WHERE id >= 10;

If you are building parts of condition dynamically it's very convenient to use andWhere() and orWhere():

$status = 10;
$search = 'yii';

$query->where(['status' => $status]);
if (!empty($search)) {
    $query->andWhere(['like', 'title', $search]);
}

In case $search isn't empty the following SQL will be generated:

WHERE (`status` = 10) AND (`title` LIKE '%yii%')

Building Filter Conditions

When building filter conditions based on user inputs, you usually want to specially handle "empty inputs" by ignoring them in the filters. For example, you have an HTML form that takes username and email inputs. If the user only enters something in the username input, you may want to build a query that only tries to match the entered username. You may use the filterWhere() method achieve this goal:

// $username and $email are from user inputs
$query->filterWhere([
    'username' => $username,
    'email' => $email,
]);

The filterWhere() method is very similar to where(). The main difference is that filterWhere() will remove empty values from the provided condition. So if $email is "empty", the resulting query will be ...WHERE username=:username; and if both $username and $email are "empty", the query will have no WHERE part.

A value is empty if it is null, an empty string, a string consisting of whitespaces, or an empty array.

You may also use andFilterWhere() and orFilterWhere() to append more filter conditions.

ORDER BY

For ordering results orderBy and addOrderBy could be used:

$query->orderBy([
    'id' => SORT_ASC,
    'name' => SORT_DESC,
]);

Here we are ordering by id ascending and then by name descending.

GROUP BY and HAVING

In order to add GROUP BY to generated SQL you can use the following:

$query->groupBy('id, status');

If you want to add another field after using groupBy:

$query->addGroupBy(['created_at', 'updated_at']);

To add a HAVING condition the corresponding having method and its andHaving and orHaving can be used. Parameters for these are similar to the ones for where methods group:

$query->having(['status' => $status]);

LIMIT and OFFSET

To limit result to 10 rows limit can be used:

$query->limit(10);

To skip 100 fist rows use:

$query->offset(100);

JOIN

The JOIN clauses are generated in the Query Builder by using the applicable join method:

  • innerJoin()
  • leftJoin()
  • rightJoin()

This left join selects data from two related tables in one query:

$query->select(['user.name AS author', 'post.title as title'])
    ->from('user')
    ->leftJoin('post', 'post.user_id = user.id');

In the code, the leftJoin() method's first parameter specifies the table to join to. The second parameter defines the join condition.

If your database application supports other join types, you can use those via the generic join method:

$query->join('FULL OUTER JOIN', 'post', 'post.user_id = user.id');

The first argument is the join type to perform. The second is the table to join to, and the third is the condition.

Like FROM, you may also join with sub-queries. To do so, specify the sub-query as an array which must contain one element. The array value must be a Query object representing the sub-query, while the array key is the alias for the sub-query. For example,

$query->leftJoin(['u' => $subQuery], 'u.id=author_id');

UNION

UNION in SQL adds results of one query to results of another query. Columns returned by both queries should match. In Yii in order to build it you can first form two query objects and then use union method:

$query = new Query();
$query->select("id, category_id as type, name")->from('post')->limit(10);

$anotherQuery = new Query();
$anotherQuery->select('id, type, name')->from('user')->limit(10);

$query->union($anotherQuery);

Batch Query

When working with large amount of data, methods such as yii\db\Query::all() are not suitable because they require loading all data into the memory. To keep the memory requirement low, Yii provides the so-called batch query support. A batch query makes uses of data cursor and fetches data in batches.

Batch query can be used like the following:

use yii\db\Query;

$query = (new Query())
    ->from('user')
    ->orderBy('id');

foreach ($query->batch() as $users) {
    // $users is an array of 100 or fewer rows from the user table
}

// or if you want to iterate the row one by one
foreach ($query->each() as $user) {
    // $user represents one row of data from the user table
}

The method yii\db\Query::batch() and yii\db\Query::each() return an yii\db\BatchQueryResult object which implements the Iterator interface and thus can be used in the foreach construct. During the first iteration, a SQL query is made to the database. Data are since then fetched in batches in the iterations. By default, the batch size is 100, meaning 100 rows of data are being fetched in each batch. You can change the batch size by passing the first parameter to the batch() or each() method.

Compared to the yii\db\Query::all(), the batch query only loads 100 rows of data at a time into the memory. If you process the data and then discard it right away, the batch query can help keep the memory usage under a limit.

If you specify the query result to be indexed by some column via yii\db\Query::indexBy(), the batch query will still keep the proper index. For example,

use yii\db\Query;

$query = (new Query())
    ->from('user')
    ->indexBy('username');

foreach ($query->batch() as $users) {
    // $users is indexed by the "username" column
}

foreach ($query->each() as $username => $user) {
}