16 フォロワー

アクティブレコード

アクティブレコード は、データベースに保存されたデータにアクセスし、操作するためのオブジェクト指向インターフェースを提供します。アクティブレコードクラスはデータベーステーブルに関連付けられ、アクティブレコードインスタンスはテーブルの行に対応し、アクティブレコードインスタンスの属性はその行の特定の列の値を表します。生の SQL ステートメントを書く代わりに、アクティブレコード属性にアクセスし、アクティブレコードメソッドを呼び出して、データベーステーブルに保存されているデータにアクセスし、操作します。

たとえば、Customercustomer テーブルに関連付けられたアクティブレコードクラスであり、namecustomer テーブルの列であると仮定します。新しい行をcustomer テーブルに挿入するには、次のコードを記述できます。

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

上記のコードは、MySQL に対して次の生の SQL ステートメントを使用することと同等ですが、直感的ではなく、エラーが発生しやすく、異なる種類のデータベースを使用している場合、互換性の問題が発生する可能性さえあります。

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii は、次のリレーショナルデータベースに対してアクティブレコードのサポートを提供します。

  • MySQL 4.1 以降:yii\db\ActiveRecord を介して
  • PostgreSQL 7.3 以降:yii\db\ActiveRecord を介して
  • SQLite 2 および 3:yii\db\ActiveRecord を介して
  • Microsoft SQL Server 2008 以降:yii\db\ActiveRecord を介して
  • Oracle:yii\db\ActiveRecord を介して
  • CUBRID 9.3 以降:yii\db\ActiveRecord を介して(cubrid PDO エクステンションのバグ bug のため、値の引用は機能しないため、クライアントとサーバーの両方で CUBRID 9.3 が必要です)
  • Sphinx:yii\sphinx\ActiveRecord を介して、yii2-sphinx エクステンションが必要です
  • ElasticSearch: yii\elasticsearch\ActiveRecord を介して利用可能。yii2-elasticsearch エクステンションが必要です。

さらに、Yii は以下の NoSQL データベースでも Active Record をサポートしています。

  • Redis 2.6.12 以降: yii\redis\ActiveRecord を介して利用可能。yii2-redis エクステンションが必要です。
  • MongoDB 1.3.0 以降: yii\mongodb\ActiveRecord を介して利用可能。yii2-mongodb エクステンションが必要です。

このチュートリアルでは、主にリレーショナルデータベースに対する Active Record の使用方法について説明します。ただし、ここで説明されている内容のほとんどは、NoSQL データベースに対する Active Record にも適用できます。

Active Record クラスの宣言

開始するには、yii\db\ActiveRecord を拡張して Active Record クラスを宣言します。

テーブル名の設定

デフォルトでは、各 Active Record クラスはデータベーステーブルに関連付けられています。tableName() メソッドは、yii\helpers\Inflector::camel2id() を介してクラス名を変換することでテーブル名を返します。テーブル名がこの規約に従っていない場合は、このメソッドをオーバーライドできます。

また、デフォルトのtablePrefix を適用できます。たとえば、tablePrefixtbl_ の場合、Customertbl_customer になり、OrderItemtbl_order_item になります。

テーブル名が {{%TableName}} として指定されている場合、パーセント記号 % はテーブルプレフィックスに置き換えられます。たとえば、{{%post}}{{tbl_post}} になります。テーブル名周りの角括弧は、SQL クエリでのクォーティングに使用されます。

次の例では、customer データベーステーブルに対応する Customer という名前の Active Record クラスを宣言します。

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Active Record は「モデル」と呼ばれます

Active Record のインスタンスはモデルと見なされます。このため、通常、Active Record クラスは app\models 名前空間(またはモデルクラスを保持するためのその他の名前空間)に配置します。

yii\db\ActiveRecordyii\base\Model から拡張されているため、属性、検証ルール、データシリアル化など、すべてのモデル機能を継承します。

データベースへの接続

デフォルトでは、Active Record はデータベースデータへのアクセスと操作にアプリケーションコンポーネントである dbDB接続として使用します。データベースアクセオブジェクトで説明されているように、以下に示すようにアプリケーション設定で db コンポーネントを構成できます。

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

db コンポーネント以外のデータベース接続を使用する場合は、getDb() メソッドをオーバーライドする必要があります。

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        return \Yii::$app->db2;  
    }
}

データのクエリ

Active Record クラスを宣言したら、それを用いて対応するデータベーステーブルからデータクエリを実行できます。このプロセスは通常、次の3つのステップで行われます。

  1. yii\db\ActiveRecord::find() メソッドを呼び出して新しいクエリオブジェクトを作成します。
  2. クエリ構築メソッドを呼び出してクエリオブジェクトを構築します。
  3. Active Record インスタンスという形でデータを取得するためにクエリメソッドを呼び出します。

ご覧のとおり、これはクエリビルダーの手順と非常によく似ています。唯一の違いは、新しいクエリオブジェクトを作成するために new 演算子を使用する代わりに、yii\db\ActiveRecord::find() を呼び出して、yii\db\ActiveQuery クラスの新しいクエリオブジェクトを返すことです。

以下は、Active Query を使用してデータクエリを実行する方法を示すいくつかの例です。

// return a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// return all active customers and order them by their IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// return the number of active customers
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// return all customers in an array indexed by customer IDs
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

上記では、$customerCustomer オブジェクトであり、$customersCustomer オブジェクトの配列です。これらはすべて、customer テーブルから取得されたデータで設定されています。

情報: yii\db\ActiveQueryyii\db\Query から拡張されているため、セクションクエリビルダーで説明されているすべてのクエリ構築メソッドとクエリメソッドを使用できます。

主キー値または一連の列値でクエリを実行することは一般的なタスクであるため、Yii はこの目的のために2つのショートカットメソッドを提供しています。

どちらのメソッドも、以下のパラメーター形式のいずれかを受け取ることができます。

  • スカラー値: この値は、検索する目的の主キー値として扱われます。Yii は、データベーススキーマ情報を読み取ることで、どの列が主キー列であるかを自動的に判断します。
  • スカラー値の配列: この配列は、検索する目的の主キー値として扱われます。
  • 連想配列: キーは列名であり、値は検索する目的の対応する列値です。ハッシュ形式の詳細については、こちらを参照してください。

