0 follower

アクティブレコード

Yii DAOが実質的にどんなデータベース関連のタスクでも取り扱うことができるものの、 我々は一般のCRUD(生成、読み出し、変更、削除)オペレーションを実行するSQL記述を書くことで自身の時間の90%を過ごしています。 さらに、SQL文とコードが混ざり合うために、コードを保守することが困難です。 これらの問題を解決するために、我々はアクティブレコードを使うことができます。

アクティブレコード(AR)は、人気があるO/Rマッピング(ORM)テクニックです。 各々のARクラスはアトリビュートがARクラスのプロパティとして描写されるデータベーステーブル(またはビュー)を表します。そして、ARインスタンスはそのテーブルでの列を表します。 共通のCRUDオペレーションは、ARメソッドとして実装されます。 その結果、我々はよりオブジェクト指向方向でデータにアクセスすることができます。 例えば、我々はPostテーブルに新しい列を挿入するために、以下のコードを使用することができます:

$post=new Post;
$post->title='sample post';
$post->content='post body content';
$post->save();

以下に、CRUDオペレーションを実行するために、ARを準備して使う方法を解説します。 次のセクションでデータベースリレーションに対処するためにARを使う方法を示します。 単純化するため、このセクションでは例として以下のデータベーステーブルを使います。 もしMySQLをお使いの場合には以下のSQLにおいて、AUTOINCREMENTAUTO_INCREMENTに修正することに注意してください。

CREATE TABLE Post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    createTime INTEGER NOT NULL
);

注意: ARはあらゆるデータベース関連のタスクを解くものではありません。 PHPの中でデータベーステーブルをモデル化し、複雑なSQLを含まないクエリを実行するために、最も使われます。 複雑なシナリオのためにはYii DAOを使うべきです。

1. DB接続の確立

ARは、DB関連のオペレーションを実行するために、DB接続に依存します。 デフォルトではdbアプリケーションコンポーネントは、DB接続として用いられる必要なCDbConnectionインスタンスを与えると仮定されます。 以下のアプリケーション構成を例として示します:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // turn on schema caching to improve performance
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

ヒント: アクティブレコードが列情報を決定するためにテーブルのメタデータに頼るので、メタデータを読んで分析する時間かかります。 データベースのスキーマが変わりそうにないならば、CDbConnection:schemaCachingDurationプロパティを0よりも大きな値に構成することによってスキーマキャッシングを行わなけなければなりません。

ARに対するサポートは、DBMSによって制限されます。現在以下のDBMSがサポートされています:

注意: Microsoft SQL Server のサポートは、バージョン 1.0.4 以降で有効です。 また、Oracle のサポートは、バージョン 1.0.5 以降で有効です。

db以外のアプリケーションコンポーネントを使いたいか、あるいはARを使う複数のデータベースで作業することを望む場合はCActiveRecord:getDbConnection()をオーバライドしなければなりません。 CActiveRecordクラスは、すべてのARクラスのためのベースクラスです。

ヒント: ARで複数のデータベースで作業するには2つの方法があります。 データベースのスキーマが異なるならば、あなたはgetDbConnection()の異なる実装を行った異なるベースARクラスを作成しても良いでしょう。 あるいは、ダイナミックに静的変数CActiveRecord:dBを変えることはより良い考えです。

2. ARクラス定義

データベーステーブルにアクセスするために、最初にCActiveRecordを継承ことによってARクラスを定義する必要があります。 それぞれのARクラスは一つのデータベーステーブルを表します。 そして、ARインスタンスはそのテーブルでの列を表します。 以下の例は、Postテーブルを表しているARクラスのために必要な最小のコードを示します。

class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
}

ヒント: ARクラスは多くの場所でしばしば参照されるため、一つずつインクルードするのではなく、ARクラスを含んでいるディレクトリごと組込むことができます。 例えば、全てのARクラスファイルがprotected/modelsの下にあるなら、以下のようにアプリケーションを設定することができます:

return array(
  'import'=>array(
      'application.models.*',
  ),
);

