2.11.3. 仮想フィールド

仮想フィールドは compute と同様に自動でフィールド値を計算します。しかしcomputeとは似て非なるものです。 データベース上ではフィールドを生成しません。もちろんフィールド値も保存しません。

仮想フィールドは Rows クラスの機能です。Queryクラスなどの条件式には使用できません。 また DAL / Table / Field / SetといったRowsより上位のクラス(よりデータベースに近いクラス)は、 仮想フィールドを扱うことができません。このため、仮想フィールドをデータベース検索の対象にできません。( Set Rows Row クラスについて も参照)

仮想フィールドの設定方法には三通りあります。
  1. Tableクラスにあるvirtualfields属性を設定する方法
  2. Rowsクラスにある setvirtualfieldsメソッドで設定する方法
  3. ニュースタイル仮想フィールドを設定する方法

一般的な設定方法はvirtualfields属性を設定する方法です。しかしvirtualfields属性は内部でsetvirtualfieldsメソッドを呼び出して 仮想フィールドを実現しています。

ニュースタイル仮想フィールドは、setvirtualfieldsメソッドとは別の方法で実装されています。Rowsインスタン生成時の構文解析で、ニュースタイル仮想フィールド が設定されている場合、登録された関数を実行しインスタンスに値を設定するという方法です。

以下仮想フィールドの詳細説明をしていきますが、説明には次のテーブル定義を利用します。

db.define_table('item',
    Field('name'),
    Field('unit_cost','decimal(10,6)'))

db.define_table('inventory',
    Field('item', db.item),
    Field('actual','decimal(10,2)'),
    Field('reserved','decimal(10,2)'))

参考: Virtual Fields仮想フィールド

setvirtualfields

仮想フィールドを生成する setvirtualfields() メソッドを実行すると、 Rows インスタンスのデータ部分を拡張し新しい フィールドを追加します。 拡張した新しいフィールド名には、setvirtualfieldsメソッドにパラメータとして渡したインスタンスのメソッド名が設定されます。 フィールド値にはメソッドの戻り値が返されます。

  • 仮想フィールド名・・・パラメータで渡したインスタンス(クラス)のメソッド名
  • 仮想フィールドの値・・・パラメータで渡したインスタンスのメソッドの戻り値

setvirtualfieldsメソッドを使った設定手順は次のようになります。

  1. 仮想フィールドに設定するためのクラス及びメソッドを定義します。
  2. Rowsインスタンスを生成します。
  3. Rowsインスタンスのsetvirtualfieldsメソッドを呼び出します。パラメータ名にテーブル名、パラメータ値に1で定義したクラスのインスタンス を設定します。パラメータは複数設定が可能です。

この他に補足説明として次のことが挙げられます。

  • クラス・メソッド定義でテーブルのフィールドを参照する場合は、 self.テーブル名.フィールド名 と記述します。
    RowsやRowインスタンスでテーブルを一個しか参照していない時はテーブル名を省略しますが、 仮想フィールド用に定義するクラスは内部処理に使用しますので、テーブル名を抜かさず記述する必要があります。
  • 仮想フィールドはRowsクラスの機能です。このため、 再帰的select をメソッド定義でも使用できます。 また、 テーブル結合(join) で生成したRowsインスタンスへの設定も可能です。

以下設定サンプルを示していきます。

単一テーブルデータへの設定です。

>>> class virtual_free(object):
...     def free(self):                                 # 利用可能な在庫数量フィールドを設定
...         return self.inventory.actual - self.inventory.reserved
>>>  rows = db(db.inventory).select()
>>>  rows.setvirtualfields(inventory=virtual_free())

再帰的select を利用した設定です。

>>> class virtual_valuation(object):
...     def valuation(self):                            # itemテーブルからコストを取得して在庫評価を設定
...         valuation = self.inventory.actual\
...             * self.inventory.item.unit_cost
...         return valuation
>>> rows = db(db.inventory).select()
>>> rows.setvirtualfields(inventory=virtual_valuation())

joinで生成したRowsインスタンスへの設定です。