次のコードは、これらのメソッドの使用方法を示しています。

// returns a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// returns customers whose ID is 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// returns an active customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// returns all inactive customers
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

警告: これらのメソッドにユーザー入力を渡す必要がある場合は、入力値がスカラーであることを確認し、配列条件の場合は、配列構造が外部から変更できないことを確認してください。

// yii\web\Controller ensures that $id is scalar
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// explicitly specifying the column to search, passing a scalar or array here will always result in finding a single record
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// do NOT use the following code! it is possible to inject an array condition to filter by arbitrary column values!
$model = Post::findOne(Yii::$app->request->get('id'));

注記: yii\db\ActiveRecord::findOne()yii\db\ActiveQuery::one() のどちらも、生成された SQL ステートメントに LIMIT 1 を追加しません。クエリが多くの行のデータを返す可能性がある場合は、パフォーマンスを向上させるために明示的に limit(1) を呼び出す必要があります(例: Customer::find()->limit(1)->one())。

クエリ構築メソッドを使用する以外にも、生の SQL を記述してデータクエリを実行し、結果を Active Record オブジェクトに設定することもできます。yii\db\ActiveRecord::findBySql() メソッドを呼び出すことでこれを行うことができます。

// returns all inactive customers
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

findBySql() を呼び出した後に、追加のクエリ構築メソッドを呼び出さないでください。それらは無視されます。

データへのアクセス

前述のように、データベースから取得されたデータは Active Record インスタンスに設定され、クエリ結果の各行は単一の Active Record インスタンスに対応します。たとえば、Active Record インスタンスの属性にアクセスすることで、列値にアクセスできます。

// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

注記: Active Record 属性は、関連付けられたテーブル列の名前に大文字と小文字を区別して付けられています。Yii は、関連付けられたテーブルのすべての列に対して、Active Record に属性を自動的に定義します。属性の再宣言は行わないでください。

Active Record 属性はテーブル列の名前に基づいているため、テーブル列がこの方法で命名されている場合、$customer->first_name のように、属性名で単語を区切るためにアンダースコアを使用する PHP コードを記述することになる場合があります。コードスタイルの一貫性を気にする場合は、テーブル列の名前をそれに応じて変更する必要があります(たとえば、camelCase を使用するなど)。

データ変換

入力および/または表示されるデータが、データベースにデータを保存するために使用される形式とは異なる形式であることはよくあります。たとえば、データベースでは顧客の誕生日を UNIX タイムスタンプとして保存していますが(ただし、これは良い設計ではありません)、ほとんどの場合、誕生日は 'YYYY/MM/DD' 形式の文字列として操作したいでしょう。この目標を達成するために、次のように Customer Active Record クラスにデータ変換メソッドを定義できます。

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('Y/m/d', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

これで、PHP コードでは $customer->birthday にアクセスする代わりに $customer->birthdayText にアクセスできるようになり、'YYYY/MM/DD' 形式で顧客の誕生日を入力して表示できます。

ヒント: 上記の例は、異なる形式のデータを変換する一般的な方法を示しています。日付値を操作する場合は、DateValidatoryii\jui\DatePicker を使用できます。これらは使いやすく、より強力です。

配列でのデータの取得

Active Record オブジェクトという形でデータを取得することは便利で柔軟性がありますが、大量のデータを取得する必要がある場合、大きなメモリフットプリントのために常に望ましいわけではありません。この場合、クエリメソッドを実行する前に asArray() を呼び出すことで、PHP 配列を使用してデータを取得できます。

// return all customers
// each customer is returned as an associative array
$customers = Customer::find()
    ->asArray()
    ->all();

注記: このメソッドはメモリを節約し、パフォーマンスを向上させますが、下位の DB 抽象化レイヤーにより近く、Active Record の機能のほとんどを失います。非常に重要な違いは、列値のデータ型にあります。Active Record インスタンスでデータを返す場合、列値は実際の列の種類に従って自動的に型変換されます。一方、配列でデータを返す場合、列値は文字列になります(それらは処理なしの PDO の結果であるため)、実際の列の種類に関係なく。

バッチでのデータの取得

クエリビルダーでは、データベースから大量のデータをクエリするときのメモリ使用量を最小限に抑えるためにバッチクエリを使用できることを説明しました。Active Record でも同じテクニックを使用できます。たとえば、

// fetch 10 customers at a time
foreach (Customer::find()->batch(10) as $customers) {
    // $customers is an array of 10 or fewer Customer objects
}

// fetch 10 customers at a time and iterate them one by one
foreach (Customer::find()->each(10) as $customer) {
    // $customer is a Customer object
}

// batch query with eager loading
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer is a Customer object with the 'orders' relation populated
}

データの保存

Active Record を使用すると、次の手順を実行することでデータをデータベースに簡単に保存できます。

  1. Active Record インスタンスを準備します。
  2. Active Record 属性に新しい値を代入します。
  3. yii\db\ActiveRecord::save() を呼び出してデータをデータベースに保存します。

たとえば、

// insert a new row of data
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// update an existing row of data
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

save() メソッドは、Active Record インスタンスの状態に応じて、データの行を挿入または更新できます。インスタンスが new 演算子で新しく作成された場合、save() を呼び出すと、新しい行が挿入されます。インスタンスがクエリメソッドの結果である場合、save() を呼び出すと、インスタンスに関連付けられた行が更新されます。

isNewRecord プロパティの値を確認することで、Active Record インスタンスの2つの状態を区別できます。このプロパティは、次のように内部的に save() によって使用されます。

public function save($runValidation = true, $attributeNames = null)
{
    if ($this->getIsNewRecord()) {
        return $this->insert($runValidation, $attributeNames);
    } else {
        return $this->update($runValidation, $attributeNames) !== false;
    }
}

ヒント: insert() または update() を直接呼び出して、行を挿入または更新できます。

データ検証

yii\db\ActiveRecordyii\base\Model から拡張されているため、同じデータ検証機能を共有します。rules() メソッドをオーバーライドすることで検証ルールを宣言し、validate() メソッドを呼び出すことでデータ検証を実行できます。

