バーチャルフィールドは任意のSQL表現を作り、それをモデルのフィールドとして割り当てることを可能にします。これらのフィールドは保存することはできませんが、読み込み操作時にモデルの他のフィールドと同じように扱われることになります。また、モデルの他のフィールドと同じように、モデルのキーを元に配置されます。
バーチャルフィールドを作るのは簡単です。各々のモデルに、フィールド => 式 という内容の配列を用いた $virtualFields プロパティを定義することができます。MySQLを用いたバーチャルフィールドの定義の例としては、以下のようになります。
public $virtualFields = array(
'name' => 'CONCAT(User.first_name, " ", User.last_name)'
);
PostgreSQLだと、以下のようになります。
public $virtualFields = array(
'name' => 'User.first_name || \' \' || User.last_name'
);
これを行った後、find操作で取得したデータのUserには name キーに連結された結果が格納されているでしょう。データベースにバーチャルフィールドと同じ名前のカラムを作成するのは賢明ではありません。これはSQLエラーを引き起こす場合があります。
User.first_name のように完全に修飾することは、常に有用というわけではありません。もし規約に従わない場合(すなわち、他のテーブルへの関連を複数持つ場合)、エラーになります。この場合、 first_name || \' \' || last_name のように、モデル名なしで使用するほうがいいかもしれません。
バーチャルフィールドを作るのは至極簡単ですが、バーチャルフィールドとの対話はいくつかの異なった方法でなされます。
Model::hasField() は、モデルが実際に持っているフィールドを一番目の引数で渡すと true を返します。hasField() の二番目の引数を true にすることによって、バーチャルフィールドもチェックされるようになります。上記の例を用いれば、
$this->User->hasField('name'); // 「name」というフィールドが実在しないため false を返します。
$this->User->hasField('name', true); // 「name」というバーチャルフィールドがあるため true を返します。
このメソッドは、フィールド・カラムがバーチャルフィールドか実在するフィールドかどうかを判定するときに用いられます。カラムがバーチャルであるときに true を返します。
$this->User->isVirtualField('name'); //true
$this->User->isVirtualField('first_name'); //false
このメソッドは、バーチャルフィールドを構成するSQL表現にアクセスするために用いられます。引数が与えられない場合、そのモデルのすべてのバーチャルフィールドを返します。
$this->User->getVirtualField('name'); // 'CONCAT(User.first_name, ' ', User.last_name)' を返します。
先に述べたように、 Model::find() はモデルの他のフィールドと同じようにバーチャルフィールドを扱います。返り値のセットの中で、バーチャルフィールドの値はモデルのキーの下に置かれます。
$results = $this->User->find('first');
// 返り値は以下のものを含みます。
array(
'User' => array(
'first_name' => 'Mark',
'last_name' => 'Story',
'name' => 'Mark Story',
//more fields.
)
);
バーチャルフィールドは find 時に普通のフィールドと同じように振舞うため、Controller::paginate() はバーチャルフィールドでもソートすることができます。
自身の名前と違うエイリアスを持つモデルとバーチャルフィールドを同時に用いた場合、結びつけられたエイリアスが反映されないという問題にぶつかることがあります。別名を持つようなモデルでバーチャルフィールドを使用するには、モデルのコンストラクタでバーチャルフィールドを定義するのがベストでしょう。
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->virtualFields['name'] = sprintf('CONCAT(%s.first_name, " ", %s.last_name)', $this->alias, $this->alias);
}
これで、モデルにどんなエイリアスを与えても、バーチャルフィールドはうまく動くことでしょう。
SQLクエリ中で直接使用される関数は、返されるデータがモデルのデータと同じ配列に格納されるのを防ぎます。例えば以下のようなとき
$this->Timelog->query("SELECT project_id, SUM(id) as TotalHours FROM timelogs AS Timelog GROUP BY project_id;");
戻り値はこのようになります。
Array
(
[0] => Array
(
[Timelog] => Array
(
[project_id] => 1234
)
[0] => Array
(
[TotalHours] => 25.5
)
)
)
もし TotalHours を Timelog 配列にグループ化したい場合、集計カラムのためのバーチャルフィールドを指定する必要があります。永続的にモデルに宣言しなくても、その場で新しいバーチャルフィールドを追加することができます。別のクエリがバーチャルフィールドを使用しようとする場合、デフォルト値として 0 を与えます。それが発生した場合、 0 が TotalHours 列に入ります。
$this->Timelog->virtualFields['TotalHours'] = 0;
また、バーチャルフィールドを追加することに加えて、カラムを MyModel__MyField の形式で別名にする必要があります。
$this->Timelog->query("SELECT project_id, SUM(id) as Timelog__TotalHours FROM timelogs AS Timelog GROUP BY project_id;");
バーチャルフィールドを設定した後クエリを再度実行すると、きれいな値のグループになるはずです。
Array
(
[0] => Array
(
[Timelog] => Array
(
[project_id] => 1234
[TotalHours] => 25.5
)
)
)
virtualFields の実装はわずかな制限があります。まず、関連モデルの「conditions」、「order」、「fields」に virtualFields を用いることが出来ません。やってみると、ORMがフィールドを置き換えないため、まずSQLエラーが起きてしまいます。これは関連モデルを見つけられるかもしれない深さを見積もるのが難しいということに起因します。
この実装の問題に対する一般的な回避策としては、 利用する必要がある時に virtualFields をあるモデルから別のモデルにコピーすることです。
$this->virtualFields['name'] = $this->Author->virtualFields['name'];
もしくは以下のようにします。
$this->virtualFields += $this->Author->virtualFields;