デフォルトでは、ARクラス名はデータベーステーブル名と同じです。 もし異なる場合はtableName()メソッドをオーバライドしてください。 [model()|CActiveRecord:model]メソッドはあらゆるARクラス(まもなく説明されます)のためにそのように宣言されます。

テーブル列の列値は、対応するARクラスインスタンスのプロパティとしてアクセスされます。 例えば、以下のコードは、タイトル列(アトリビュート)をセットします:

$post=new Post;
$post->title='a sample post';

Postクラスでは決してはっきりとtitleプロパティを宣言していませんが、上記のコードでそれにまだアクセスすることができます。 これは、titlePostテーブルの列であるからであり、CActiveRecordがそれをPHPの魔法の方法である__get()の助けを借りて、プロパティとしてアクセスできるようにします。 同様な方法で存在しない列にアクセスしようとすると、例外が発生します。

情報: 読みやすいように、キャメルケースでテーブルカラム(列)名をつけることを提案します。 特にカラム名は、名前の最初を除き、各々の語の先頭を大文字にして、空白無しで繋げ合せることによって作られます。 たとえば、行の作成時間を保存するカラム名は createTime を使います。 テーブル名は、あなたの嗜好次第です。 このガイドでは、最初の文字が大文字である事を除いて、キャメルケース命名規則に従います。 たとえば、投稿データを保存するテーブルには、Post という名前を用います。 注意:しかしながら、キャメルケースを使うとある種のDBMS、例えばMySQLでは不便になることがあります。というのは、異なるOS上では異なる動作となるからです。

3. レコードの作成

新しい列をデータベーステーブルに挿入するため、対応するARクラスの新しいインスタンス作り、 テーブル列に関連したプロパティをセットし、 挿入を完了するためsave()メソッドを呼びます。

$post=new Post;
$post->title='sample post';
$post->content='content for the sample post';
$post->createTime=time();
$post->save();

テーブルのプライマリキーが自動増加するならば、挿入の後ではARインスタンスは更新されたプライマリキーを含みます。 たとえ表立ってそれを変えないとしても、上記の例においてはidプロパティは新しく挿入された記事のプライマリーキーとなる値を反映します。

列がテーブルスキーマ中の静的デフォルト値(例えばストリング、数)で定められるならば、 インスタンスが生成された後、ARインスタンスの対応するプロパティは自動的にそのような値を持ちます。 このデフォルト値を変える1つの方法は、ARクラスで明白にプロパティを宣言することです:

class Post extends CActiveRecord
{
    public $title='please enter a title';
    ......
}
 
$post=new Post;
echo $post->title;  // this would display: please enter a title

バージョン1.0.2からは、レコードがデータベースにセーブされる(挿入か更新のいずれか)前に、アトリビュートには[CDbExpression]タイプの値を割り当てることができます。 例えば、MySQLのNOW()関数によって返されるタイムスタンプを保存するために、以下のコードを使用することができます:

$post=new Post;
$post->createTime=new CDbExpression('NOW()');
// $post->createTime='NOW()'; は'NOW()'が文字列として扱われるため、
// 動作しません
$post->save();

ヒント: AR が複雑な SQL 文を書くことなく、データベース操作を実行させる際、 しばしば、AR 下でどんな SQL 文が実行されるかを知りたい場合があります。 これは Yii の ロギング機能 により実現可能です。 たとえば、アプリケーション初期構成で、CWebLogRoute をつけると、実行された SQL 文を各ウェブページの終わりに表示させられます。 バージョン 1.0.5 以降、アプリケーション初期構成で、CDbConnection::enableParamLogging を true に設定すると、 SQL 文と結合したパラメータ値もログされます。

4. レコードの読み出し

データをデータベーステーブルから読むためには、findメソッドのうちの1つを以下のように呼出します。