save() を呼び出すと、デフォルトで validate() が自動的に呼び出されます。検証が成功した場合のみ、実際にデータが保存されます。それ以外の場合は、単に false を返し、errors プロパティをチェックして検証エラーメッセージを取得できます。

ヒント: データの検証が不要であると確信している場合(例:データが信頼できるソースから取得された場合)、検証をスキップするために `save(false)` を呼び出すことができます。

一括代入

通常のモデルと同様に、Active Recordインスタンスも一括代入機能を利用できます。この機能を使用すると、以下に示すように、単一のPHPステートメントでActive Recordインスタンスの複数の属性に値を代入できます。ただし、一括代入できるのは安全な属性のみであることを忘れないでください。

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

カウンタの更新

データベーステーブルのカラムを増分または減分することは一般的なタスクです。これらのカラムを「カウンタカラム」と呼びます。1つまたは複数のカウンタカラムを更新するには、updateCounters() を使用できます。例えば、

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

注意: yii\db\ActiveRecord::save() を使用してカウンタカラムを更新する場合、同じカウンタ値を読み書きする複数のリクエストによって同じカウンタが保存される可能性があるため、結果が不正確になる可能性があります。

変更された属性

Active Recordインスタンスを保存するためにsave()を呼び出すと、変更された属性のみが保存されます。属性は、DBからロードされた後または最後にDBに保存された後、その値が変更された場合に変更済みと見なされます。Active Recordインスタンスに変更された属性があるかどうかに関係なく、データ検証は実行されることに注意してください。

Active Recordは、変更された属性のリストを自動的に保持します。これは、属性値の古いバージョンを保持し、最新のバージョンと比較することで行われます。現在変更されている属性を取得するには、yii\db\ActiveRecord::getDirtyAttributes() を呼び出すことができます。属性を明示的に変更済みとしてマークするには、yii\db\ActiveRecord::markAttributeDirty() を呼び出すこともできます。

最後に変更される前の属性値に関心がある場合は、getOldAttributes()またはgetOldAttribute()を呼び出すことができます。

注意: 古い値と新しい値の比較は `===` 演算子を使用して行われるため、値が同じでも型が異なる場合は、値は変更済みと見なされます。これは、すべての値が文字列として表されるHTMLフォームからモデルがユーザー入力を受け取る場合によく起こります。例えば、整数値の正しい型を確保するには、バリデーションフィルタを適用できます: `['attributeName', 'filter', 'filter' => 'intval']`。これは、intval()floatval()boolvalなど、PHPのすべての型変換関数で動作します。

デフォルトの属性値

テーブルカラムの一部には、データベースに定義されたデフォルト値がある場合があります。場合によっては、Active RecordインスタンスのWebフォームにこれらのデフォルト値を事前に設定したい場合があります。同じデフォルト値を再度記述するのを避けるために、loadDefaultValues() を呼び出して、DBで定義されたデフォルト値を対応するActive Record属性に設定できます。

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz will be assigned the default value declared when defining the "xyz" column

属性の型変換

yii\db\ActiveRecord は、クエリ結果によって設定されるため、データベーステーブルスキーマの情報を使用して、属性値の自動型変換を実行します。これにより、整数として宣言されたテーブルカラムから取得されたデータは、PHP整数でActive Recordインスタンスに設定され、ブール値はブール値で設定されるなどとなります。ただし、型変換メカニズムにはいくつかの制限があります。

  • 浮動小数点値は変換されず、文字列として表されます。そうでない場合、精度が失われる可能性があります。
  • 整数値の変換は、使用するオペレーティングシステムの整数容量に依存します。特に、「符号なし整数」または「ビッグ整数」として宣言されたカラムの値は、64ビットオペレーティングシステムでのみPHP整数に変換され、32ビットオペレーティングシステムでは文字列として表されます。

属性の型変換は、クエリ結果からActive Recordインスタンスを設定する場合にのみ実行されます。HTTPリクエストからロードされた値や、プロパティアクセスを介して直接設定された値に対する自動変換はありません。テーブルスキーマは、Active Recordデータの保存のためのSQLステートメントを準備する際にも使用され、値が正しい型でクエリにバインドされることを保証します。ただし、Active Recordインスタンスの属性値は、保存プロセス中に変換されません。

ヒント: yii\behaviors\AttributeTypecastBehavior を使用して、Active Recordの検証または保存時の属性値の型変換を容易にすることができます。

2.0.14以降、Yii ActiveRecordはJSONや多次元配列などの複雑なデータ型をサポートしています。

MySQLとPostgreSQLのJSON

データ設定後、JSONカラムの値は標準的なJSONデコードルールに従ってJSONから自動的にデコードされます。

属性値をJSONカラムに保存するには、ActiveRecordはJsonExpressionオブジェクトを自動的に作成し、QueryBuilderレベルでJSON文字列にエンコードされます。

PostgreSQLの配列

データ設定後、配列カラムの値はPgSQL表記からArrayExpressionオブジェクトに自動的にデコードされます。これはPHPの `ArrayAccess` インターフェースを実装しているため、配列として使用したり、 `->getValue()` を呼び出して配列自体を取得したりできます。

属性値を配列カラムに保存するには、ActiveRecordはArrayExpressionオブジェクトを自動的に作成し、QueryBuilderによって配列のPgSQL文字列表現にエンコードされます。

JSONカラムにも条件を使用できます。

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])])

式の構築システムの詳細については、クエリビルダー - カスタム条件と式の追加の記事を参照してください。

複数行の更新

上記の方法はいずれも個々のActive Recordインスタンスで動作し、個々のテーブル行の挿入または更新が行われます。複数の行を同時に更新するには、代わりに静的メソッドであるupdateAll()を呼び出す必要があります。

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

同様に、updateAllCounters() を呼び出して、複数の行のカウンタカラムを同時に更新できます。

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

データの削除

単一行のデータを削除するには、まずその行に対応するActive Recordインスタンスを取得し、次にyii\db\ActiveRecord::delete()メソッドを呼び出します。

$customer = Customer::findOne(123);
$customer->delete();

複数の行またはすべての行を削除するには、yii\db\ActiveRecord::deleteAll()を呼び出すことができます。例えば、

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

