アクティブレコード (AR)

Yii DAO は実質的にどんなデータベース関連のタスクでも取り扱うことができますが、 現実には、私たちはありきたりの CRUD (作成 Create, 読み出し Read, 更新 Update, 削除 Delete) 操作を実行する SQL 文を書くことに自分の時間の 90% を使うことになるでしょう。 さらに、SQL 文が混ざると、コードを保守することが困難になります。 これらの問題を解決するために、我々はアクティブレコードを使うことができます。

アクティブレコード (AR) は、人気があるオブジェクト関係マッピング (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 は、データベース関連のタスクの全てを解くためのものではありません。 AR は、PHP の構文でデータベーステーブルをモデル化し、複雑な SQL を含まないクエリを実行するのに最も適しています。 複雑なシナリオのためには Yii DAO を使うべきです。

1. DB 接続の確立

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

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // パフォーマンスを向上させるために、スキーマキャッシュを有効にできます
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

ヒント: アクティブレコードはカラムの情報を決定するためにテーブルのメタデータに頼ります。 そのため、メタデータを読んで分析するのに時間がかかります。 データベースのスキーマが変わりそうにないならば、CDbConnection::schemaCachingDuration プロパティを 0 よりも大きな値にして、スキーマキャッシングを有効にしたほうがいいでしょう。

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

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

ヒント: AR で複数のデータベースで作業するには二つの方法があります。 データベースのスキーマが異なるならば、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 インスタンスのプロパティとしてアクセスできます。 例えば、以下のコードは、title カラム (属性) をセットします。

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

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

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

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

public function primaryKey()
{
    return 'id';
    // 複合プライマリキーの場合は、次のような配列を返します
    // 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 インスタンスの対応するプロパティは、インスタンスが生成された後に、自動的にそのような値を持つようになります。 このデフォルト値を変える一つの方法は、AR クラスで明示的にプロパティを宣言することです。

class Post extends CActiveRecord
{
    public $title='please enter a title';
    ......
}
 
$post=new Post;
echo $post->title;  // '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メソッドのうちの一つを以下のように呼び出します。

// 指定された条件を満たす最初の行を見つけます
$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 パラメータは不要です。 というのは、$paramsCDbCriteria の中で指定されるからです。

CDbCriteria を使用する別の方法として、find メソッドに配列を渡す方法があります。 配列のキーと値が、クライテリアのプロパティの名前と値にそれぞれ対応します。 上記の例は以下のように書き換えることが出来ます。

$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 を返すからです。

上記の find メソッドと findAll メソッドの他に、以下のメソッドが便宜上用意されています。

// 指定された条件を満たす行数を取得します
$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 インスタンスが find メソッドや findAll メソッドの結果である場合には、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 インスタンスは主キーによってユニークに識別されます。 そのため、二つの AR インスタンスを比較することは、それらが同じ AR クラスに属すると仮定すると、単にそれらの主キーを比較するだけで済みます。 しかしながら、もっと簡単な方法は CActiveRecord::equals() を呼ぶことです。

情報: 他のフレームワークの AR 実装と異り、Yii は AR において複合プライマリキーをサポートします。 複合プライマリキーは二つ以上のカラムから構成されます。 これに呼応して、Yii では複合プライマリキーの値は配列として表現されます。 primaryKey が AR インスタンスのプライマリキーの値です。

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

CActiveRecord クラスは、ワークフローをカスタマイズするために子クラスでオーバーライド出来る、いくつかのプレースホルダメソッドを持っています。

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

  • beforeSaveafterSave: これらは AR インスタンスの保存の実行前と実行後に呼び出されます。

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

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

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

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

10. AR でトランザクションを使う

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

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // find と save の二つのステップの間に他のリクエストが割って入る可能性があります
    // このため、一貫性と完全性を確保するために、トランザクションを使用します
    $post=$model->findByPk(10);
    $post->title='new post title';
    if($post->save())
        $transaction->commit();
    else
        $transaction->rollback();
}
catch(Exception $e)
{
    $transaction->rollback();
    throw $e;
}

11. 名前付きスコープ

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

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

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

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 クエリを作成することが出来ません。

Be the first person to leave a comment

Please to leave your comment.