// 指定された条件を満足する最初の列を見つけます
$post=Post::model()->find($condition,$params);
// 指定されたプライマリキーを持つ列を見つけます
$post=Post::model()->findByPk($postID,$condition,$params);
// 指定されたアトリビュート値を持つ列を見つけます
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// 指定されたSQLにより列を見つけます
$post=Post::model()->findBySql($sql,$params);

上記においてPost::model()を用いてfindメソッドを呼出します。 静的メソッドmodel()が全てのARクラスで必要なことを覚えてください。 メソッドは、オブジェクトコンテキストにおけるクラスレベルメソッド(静的クラスメソッドに類似したもの)にアクセスするために用いられるARインスタンスを返します。

もしfindメソッドがクエリ条件を満足する列を見つけた場合、Postインスタンスが返されます。そのプロパティはテーブル列の対応する項目値となります。そのため、普通のオブジェクトのプロパティを読むように、例えば、echo $post->title;のように値を読むことができます。

与えられたクエリ条件でデータベースからみつけることができない場合には、findメソッドはナルを返します。

findを呼出す際には、クエリ条件を指定するため$condition$paramsを用います。 ここで、$conditionはSQL文のWHERE句を表す文字列であり、$paramsは配列パラメータであり、それらの値は$conditionのプレースホルダに対応させる必要があります。例えば、

// postIDが10である列を見つけます
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

$conditionはもっと複雑なクエリ条件に使うことができます。$conditionは文字列ではなくCDbCriteriaインスタンスを使うことができ、それはWHERE句だけでない他の条件を使用することができます。例えば、

$criteria=new CDbCriteria;
$criteria->select='title';  // 'title'行のみを選択します
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $paramsは不要です

上記に見られるように、CDbCriteriaをクエリ条件として使用する場合には$paramsパラメータは不要です。というのはそれはCDbCriteria中で指定されるからです。

別の方法として、CDbCriteriafindメソッドに配列を渡します。配列のキーと値はクライテリアの行の名前と値にそれぞれ対応します。上記の例は以下のように書き換えられます。

$post=Post::model()->find(array(
    'select'=>'title',
    'condition'=>'postID=:postID',
    'params'=>array(':postID'=>10),
));

情報: 指定された値によってクエリ条件をいくつかの列にマッチさせるとき、findByAttributes()を使用します。 $attributesパラメータは列名によりインデックスされた値の配列です。 ある種のフレームワークでは、このタスクはfindByNameAndTitleのようなメソッドをコールすることで達成されます。 このアプローチは魅力的ではありますが、しばしば混乱と列名のケースセンシティブの問題により競合を招きます。

指定されたクエリ条件が複数の行のデータにマッチした場合は、我々は以下のfindAllメソッドによりまとめることができます。 それぞれの条件は既に述べたfindメソッドにより対応されます。

// 指定された条件を満足する全ての行を見つけます
$posts=Post::model()->findAll($condition,$params);
// 指定された主キーを満足する全ての行を見つけます
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// 指定されたアトリビュート値をキーを満足する全ての行を見つけます
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// 指定されたSQLにより満足する全ての行を見つけます
$posts=Post::model()->findAllBySql($sql,$params);

もしもクエリ条件に何もマッチしなければ、findAllは空の配列を返します。これはfindと異ります。 もし何も見つけられなかった場合はnullを返すからです。

findfindAllが上記で示される違いはあっても、以下のようなメソッドが便宜上提供されます。

// 指定された条件を満足する行の数を得ます
$n=Post::model()->count($condition,$params);
// 指定されたSQLを用いた行の数を得ます
$n=Post::model()->countBySql($sql,$params);
// 指定された条件を満足する行が一つでもあるかをチェックします
$exists=Post::model()->exists($condition,$params);

5. レコードの更新

ARインスタンスに列の値が読み出された後、その値を更新し、元のデータベーステーブルに格納することができます。

$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // 変更をテーブルに格納します

