バーチャルフィールドは任意の 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 にすることによって、バーチャルフィールドもチェックされるように なります。上記の例を用いれば:
// 「name」というフィールドが実在しないため false を返します。
$this->User->hasField('name');
// 「name」というバーチャルフィールドがあるため true を返します。
$this->User->hasField('name', true);
このメソッドは、フィールド・カラムがバーチャルフィールドか実在するフィールドかどうかを 判定するときに用いられます。カラムがバーチャルであるときに true を返します。
$this->User->isVirtualField('name'); //true
$this->User->isVirtualField('first_name'); //false
このメソッドは、バーチャルフィールドを構成する SQL 表現にアクセスするために用いられます。 引数が与えられない場合、そのモデルのすべてのバーチャルフィールドを返します。
// 'CONCAT(User.first_name, ' ', User.last_name)' を返します。
$this->User->getVirtualField('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
);
}
これで、モデルにどんなエイリアスを与えても、バーチャルフィールドはうまく動くことでしょう。
次の例は、hasMany アソシエーションのカウンターを持ち、バーチャルフィールドを有効にすることができます。 例えば、もしテンプレートの中で次のようなソートリンクがある場合、
// バーチャルフィールドのソートリンクを作成
$this->Paginator->sort('ProductsItems.Total','Items Total');
コントローラの中で、次のようなページ制御の設定を行います。
$this->Products->recursive = -1;
// Products hasMany ProductsItems アソシエーション
$this->Products->ProductsItems->virtualFields['Total'] = 'count(ProductsItems.products_id)';
// ORM の条件
$where = array(
'fields' => array(
'Products.*',
'count(ProductsItems.products_id) AS ProductsItems__Total',
),
'joins' => array(
array(
'table' => 'products_items',
'alias' => 'ProductsItems',
'type' => 'LEFT',
'conditions' => array(
'ProductsItems.products_id = Products.id',
)
)
),
'group' => 'ProductsItems.products_id'
);
// Paginator に条件を設定
$this->paginate = $where;
// データの取得
$data = $this->Paginator->paginate();
次のようなものを返すでしょう。
Array
(
[0] => Array
(
[Products] => Array
(
[id] => 1234,
[description] => 'テキストなどなど...',
)
[ProductsItems] => Array
(
[Total] => 25
)
)
[1] => Array
(
[Products] => Array
(
[id] => 4321,
[description] => 'テキスト 2 などなど...',
)
[ProductsItems] => Array
(
[Total] => 50
)
)
)
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;