注意: deleteAll() を呼び出す際には十分に注意してください。条件の指定を間違えると、テーブルからすべてのデータが完全に消去される可能性があります。

Active Recordのライフサイクル

さまざまな目的に使用する場合、Active Recordのライフサイクルを理解することが重要です。各ライフサイクルにおいて、特定の順序でメソッドが呼び出され、これらのメソッドをオーバーライドしてライフサイクルをカスタマイズすることができます。ライフサイクル中にトリガーされる特定のActive Recordイベントに応答して、カスタムコードを挿入することもできます。これらのイベントは、Active Recordのライフサイクルをカスタマイズする必要があるActive Recordビヘイビアを開発する場合に特に役立ちます。

以下では、さまざまなActive Recordのライフサイクルと、ライフサイクルに関連するメソッド/イベントの概要を示します。

新規インスタンスのライフサイクル

`new`演算子を使用して新しいActive Recordインスタンスを作成すると、次のライフサイクルが発生します。

  1. クラスコンストラクタ。
  2. init(): EVENT_INITイベントをトリガーします。

データクエリライフサイクル

クエリメソッドのいずれかを使用してデータクエリを行うと、新しく設定された各Active Recordは次のライフサイクルを経ます。

  1. クラスコンストラクタ。
  2. init(): EVENT_INITイベントをトリガーします。
  3. afterFind(): EVENT_AFTER_FINDイベントをトリガーします。

データ保存ライフサイクル

Active Recordインスタンスを挿入または更新するためにsave()を呼び出すと、次のライフサイクルが発生します。

  1. beforeValidate(): EVENT_BEFORE_VALIDATEイベントをトリガーします。メソッドが `false` を返すか、yii\base\ModelEvent::$isValid が `false` の場合、残りのステップはスキップされます。
  2. データ検証を実行します。データ検証に失敗した場合、ステップ3以降のステップはスキップされます。
  3. afterValidate(): EVENT_AFTER_VALIDATEイベントをトリガーします。
  4. beforeSave(): EVENT_BEFORE_INSERTまたはEVENT_BEFORE_UPDATEイベントをトリガーします。メソッドが `false` を返すか、yii\base\ModelEvent::$isValid が `false` の場合、残りのステップはスキップされます。
  5. 実際のデータ挿入または更新を実行します。
  6. afterSave(): EVENT_AFTER_INSERTまたはEVENT_AFTER_UPDATEイベントをトリガーします。

データ削除ライフサイクル

Active Recordインスタンスを削除するためにdelete()を呼び出すと、次のライフサイクルが発生します。

  1. beforeDelete(): EVENT_BEFORE_DELETEイベントをトリガーします。メソッドが `false` を返すか、yii\base\ModelEvent::$isValid が `false` の場合、残りのステップはスキップされます。
  2. 実際のデータ削除を実行します。
  3. afterDelete(): EVENT_AFTER_DELETEイベントをトリガーします。

注意: 以下のメソッドを呼び出しても、レコード単位ではなくデータベースで直接動作するため、上記のライフサイクルは開始されません。

注意: パフォーマンス上の問題のため、DIはデフォルトではサポートされていません。必要に応じて、instantiate()メソッドをオーバーライドして、Yii::createObject()を介してクラスをインスタンス化することで、サポートを追加できます。

public static function instantiate($row)
{
    return Yii::createObject(static::class);
}

データ更新ライフサイクル

Active Recordインスタンスを更新するためにrefresh()を呼び出すと、更新が成功し、メソッドが `true` を返す場合、EVENT_AFTER_REFRESHイベントがトリガーされます。

トランザクションの使用

Active Recordを使用する際にトランザクションを使用する方法は2つあります。

1つ目の方法は、以下に示すように、トランザクションブロックでActive Recordメソッド呼び出しを明示的に囲むことです。

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
});

// or alternatively

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

注記: 上記のコードでは、PHP 5.x と PHP 7.x の互換性のために2つの catch ブロックを使用しています。\Exception は PHP 7.0 以降で \Throwable インターフェース を実装しているため、アプリケーションが PHP 7.0 以降のみを使用する場合は、\Exception を含む部分を省略できます。

2つ目の方法は、トランザクションサポートを必要とするDB操作を yii\db\ActiveRecord::transactions() メソッドにリストすることです。例えば、

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

yii\db\ActiveRecord::transactions() メソッドは、キーが シナリオ 名で、値がトランザクションで囲むべき対応する操作である配列を返す必要があります。異なるDB操作を参照するには、次の定数を使用してください。

複数の操作を示すには、| 演算子を使用して上記の定数を連結します。上記の3つの操作すべてを参照するには、ショートカット定数 OP_ALL を使用することもできます。

このメソッドを使用して作成されたトランザクションは、beforeSave() を呼び出す前に開始され、afterSave() が実行された後にコミットされます。

楽観的ロック

楽観的ロックは、1つのデータ行が複数のユーザーによって更新される際に発生する可能性のある競合を防ぐ方法です。例えば、ユーザーAとユーザーBの両方が同時に同じウィキ記事を編集しています。ユーザーAが編集を保存した後、ユーザーBが彼の編集を保存しようと「保存」ボタンをクリックします。ユーザーBが実際には記事の古いバージョンで作業していたため、彼による記事の保存を防ぎ、何らかのヒントメッセージを表示する方法は望ましいでしょう。

楽観的ロックは、各行のバージョン番号を記録する列を使用して上記の解決策を提供します。古いバージョン番号でデータ行が保存されると、yii\db\StaleObjectException 例外がスローされ、行の保存が防止されます。楽観的ロックは、それぞれyii\db\ActiveRecord::update()またはyii\db\ActiveRecord::delete()を使用して既存のデータ行を更新または削除する場合にのみサポートされます。

楽観的ロックを使用するには、

  1. Active Recordクラスに関連付けられたDBテーブルに、各行のバージョン番号を格納する列を作成します。この列は大きな整数型にする必要があります(MySQLではBIGINT DEFAULT 0になります)。
  2. yii\db\ActiveRecord::optimisticLock() メソッドをオーバーライドして、この列の名前を返します。
  3. モデルクラス内にOptimisticLockBehaviorを実装して、受信したリクエストからその値を自動的に解析します。OptimisticLockBehaviorが処理するため、バリデーションルールからバージョン属性を削除します。
  4. ユーザー入力を受け取るWebフォームに、更新される行の現在のバージョン番号を格納する非表示フィールドを追加します。
  5. Active Recordを使用して行を更新するコントローラーアクションで、yii\db\StaleObjectException例外をtry-catchします。競合を解決するための必要なビジネスロジック(例:変更のマージ、古いデータの表示)を実装します。