見てきたように、挿入操作と更新操作について、同じメソッドsave()を使用します。 もしARインスタンスがnew演算子によって生成された場合、save()を呼ぶとデータベーステーブルに新しい行が挿入されます。一方もしARインスタンスが何らかのfindfindAllメソッドの結果である場合には、save()を呼ぶと既存の行が更新されます。実際にはARインスタンスが新しいか否かはCActiveRecord::isNewRecordを用いて伝えることができます。

いくつかのデータベーステーブルの行を前もってロードすることなしに更新することが可能です。ARは以下のような便利なクロスレベルのメソッドをこの目的のため提供します。

// 指定された条件を満足する行を更新します
Post::model()->updateAll($attributes,$condition,$params);
// 指定された条件と主キーを満足する行を更新します
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// 指定された条件を満足するカウンタ行を更新します
Post::model()->updateCounters($counters,$condition,$params);

上記において、$attributesは列名によってインデックスされた列値の配列です。 $countersは列名によってインデックスされた増加値の配列です。 そして$condition$params`は以前のサブセクションで示されたものです。

6. レコードの削除

ARインスタンスに行が読み出されている場合、この行を削除することができます。

$post=Post::model()->findByPk(10); // IDが10という記事が存在すると仮定します
$post->delete(); // その記事をテーブルから削除します

削除後でもARインスタンスは変更されていないことに注意してください。その一方で対応するデータベーステーブルの行は無くなっています。

以下のクラスレベルのメソッドが、前もってロードすることなしに行を削除するために提供されています。

// 指定された条件を満足する行を削除します
Post::model()->deleteAll($condition,$params);
// 指定された条件と主キーを満足する行を削除します
Post::model()->deleteByPk($pk,$condition,$params);

7. データ検証

行が挿入されたり更新される場合には、列の値が指定されたルールに適合しているかをチェックする必要があります。 これは列の値がエンドユーザによって与えられる場合には特に重要になります。 一般に、クライアント側から来る値を一切信じてはなりません。

ARはsave()が呼ばれた場合に自動的にデータ検証を行います。 検証はARクラスのrules()メソッド中で指定されるルールに基いて行われます。 もっと詳しい検証ルールの設定法に関してはバリデーションルールの宣言 の章を参照してください。 以下はレコードの格納をする場合の典型的なワークフローです。

if($post->save())
{
    // データはバリッドであり正常に格納もしくは更新されました
}
else
{
    // データはバリッドではなく、エラーメッセージを表示するにはgetErrors()を呼びます
}

もし、挿入や更新されるべきデータがエンドユーザによってHTMLフォームの中からサブミットされた場合は 対応するARプロパティを割当る必要があります。これは以下のように行います。

$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();

もしたくさんの列がある場合には、この割当ては非常に長いリストとなってしまいます。 これはattributesを利用することで以下に示すように軽減することができます。 より詳細はアトリビュート割り当ての安全化章とアクションの生成章に見ることができます。

// $_POST['Post']は列の値の、列名でインデックスされた配列とします
$post->attributes=$_POST['Post'];
$post->save();

8. レコードの比較

テーブルの列のように、ARインスタンスは主キーによってユニークに同定されます。 そのため、2つのARインスタンスを比較することは、それらが同じARクラスに属すると仮定するとき、 単にそれらの主キーを比較するだけです。しかしながらより簡単な方法があり、それはCActiveRecord::equals()を呼ぶことです。

情報: 他のフレームワークのAR実装と異り、YiiはARにおいて複合された主キーをサポートします。 複合された主キーは2つ以上の列から構成されます。Yiiでは主キー値は対応する配列として表現されます。 primaryKeyはARインスタンスの主キー値を与えます。

9. カスタマイゼーション

CActiveRecordクラスは場所取りメソッドを提供しますが、ワークフローにおいてそれを継承する子クラスのメソッドによりオーバライドされます。

  • beforeValidateafterValidate: これらは検証前と検証後に実行されます。

  • beforeSaveafterSave: これらはARインスタンスの格納前と格納後に実行されます。

  • beforeDeleteafterDelete: これらはARインスタンスの削除前と削除後に実行されます。

  • afterConstruct: これはnew演算子により新なARインスタンスが作成されるたびに実行されます。

  • afterFind: これはクエリの結果によりARインスタンスが作成されるたびに実行されます。

10. ARを用いたトランザクション

どのARインスタンスにもdbConnectionという名のCDbConnectionクラスのインスタンスであるプロパティがあります。 それを用いると以下のコードのように、YiiのDAOにより提供されるトランザクション機能を使うことができます。

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // findとsaveは2つのステップなので、他のリクエストにより順番が反転しかねません
    // 従って、一貫性と完全性を確実にするために、トランザクションを使用します
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
}
catch(Exception $e)
{
    $transaction->rollBack();
}

11. Named Scope

注意: Named Scope のサポートは、バージョン 1.0.5 以降で有効です。 Named Scope の発想元は、Ruby on Rails から来ました。

Named Scope は、他の Named Scope に結合して、アクティブレコードクエリーに適用できる、 Named(名前の付けられた)クエリー基準(クライテリア)のことです。

Named Scope は主に、名前 - 基準の対として、CActiveRecord::scopes() メソッドで宣言されます。 下記コードでは、Post モデルクラスで publishedrecently の 2 つの Named Scope を宣言しています:

class Post extends CActiveRecord
{
    ......
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'createTime DESC',
                'limit'=>5,
            ),
        );
    }
}

各 Named Scope は、CDbCriteria インスタンスを初期化するのに使用できる 配列として宣言されます。 例えば、recently Named Scope は、order プロパティを createTime DESC に、 limit プロパティを 5 に(最新の 5 件を返すはずであるクエリー基準として解釈される) 指定します。

Named Scope は、find メソッド呼び出しの修飾句としてとして主に使用されます。 いくつかの Named Scope が、連結されて使用されると、より絞り込まれたクエリ結果のセットが返ります。 例えば、最近公開された(published)投稿を見つけるために、下記コードを利用できます:

$posts=Post::model()->published()->recently()->findAll();

一般的に、Named Scope は find メソッド呼び出しの左側に現れなくてはなりません。 それらは find メソッド呼び出しへ渡されたものを含むクエリー基準(他の基準と結合されて)を提供します。 実際は、クエリーにフィルタのリストを加えるのとほぼ同じです。

バージョン 1.0.6 移行、Named Scope は updatedelete メソッドと共に使用することもできます。 たとえば、下記コードは全ての最近公開された投稿を削除します:

Post::model()->published()->recently()->delete();

注意: Named Scope はクラスレベルのメソッドと共にのみ使用できます。すなわち、メソッドは ClassName::model() を使用してコールしなければなりません。

パラメータ化された Named Scopes

Named Scope はパラメータ化することが可能です。 例えば、recently Named Scope によって指定された投稿数をカスタマイズ出来るようにしたいとします。 その場合、CActiveRecord::scopes で Named Scope を宣言する代わりに、 その Named Scope と同じ名前で、新しいメソッドを定義します:

public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'createTime DESC',
        'limit'=>$limit,
    ));
    return $this;
}

その後、最近公開された 3 つの投稿を検索するには、下記のように使用できます:

$posts=Post::model()->published()->recently(3)->findAll();

もし、上記でパラメータ 3 を渡さなければ、デフォルトで最近公開された 5 つの投稿が検索されます。

デフォルト Named Scope

モデルクラスにはデフォルトのNamed Scopeを設定し、 (リレーションを含めた)すべてのクエリに適用するようにすることができます。 例えば、複数の言語で利用できるウェブサイトでは、利用中のユーザが指定した言語のコンテンツだけを表示したいということがありうるでしょう。 サイトコンテンツを取り出すクエリはたくさんあるでしょうから、デフォルト Named Scope を定義して、この問題を解決することができます。 そのためにCActiveRecord::defaultScopeメソッドを以下のようにオーバーライドします。

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

これで、次のようにメソッドを呼ぶことで、自動的に上記で定義されたクエリクライテリアが利用つかわれます。

$contents=Content::model()->findAll();