アクティブレコード

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

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

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

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

CREATE TABLE tbl_post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    create_time 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がサポートされています:

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

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

2. ARクラス定義

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

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

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

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

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

情報: テーブルプレフィクス機能を使うためには ARクラスのためのtableName()メソッドは以下のようにオーバライドする必要 があります。

public function tableName()
{
    return '{{post}}';
}

フルに定義されたテーブル名を返す代わりに、プレフィックスを除いたテーブル名を使い、二重波括弧で囲んで返します。

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

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

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

情報: このガイドでは、すべてのテーブル名やカラム名には小文字を使用します。 これは、DBMSによってケースの扱いが異るからです。例えば、PostgreSQLではデフォルトではカラム名はケースセンシティブです。カラム名が大文字小文字を含んでいる場合には、クエリ条件の中でカラム名を記述するときにはクオートしなければなりません。 小文字のみを用いることでこのような問題を回避することができます。

ARはテーブルのプライマリキーに依存しています。もしテーブルがプライマリキーを持たない場合、対応するクラスにおいて、どのカラムがプライマリキーなのか、primaryKey()メソッドを以下のようにオーバライドすることが必要です。

public function primaryKey()
{
    return 'id';
    // For composite primary key, return an array like the following
    // return array('pk1', 'pk2');
}

3. レコードの作成

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

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

テーブルのプライマリキーがauto-incrementなら、挿入した後のARインスタンスには最新のプライマリキーが入ります。 上の例では、idプロパティを明示的に変更しなくても、新しく挿入したポストのプライマリキーの値が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

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

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

ヒント: AR が複雑な SQL 文を書くことなく、データベース操作を実行させる際、 しばしば、AR 下でどんな SQL 文が実行されるかを知りたい場合があります。 これは Yii の ロギング機能 により実現可能です。 たとえば、アプリケーション初期構成で、CWebLogRoute をつけると、実行された SQL 文を各ウェブページの終わりに表示させられます。 アプリケーション初期構成で、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メソッドはnullを返します。

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

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

注: 上記において、ある種のDBMSではpostIDカラムを参照するためにエスケープする必要があります。 例えば、もしPostgreSQLを使っている場合は"postID"=:postIDのように記述する必要があります。 というのはPostgreSQLはデフォルトではカラム名をケースセンシティブで取り扱うからです。

$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と異ります。 もし何も見つけられなかった場合、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インスタンスが作成されるたびに実行されます。

  • [beforeFind: これはARファインダがクエリ(例 find(), findAll())を実行する前に実行されます。

  • 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. 名前付きスコープ

注意: 名前付きスコープの元になる発想は、Ruby on Rails から来ました。

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

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

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

それぞれの名前付きスコープは、CDbCriteria インスタンスを初期化するのに使用できる 配列として宣言されます。 例えば、recently という名前付きスコープは、order プロパティを create_time DESC に、 limit プロパティを 5 に指定します。これは、最新の 5 件の投稿記事を返すクエリ基準として解釈されます。

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

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

一般的に、名前付きスコープは find メソッド呼び出しの左側に現れなくてはなりません。 それぞれの名前付きスコープがクエリ基準を提供して、それらがすべて、find メソッドの呼出しに渡されたクエリ基準も含めて結合されます。 実質的な効果は、クエリにフィルタのリストを加えるのとほぼ同じです。

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

パラメータ化された名前付きスコープ

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

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

こうすると、最近公開された 3 つの投稿記事を検索するために、下記の文を使用できるようになります:

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

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

デフォルトスコープ

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

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

これで、次のようにメソッドを呼ぶことで、自動的に上記で定義されたクエリ基準が使用されます。

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

注意: デフォルトスコープと名前付きスコープは SELECT クエリにのみ適用されます。INSERTUPDATEDELETE クエリに対しては無視されます。

さらに、デフォルトスコープまたは名前付きスコープを宣言するとき、そのスコープを宣言するメソッドの中では AR クラスを使って DB クエリを作成することが出来ません。

$Id: database.ar.txt 3318 2011-06-24 21:40:34Z qiang.xue $

Be the first person to leave a comment

Please to leave your comment.