>>> class virtual_valuation(object):
...     def valuation(self):                            # itemテーブルからコストを取得して在庫評価を設定
...         valuation = self.inventory.actual\
...             * self.item.unit_cost
...         return valuation
>>> rows = db(db.inventory.item==db.item.id).select()
>>> rows.setvirtualfields(inventory=virtual_valuation())

クラス内で2つのフィールドを定義しています。

>>> class virtual(object):
...     def free(self):                                 # 利用可能な在庫数量フィールドを設定
...         return self.inventory.actual - self.inventory.reserved
...     def valuation(self):                            # itemテーブルからコストを取得して在庫評価を設定
...         valuation = self.inventory.actual\
...             * self.inventory.item.unit_cost
...         return valuation
>>> rows = db(db.inventory).select()
>>> rows.setvirtualfields(inventory=virtual())

joinしたテーブルに2つのクラスを設定しています。

>>> class virtual_costup(object):
...     def costup(self):                               # コストを1.5倍にしたフィールドを設定
...         from decimal import Decimal
...         return self.item.unit_cost * Decimal('1.5')
>>> class virtual(object):                              # 1.5倍のコストを使って在庫評価を設定
...     def valuation(self):
...         valuation = self.inventory.actual\
...             * self.item.costup
...         return valuation
>>> rows = db(db.inventory.item==db.item.id).select()
>>> rows.setvirtualfields(item=virtual_costup(),inventory=virtual())

setvirtualfieldsについては setvirtualfields() も参照ください。

virtualfields

virtualfieldsは Table インスタンスに用意されている属性です。 この属性値には setvirtualfields で利用するクラス名を設定します。

virtualfieldsが設定されているテーブルのRowsインスタンスを生成すると、自動的にsetvirtualfieldsメソッドが 呼び出されるようになっています。つまりRowsインスタンスを生成する度に、いちいち明示的にsetvirtualfields メソッドを呼び出す必要がありません。

設定サンプルを示してみます。

>>> class virtual_free(object):                         # 利用可能な在庫数量フィールドを設定
...     def free(self):
...         return self.inventory.actual - self.inventory.reserved
>>>  db.inventory.virtualfields.append(virtual_free())

設定後、Rowsインスタンスを生成すれば自動的に仮想フィールドがセットされます。

>>> rows = db(db.inventory).select()
>>> print rows[0]
<Row {'reserved': Decimal(""35.00""), 'update_record': <function <lambda> at 0x039
01DF0>, 'free': Decimal(""115.00""), 'item': 1, 'actual': Decimal(""150.00""), 'id':
 1, 'delete_record': <function <lambda> at 0x03901DB0>}>"

virtualfieldsについては virtualfields も参照ください。

遅延(Lazy)

遅延とは、仮想フィールドの値が必要になった時に、計算して取り出すという処理のことです。

通常では setvirtualfields メソッドを実行した時点で、仮想フィールドの値を算出しRowsインスタンスに仮想フィールドが埋め込まれます。 遅延ではsetvirtualfieldsメソッドを実行した時点で、Rowsインスタンスに仮想フィールドのメソッド式を埋め込みます。 仮想フィールド値を取り出すためには、値毎にメソッド形式で命令を発行する必要があります。

遅延の設定は、setvirtualfieldsに渡すクラスメソッドで、関数式を返すようにすればよいです。

設定サンプルを示してみます。

>>> class virtual_free(object):
...     def free(self):
...         def lazy(self=self):
...             return self.inventory.actual - self.inventory.reserved
...         return lazy
>>> rows = db(db.inventory).select()
>>> rows.setvirtualfields(inventory=virtual_free())

設定した遅延を利用する場合、次のようにします。

>>> for row in rows:
...     print row.actual, row.reserved, row.free()      # 仮想フィールドだけメソッド形式で呼び出す。
150.00 35.00 115.00

遅延設定では無名関数(lambda)を利用すると簡潔な記述になります。

>>> class virtual_free(object):
...     def free(self):
...         return lambda self=self: self.inventory.actual - self.inventory.reserved
>>> rows = db(db.inventory).select()
>>> rows.setvirtualfields(inventory=virtual_free())

ニュースタイル仮想フィールド

web2py book 第四版から、ニュースタイル仮想フィールドが登場しました。特徴としては一言、「わかりやすい」と言えるでしょう。