例えば、バージョン列の名前がversionだと仮定します。楽観的ロックは、次のようなコードで実装できます。

// ------ view code -------

use yii\helpers\Html;

// ...other input fields
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
    $model = $this->findModel($id);

    try {
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    } catch (StaleObjectException $e) {
        // logic to resolve the conflict
    }
}

// ------ model code -------

use yii\behaviors\OptimisticLockBehavior;

public function behaviors()
{
    return [
        OptimisticLockBehavior::class,
    ];
}

public function optimisticLock()
{
    return 'version';
}

注記: OptimisticLockBehaviorgetBodyParam()を直接解析することで、ユーザーが有効なバージョン番号を送信した場合にのみレコードが保存されることを保証するため、内部使用専用のインスタンスを持ちながら、エンドユーザー入力の受信を担当するコントローラーに別のインスタンスを関連付けるために、親モデルでステップ2を実行し、子クラスにビヘイビア(ステップ3)をアタッチするモデルクラスを拡張することが役立つ場合があります。あるいは、そのvalueプロパティを設定することで独自のロジックを実装することもできます。

リレーショナルデータの操作

個々のデータベーステーブルの操作に加えて、Active Recordは関連するデータをまとめて、主データから容易にアクセスできるようにすることもできます。例えば、顧客データは注文データと関連しており、1人の顧客が1つまたは複数の注文を出している可能性があります。この関係を適切に宣言することで、$customer->ordersという式を使用して顧客の注文情報にアクセスできます。これにより、Order Active Recordインスタンスの配列として顧客の注文情報が返されます。

関係の宣言

Active Recordを使用してリレーショナルデータを使用するには、まずActive Recordクラスで関係を宣言する必要があります。この作業は、次のように、関係ごとに関係メソッドを宣言するほど簡単です。

class Customer extends ActiveRecord
{
    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    // ...

    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

上記のコードでは、Customerクラスにorders関係を、Orderクラスにcustomer関係を宣言しています。

各関係メソッドの名前はgetXyzにする必要があります。xyz(最初の文字は小文字)を関係名と呼びます。関係名は大文字と小文字が区別されます

関係を宣言する際には、次の情報を指定する必要があります。

  • 関係の多重度: hasMany()またはhasOne()を呼び出すことによって指定します。上記の例では、関係宣言から、顧客は多くの注文を持っている一方、注文は1つの顧客しか持たないことが容易に読み取れます。
  • 関連するActive Recordクラスの名前: hasMany()またはhasOne()の最初の引数として指定します。IDEの自動補完とコンパイル時のエラー検出のために、クラス名文字列を取得するためにXyz::classを呼び出すことをお勧めします。
  • 2種類のデータ間のリンク: 2種類のデータが関連付けられている列を指定します。配列の値は主データの列(関係を宣言しているActive Recordクラスで表される)であり、配列のキーは関連データの列です。

    これを覚えるための簡単なルールは、上記の例のように、関連するActive Recordに属する列をそのすぐ隣に書くことです。そこでは、customer_idOrderのプロパティであり、idCustomerのプロパティであることがわかります。

警告: 関係名relationは予約されています。使用するとArgumentCountErrorが発生します。

リレーショナルデータへのアクセス

関係を宣言したら、関係名を通じてリレーショナルデータにアクセスできます。これは、関係メソッドによって定義されたオブジェクトプロパティにアクセスするのと同じです。このため、これを関係プロパティと呼びます。例えば、

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

情報: ゲッターメソッドgetXyz()を介してxyzという名前の関係を宣言すると、オブジェクトプロパティのようにxyzにアクセスできます。名前は大文字と小文字が区別されます。

hasMany()で関係が宣言されている場合、この関係プロパティにアクセスすると、関連するActive Recordインスタンスの配列が返されます。hasOne()で関係が宣言されている場合、関係プロパティにアクセスすると、関連するActive Recordインスタンス、または関連データが見つからない場合はnullが返されます。

初めて関係プロパティにアクセスすると、上記の例のようにSQL文が実行されます。同じプロパティに再度アクセスした場合、SQL文を再実行せずに以前の結果が返されます。SQL文の再実行を強制するには、まず関係プロパティをアンセットする必要があります: unset($customer->orders)

注記: この概念はオブジェクトプロパティ機能と似ていますが、重要な違いがあります。通常のオブジェクトプロパティの場合、プロパティの値は定義するゲッターメソッドと同じ型です。しかし、関係メソッドはyii\db\ActiveQueryインスタンスを返し、関係プロパティにアクセスすると、yii\db\ActiveRecordインスタンスまたはその配列が返されます。

$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance

これは、次のセクションで説明するカスタマイズされたクエリを作成するのに役立ちます。

動的なリレーショナルクエリ

関係メソッドはyii\db\ActiveQueryのインスタンスを返すため、DBクエリを実行する前に、クエリビルダーメソッドを使用してこのクエリをさらに構築できます。例えば、

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

関係プロパティにアクセスする場合とは異なり、関係メソッドを介して動的なリレーショナルクエリを実行するたびに、同じ動的なリレーショナルクエリが以前に実行されていた場合でも、SQL文が実行されます。

動的なリレーショナルクエリをより簡単に実行できるように、関係宣言をパラメーター化することもあります。例えば、次のようにbigOrders関係を宣言できます。

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

その後、次のリレーショナルクエリを実行できます。

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

結合テーブルを介した関係

データベースモデリングでは、2つの関連テーブル間の多重度が多対多の場合、通常結合テーブルが導入されます。例えば、orderテーブルとitemテーブルは、order_itemという名前の結合テーブルを介して関連付けられている可能性があります。1つの注文は複数の注文アイテムに対応し、1つの製品アイテムも複数の注文アイテムに対応します。

このような関係を宣言する場合は、via()またはviaTable()を呼び出して結合テーブルを指定します。via()viaTable()の違いは、前者が既存の関係名で結合テーブルを指定するのに対し、後者は直接結合テーブルを使用することです。例えば、

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

または、

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->via('orderItems');
    }
}