ニュースタイル仮想フィールドのポイント
  1. Tableインスタンス変数 に設定します。インスタンス変数は仮想フィールド名になります。
  2. 仮想フィールド値は Fieldクラスの Virtual 属性を使い、パラメータに無名関数を指定します。
  3. 遅延を使う場合、Fieldクラスの Method 属性を使い、パラメータに無名関数を指定します。

Note

Fieldクラスの Lazy 属性は、Method に改名されました。 後方互換性のため Lazy属性を引き続き使用可能ですが、今後は Method属性値を使用してください。 このドキュメントでも Method に変更しています。

ニュースタイル仮想フィールドは、 Table インスタンス変数 に仮想フィールドを設定します。設定値は、 Field クラスの Virtual 属性を使います。 Virtual属性にはパラメータを一つセットします。このパラメータには、 Row インスタンスを受け取る無名関数を指定します。 [1]

[1]Virtual属性は内部定義で、FieldVirtualクラスの変数になっています。つまりVirtual属性を使うというのは、、FieldVirtualインスタンスを生成することです。

Tableインスタンス変数は、仮想フィールドのフィールド名になります。

サンプルを示します。

db.inventory.free = \
Field.Virtual(lambda row: row.inventory.actual-row.inventory.reserved)

Rowsインスタンス生成時に、free の値が計算されます。

>>> for row in db(db.inventory).select():
... print row.actual, row.reserved, row.free
150.00 35.00 115.00
100.00 20.00 80.00
110.00 50.00 60.00

遅延に関しては、Fieldクラスの Method 属性を使用します。設定方法は Virtual属性と同様に、Rowインスタンスを受け取る関数を指定します。 [2]

[2]Method属性は内部定義で、FieldMethodクラスの変数になっています。つまりMethod属性を使うというのは、、FieldMethodインスタンスを生成することです。
>>> db.inventory.free = \
... Field.Method(lambda row: row.inventory.actual-row.inventory.reserved)

遅延ではフィールドに関数のままで渡されます。このため利用時には、関数呼び出し形式で記述する必要があります。

>>> for row in db(db.inventory).select():
...     print row.actual, row.reserved, row.free()
150.00 35.00 115.00
100.00 20.00 80.00
110.00 50.00 60.00

遅延の仮想フィールドの使用時に計算を行うため、row以外の他のパラメータを渡すことも可能です。

>>> db.inventory.free = \
... Field.Method(lambda row,min=50: row.inventory.actual-row.inventory.reserved-min)

最小数量(min)を設定し、free数量を調整しています。利用時にパラメータを指定しないと、デフォルト値(50)が使用されます。

>>> for row in db(db.inventory).select():
...     print row.actual, row.reserved, row.free()
150.00 35.00 65.00
100.00 20.00 30.00
110.00 50.00 10.00

>>> for row in db(db.inventory).select():
...     print row.actual, row.reserved, row.free(30)
150.00 35.00 85.00
100.00 20.00 50.00
110.00 50.00 30.00

SQLTABLEでの仮想フィールドの利用

SQLTABLEはデフォルト設定では仮想フィールドは出力されません。

仮想フィールドが出力されないSQLTABLEサンプル

class virtual_free(object):
    def free(self):
        return self.inventory.actual - self.inventory.reserved

def sqltable():
    rows = db(db.inventory).select()
    rows.setvirtualfields(inventory=virtual_free())
    return dict(rows=rows)

理由は Rows インスタンスの colnames 属性にはモデルで定義したフィールドしか設定しないからです。 しかしSQLTABLEのオプションでは出力フィールドを指定できるcolumnsパラメータがあります。ここに仮想フィールドを含めた フィールドのリストを設定すれば出力可能です。

class virtual_free(object):
    def free(self):
        return self.inventory.actual - self.inventory.reserved

def sqltable():
    rows = db(db.inventory).select()
    rows.setvirtualfields(inventory=virtual_free())
    col = ['inventory.id','inventory.item','inventory.actual',\
           'inventory.reserved','inventory.free']
    return dict(rows=SQLTABLE(rows,columns=col))

Note

SQLTABLEでもニュースタイル仮想フィールドが使用できるようになっています。 ただ ver.2.2.1以前では対応していないので注意ください。