結合テーブルで宣言された関係の使用方法は、通常の関係と同じです。例えば、

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// returns an array of Item objects
$items = $order->items;

複数のテーブルを介した関係定義のチェーン化

さらに、via() を使用してリレーション定義をチェーンすることで、複数のテーブルを介してリレーションを定義することも可能です。上記の例では、CustomerOrderItemというクラスがあります。 これらのクラスに、顧客が注文したすべてのアイテムをリストするリレーションを追加し、getPurchasedItems()という名前を付けることができます。リレーションのチェーンは、次のコード例に示されています。

class Customer extends ActiveRecord
{
    // ...

    public function getPurchasedItems()
    {
        // customer's items, matching 'id' column of `Item` to 'item_id' in OrderItem
        return $this->hasMany(Item::class, ['id' => 'item_id'])
                    ->via('orderItems');
    }

    public function getOrderItems()
    {
        // customer's order items, matching 'id' column of `Order` to 'order_id' in OrderItem
        return $this->hasMany(OrderItem::class, ['order_id' => 'id'])
                    ->via('orders');
    }

    public function getOrders()
    {
        // same as above
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

遅延読み込みと早期読み込み

リレーショナルデータへのアクセスで説明したように、Active Recordインスタンスのリレーションプロパティには、通常のオブジェクトプロパティにアクセスする場合と同様にアクセスできます。SQL文は、リレーションプロパティに初めてアクセスしたときにのみ実行されます。このようなリレーショナルデータアクセス方法を遅延読み込みと呼びます。例:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// no SQL executed
$orders2 = $customer->orders;

遅延読み込みは非常に便利ですが、複数のActive Recordインスタンスの同じリレーションプロパティにアクセスする必要がある場合、パフォーマンスの問題が発生することがあります。次のコード例を考えてみましょう。実行されるSQL文は何個ですか?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

上記のコードコメントからわかるように、101個のSQL文が実行されています!これは、forループ内で異なるCustomerオブジェクトのordersリレーションプロパティにアクセスするたびに、SQL文が実行されるためです。

このパフォーマンスの問題を解決するために、以下に示すように、いわゆる早期読み込みアプローチを使用できます。

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // no SQL executed
    $orders = $customer->orders;
}

yii\db\ActiveQuery::with()を呼び出すことで、Active Recordに対し、最初の100人の顧客の注文を1つのSQL文で取得するように指示します。その結果、実行されるSQL文の数は101個から2個に削減されます!

1つまたは複数のリレーションを早期読み込みできます。ネストされたリレーションを早期読み込みすることもできます。ネストされたリレーションとは、関連するActive Recordクラス内で宣言されたリレーションです。たとえば、Customerordersリレーションを介してOrderと関連付けられ、Orderitemsリレーションを介してItemと関連付けられています。Customerをクエリする場合、ネストされたリレーション表記orders.itemsを使用してitemsを早期読み込みできます。

次のコードは、with()のさまざまな使用方法を示しています。Customerクラスにはorderscountryの2つのリレーションがあり、Orderクラスにはitemsという1つのリレーションがあると仮定します。

// eager loading both "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalent to the array syntax below
$customers = Customer::find()->with(['orders', 'country'])->all();
// no SQL executed 
$orders= $customers[0]->orders;
// no SQL executed 
$country = $customers[0]->country;

// eager loading "orders" and the nested relation "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// access the items of the first order of the first customer
// no SQL executed
$items = $customers[0]->orders[0]->items;

a.b.c.dなど、深くネストされたリレーションを早期読み込みできます。すべての上位リレーションが早期読み込みされます。with()a.b.c.dを使用して呼び出すと、aa.ba.b.ca.b.c.dが早期読み込みされます。

情報: 一般に、結合テーブルで定義されたリレーションがM個あるN個のリレーションを早期読み込みする場合、合計N+M+1個のSQL文が実行されます。ネストされたリレーションa.b.c.dは4つのリレーションとしてカウントされます。

リレーションを早期読み込みする際、無名関数を使用して対応するリレーショナルクエリをカスタマイズできます。例:

// find customers and bring back together their country and active orders
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

リレーションのリレーショナルクエリをカスタマイズする際には、リレーション名を配列キーとして指定し、無名関数を対応する配列値として使用します。無名関数は、リレーションのリレーショナルクエリを実行するために使用されるyii\db\ActiveQueryオブジェクトを表す$queryパラメータを受け取ります。上記のコード例では、注文ステータスに関する追加条件を追加することで、リレーショナルクエリを変更しています。

注意: リレーションを早期読み込み中にselect()を呼び出す場合は、リレーション宣言で参照されている列が選択されていることを確認する必要があります。それ以外の場合は、関連モデルが正しく読み込まれない可能性があります。例:

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer is always `null`. To fix the problem, you should do the following:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

リレーションを使用した結合

注意: この小節で説明されている内容は、MySQL、PostgreSQLなど、リレーショナルデータベースにのみ適用されます。

これまで説明してきたリレーショナルクエリは、主データのクエリを行う際に主テーブルの列のみを参照します。実際には、関連テーブルの列を参照する必要があることがよくあります。たとえば、少なくとも1つのアクティブな注文を持つ顧客を取得したい場合があります。この問題を解決するために、次のような結合クエリを作成できます。

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id`
// WHERE `order`.`status` = 1
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->select('customer.*')
    ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->with('orders')
    ->all();

注意: JOIN SQL文を含むリレーショナルクエリを作成する際には、列名を明確にすることが重要です。一般的な方法は、列名にそれぞれのテーブル名をプレフィックスとして付けることです。

しかし、より良いアプローチは、yii\db\ActiveQuery::joinWith()を呼び出すことで、既存のリレーション宣言を利用することです。

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

どちらのアプローチも、同じSQL文セットを実行します。ただし、後者のアプローチの方がはるかにクリーンで簡潔です。

デフォルトでは、joinWith()LEFT JOINを使用して主テーブルと関連テーブルを結合します。第3パラメータ$joinTypeを介して、異なる結合タイプ(例:RIGHT JOIN)を指定できます。目的の結合タイプがINNER JOINの場合は、代わりにinnerJoinWith()を呼び出すだけです。

joinWith()を呼び出すと、デフォルトで関連データが早期読み込みされます。関連データを読み込みたくない場合は、第2パラメータ$eagerLoadingfalseに指定できます。

注意: 早期読み込みを有効にしてjoinWith()またはinnerJoinWith()を使用する場合でも、関連データはJOINクエリの結果から読み込まれません早期読み込みに関するセクションで説明されているように、結合されたリレーションごとに追加のクエリが実行されます。

with()と同様に、1つまたは複数のリレーションを結合できます。リレーションクエリをオンザフライでカスタマイズできます。ネストされたリレーションを結合できます。with()joinWith()を組み合わせて使用できます。例:

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

2つのテーブルを結合する際に、JOINクエリのON部分にいくつかの追加条件を指定する必要がある場合があります。これは、yii\db\ActiveQuery::onCondition()メソッドを次のように呼び出すことで実行できます。

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
    },
])->all();

上記のクエリは、すべての顧客と、各顧客のアクティブなすべての注文を取得します。これは、少なくとも1つのアクティブな注文を持つ顧客のみを取得する以前の例とは異なります。

情報: yii\db\ActiveQueryonCondition()を介して条件で指定されている場合、クエリにJOINクエリが含まれている場合は、条件がON部分に配置されます。クエリにJOINが含まれていない場合、オン条件はクエリのWHERE部分に自動的に追加されます。そのため、関連テーブルの列を含む条件のみが含まれている可能性があります。

リレーションテーブルのエイリアス

前述のように、クエリでJOINを使用する場合は、列名を明確にする必要があります。そのため、多くの場合、テーブルのエイリアスが定義されます。リレーショナルクエリのエイリアスを設定するには、次のようにリレーションクエリをカスタマイズできます。

$query->joinWith([
    'orders' => function ($q) {
        $q->from(['o' => Order::tableName()]);
    },
])

ただし、これは非常に複雑に見え、関連オブジェクトのテーブル名をハードコーディングするか、Order::tableName()を呼び出す必要があります。バージョン2.0.7以降、Yiiはこれに対するショートカットを提供しています。リレーションテーブルのエイリアスを次のように定義して使用できるようになりました。

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

上記の構文は、単純なリレーションに対して機能します。ネストされたリレーションを結合する際に中間テーブルのエイリアスが必要な場合(例:$query->joinWith(['orders.product']))、次の例のようにjoinWith呼び出しをネストする必要があります。

$query->joinWith(['orders o' => function($q) {
        $q->joinWith('product p');
    }])
    ->where('o.amount > 100');

逆リレーション

リレーションの宣言は、多くの場合、2つのActive Recordクラス間で相互的です。たとえば、Customerordersリレーションを介してOrderと関連付けられ、Ordercustomerリレーションを介してCustomerと関連付けられます。

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

次のコードを考えてみましょう。

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

$customer$customer2は同じであると考えられますが、そうではありません!実際には、同じ顧客データを含んでいますが、異なるオブジェクトです。$order->customerにアクセスすると、新しいオブジェクト$customer2を設定するために、追加のSQL文が実行されます。

上記の例の最後のSQL文の冗長な実行を回避するには、次のように示すようにinverseOf()メソッドを呼び出すことで、customerorders逆リレーションであることをYiiに伝える必要があります。

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
    }
}

この変更されたリレーション宣言を使用すると、次のようになります。

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// No SQL will be executed
$customer2 = $order->customer;

// displays "same"
echo $customer2 === $customer ? 'same' : 'not the same';

注意: 逆リレーションは、結合テーブルを含むリレーションに対しては定義できません。via()またはviaTable()でリレーションが定義されている場合、inverseOf()をさらに呼び出すことはできません。

リレーションの保存

リレーショナルデータを使用する際には、異なるデータ間のリレーションを確立したり、既存のリレーションを破棄する必要があることがよくあります。これには、リレーションを定義する列に適切な値を設定する必要があります。Active Recordを使用すると、次のコードを書くことになります。

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

// setting the attribute that defines the "customer" relation in Order
$order->customer_id = $customer->id;
$order->save();

Active Recordは、このタスクをより適切に実行できるlink()メソッドを提供します。

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

link()メソッドでは、リレーション名と、リレーションを確立する対象となるActive Recordインスタンスを指定する必要があります。このメソッドは、2つのActive Recordインスタンスをリンクする属性の値を変更し、それらをデータベースに保存します。上記の例では、Orderインスタンスのcustomer_id属性をCustomerインスタンスのid属性の値に設定し、それをデータベースに保存します。

注意: 新しく作成された2つのActive Recordインスタンスをリンクすることはできません。

link()を使用することの利点は、リレーションが結合テーブルを介して定義されている場合にさらに明らかになります。たとえば、次のコードを使用してOrderインスタンスとItemインスタンスをリンクできます。

$order->link('items', $item);

上記のコードは、注文とアイテムを関連付けるために、order_item結合テーブルに自動的に行を挿入します。

情報: link() メソッドは、影響を受けるアクティブレコードインスタンスを保存する際に、データ検証を実行しません。このメソッドを呼び出す前に、入力データの検証を行うのはあなたの責任です。

link() の逆の操作は、2つのアクティブレコードインスタンス間の既存の関係を解除する unlink() です。例えば、

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

デフォルトでは、unlink() メソッドは、既存の関係を指定する外部キー値をnullに設定します。ただし、メソッドに$deleteパラメータをtrueとして渡すことで、外部キー値を含むテーブル行を削除することもできます。

関係に結合テーブルが関与している場合、unlink() を呼び出すと、結合テーブルの外部キーがクリアされるか、$deletetrueの場合、結合テーブルの対応する行が削除されます。

クロスデータベース関係

アクティブレコードを使用すると、異なるデータベースによって制御されるアクティブレコードクラス間の関係を宣言できます。データベースは異なるタイプ(例:MySQLとPostgreSQL、またはMS SQLとMongoDB)で、異なるサーバー上で実行できます。同じ構文を使用してリレーショナルクエリを実行できます。例えば、

// Customer is associated with the "customer" table in a relational database (e.g. MySQL)
class Customer extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // a customer has many comments
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

// Comment is associated with the "comment" collection in a MongoDB database
class Comment extends \yii\mongodb\ActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // a comment has one customer
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

このセクションで説明されているリレーショナルクエリの機能のほとんどを使用できます。

注意: joinWith() の使用は、クロスデータベースJOINクエリを許可するデータベースに限定されます。このため、MongoDBはJOINをサポートしていないため、上記の例ではこのメソッドを使用できません。

クエリクラスのカスタマイズ

デフォルトでは、すべてのアクティブレコードクエリはyii\db\ActiveQueryによってサポートされています。アクティブレコードクラスでカスタマイズされたクエリクラスを使用するには、yii\db\ActiveRecord::find()メソッドをオーバーライドし、カスタマイズされたクエリクラスのインスタンスを返す必要があります。例えば、

// file Comment.php
namespace app\models;

use yii\db\ActiveRecord;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

これで、Commentでクエリ(例:find()findOne())を実行したり、リレーション(例:hasOne())を定義したりするたびに、ActiveQueryではなくCommentQueryのインスタンスを呼び出すようになります。

これで、CommentQueryクラスを定義する必要があります。これは、クエリ構築エクスペリエンスを向上させるために、さまざまな創造的な方法でカスタマイズできます。例えば、

// file CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

class CommentQuery extends ActiveQuery
{
    // conditions appended by default (can be skipped)
    public function init()
    {
        $this->andOnCondition(['deleted' => false]);
        parent::init();
    }

    // ... add customized query methods here ...

    public function active($state = true)
    {
        return $this->andOnCondition(['active' => $state]);
    }
}

注意: 新しいクエリ構築メソッドを定義する際に、onCondition()を呼び出す代わりに、通常はandOnCondition()またはorOnCondition()を呼び出して追加の条件を追加する必要があります。これにより、既存の条件が上書きされることはありません。

これにより、次のクエリ構築コードを書くことができます。

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

ヒント: 大規模なプロジェクトでは、クエリ関連のコードの大部分を保持するためにカスタマイズされたクエリクラスを使用し、アクティブレコードクラスをクリーンに保つことをお勧めします。

Commentに関するリレーションを定義したり、リレーショナルクエリを実行したりする場合にも、新しいクエリ構築メソッドを使用できます。

class Customer extends \yii\db\ActiveRecord
{
    public function getActiveComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id'])->active();
    }
}

$customers = Customer::find()->joinWith('activeComments')->all();

// or alternatively
class Customer extends \yii\db\ActiveRecord
{
    public function getComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

$customers = Customer::find()->joinWith([
    'comments' => function($q) {
        $q->active();
    }
])->all();

情報: Yii 1.1では、スコープという概念がありました。スコープはYii 2.0では直接サポートされなくなりましたが、カスタマイズされたクエリクラスとクエリメソッドを使用して同じ目標を達成できます。

追加フィールドの選択

アクティブレコードインスタンスがクエリ結果から設定されると、その属性は受信したデータセットから対応する列値で埋められます。

クエリから追加の列または値を取得し、アクティブレコードに格納することができます。たとえば、ホテルで利用可能な部屋に関する情報を格納するroomという名前のテーブルがあるとします。各部屋は、lengthwidthheightフィールドを使用して幾何学的サイズに関する情報を格納します。ボリューム降順ですべての利用可能な部屋のリストを取得する必要があるとします。そのため、レコードをその値でソートする必要があるため、PHPを使用してボリュームを計算することはできませんが、リストにvolumeも表示したいと考えています。この目標を達成するには、volume値を格納する追加フィールドをRoomアクティブレコードクラスに宣言する必要があります。

class Room extends \yii\db\ActiveRecord
{
    public $volume;

    // ...
}

次に、部屋のボリュームを計算し、ソートを実行するクエリを作成する必要があります。

$rooms = Room::find()
    ->select([
        '{{room}}.*', // select all columns
        '([[length]] * [[width]] * [[height]]) AS volume', // calculate a volume
    ])
    ->orderBy('volume DESC') // apply sort
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by SQL
}

追加フィールドを選択する機能は、集計クエリに非常に役立ちます。顧客のリストを、彼らが行った注文の数とともに表示する必要があるとします。まず、ordersリレーションとカウント格納用の追加フィールドを持つCustomerクラスを宣言する必要があります。

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

次に、注文を結合してその数を計算するクエリを作成できます。

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

この方法を使用する際の欠点は、情報がSQLクエリで読み込まれない場合、別途計算する必要があることです。したがって、追加のselect文なしで通常のクエリを介して特定のレコードが見つかった場合、追加フィールドの実際の値を返すことができなくなります。新しく保存されたレコードでも同じことが起こります。

$room = new Room();
$room->length = 100;
$room->width = 50;
$room->height = 2;

$room->volume; // this value will be `null`, since it was not declared yet

__get()__set()マジックメソッドを使用して、プロパティの動作をエミュレートできます。

class Room extends \yii\db\ActiveRecord
{
    private $_volume;
    
    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }
    
    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }
        
        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }
        
        return $this->_volume;
    }

    // ...
}

selectクエリがボリュームを提供しない場合、モデルはモデルの属性を使用して自動的に計算できます。

定義されたリレーションを使用して、集計フィールドも計算できます。

class Customer extends \yii\db\ActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // calculate aggregation on demand from relation
        }

        return $this->_ordersCount;
    }

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

このコードでは、'ordersCount'が'select'文に存在する場合、Customer::ordersCountはクエリ結果によって設定され、そうでない場合はCustomer::ordersリレーションを使用してオンデマンドで計算されます。

このアプローチは、特に集計のための、いくつかのリレーショナルデータのショートカットの作成にも使用できます。例えば

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Defines read-only virtual property for aggregation data.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }
        
        return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted'];
    }

    /**
     * Declares normal 'orders' relation.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    /**
     * Declares new relation based on 'orders', which provides aggregation.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // outputs aggregation data from relation without extra query due to eager loading
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // output aggregation data from lazy loaded relation

タイプミスを見つけたか、このページの改善が必要だと考えますか?
githubで編集する !