コンピューターの世界において、ユーザーやビジネスに対して柔軟かつ発展的にデータベースが適応していくことは、しばしば有益であり、場合によっては不可欠でさえあります。また、アクセス可能なデータの管理は、繰り返し問われるデリケートなテーマです。この観点から、開発者はコンテキストやユーザーのアクセス権に応じて、情報へのアクセスを与えたり制限したりするために、ときに複雑なメソッドやフォーミュラを用います。
簡単な例を挙げてみましょう。アプリケーションの中で、人物のリストを表示する必要があるとします。ある列にはフルネームを表示しますが、データベースには姓のフィールドと名のフィールドがあります。現在はリストボックス列にフォーミュラを書き、ソートも別途管理しています。もし、計算用のフォーミュラとソートの方法を定義できる計算フィールドがあって、ビジネスロジックを各インターフェースではなく、クラス内に持つことができれば、素晴らしいと思いませんか?
4D v19 R3 から、4D はこの問題に解決策を提供しています: 計算属性です!
想像してみてください…
姓、名、生年月日などを持つ人物のリストがあるとします。
あるフォームに、フルネーム (名 + 姓に基づく)、年齢 (生年月日に基づく)、現在の写真 (年齢に基づく)… を含む人物の一覧を表示したいとします。
そして、一人を選択すると同時に、その人の両親、祖父母、子供、兄弟姉妹、叔父叔母、従兄弟姉妹の情報を表示できたらいいですね。
そんなことができるのでしょうか? もちろんです!
フォームに複雑なコードを書かなくても? そのとおりです!
この魔法は、他の属性と同様にクエリやソートが可能な計算属性のおかげで可能になりました。では、詳しく見ていきましょう。
定義と計算
このコンセプトの威力を説明するために、具体的な例から始めましょう。”people” データクラスには、”lastname”, “firstname”, “address”, “zipcode”, “city”, “country”, “birthdate” といった属性があります。
このクラスから、フルネーム (lastname + firstname)、年齢 (生年月日に基づく)、または 住所 (オブジェクトとして) の情報が必要とします。
これで終わりではありません。リレーションが許せば、エンティティ型 (たとえば、父や母) や エンティティセレクション型 (子供、両親、祖父母、兄弟姉妹など) の計算属性も使用することができます。
これらの属性へのアクセスは “peopleEntity” クラスを通じて可能になりますが、クラスには新しい関数を定義する必要があります。
これらの関数は、読み取り (単純な表示など)、クエリやソート、そして修正後の保存など、4D が計算属性にアクセスする必要があるときに呼び出されます。
住所のような計算属性には、フルネームのような別の計算属性が必要かもしれません。このような再帰的な処理も可能であり、これについては後述します。
これらの関数 (get, set と、後述する query および orderBy) は、エンティティクラス (例: peopleEntity) に定義する必要があります。
計算属性へのアクセス
get
最初の関数である “get” は、属性の計算方法を定義します。そのため、計算結果を返さなければなりません。
この関数は、計算属性を使うにあたって唯一必須の関数です。計算属性の存在は、この関数があることによって決定されます。
Function get fullName($event : Object) -> $result : Text
If (This.firstname=Null)
$result:=This.lastname
Else
If (This.lastname=Null)
$result:=This.firstname
Else
$result:=This.firstname+" "+This.lastname
End if
End if
exposed、local…それとも?
この属性はどのようにアクセスされ、どのような条件下で計算されるのでしょうか?
関数定義の際に使用できる “exposed” キーワードは、ストラクチャーエディターのインスペクターにある “RESTリソースとして公開” チェックボックスに相当します。
クライアントサーバーモードで使用する “local” キーワードは、計算をサーバー上でおこなうか、ネットワークアクセスを制限するためにローカルでおこなうかを決定します。
SET
2番目の関数 “set” は、計算属性から、計算属性を構成している実際の属性を変更する逆の操作を可能にします。
fullname の例では、計算属性値のスペースを基準に、firstname とlastname に値を分配する簡単なルールが使えます。もちろん、これは単純な例に過ぎません。フルネームに複数のスペースが含まれている場合は、スラッシュ (/) を優先的に基準として採用するなど、より複雑なビジネスルールを適用する必要があります。
例: “Pablo Miguel/de la Casa del Mar”。
この関数を使うことの利点は、一度定義すればそれが適用されるため、入力コンテキストに応じて再定義する必要がないことです。
Function set fullName($value : Text)
var $p Integer
$p:=Position("/"; $value)
If ($p<=0)
$p:=Position(" "; $value)
End if
If ($p>0)
This.firstname:=Substring($value; 1; $p-1)
This.lastname:=Substring($value; $p+1)
Else
This.firstname:=""
This.lastname:=$value
End if
set なしの get? そしてその逆は…
get関数は、計算属性にアクセスするために必須です。この属性を変更可能にするには set関数が必須ですが、これらの関数のうちどちらか一方しか存在しない場合はどうなるでしょうか?
getだけが存在する場合、その属性は “読み取り専用” となり、変更はできません。
setだけが存在する場合、属性の書き込みは可能ですが、読み取りはできません。これは、メールボックスやパスワードの原理です。
その次は?
3番目と 4番目の関数 query と orderBy も、同じようなものです。
これらの関数は必須ではありませんが、後述するように、これらの関数を使うことで性能が飛躍的に向上します。実際、orderBy関数が定義されていない場合にソートをおこなうと、4D は各エンティティに対して get関数を実行し、シーケンシャルソートをおこないます。これには時間がかかり、最適化とは程遠いものになってしまいます。
計算属性にはインデックスがないことに留意しておくことが肝要です。しかし、計算属性を構成する実際の属性にはインデックスがあるかもしれません (ない場合には設定すべきかもしれません)。
このため、計算属性に対してクエリやソートが開始されたときに、実際にどのように処理がおこなわれるかを定義できるようになっています。
fullname を例にとると、firstname と lastname (両方ともインデックスされています) 属性に基づいて、fullname = “Paul Smith” というタイプのクエリは、firstname = “Paul” かつ lastname = “Smith” という意味になるでしょう。この検索を拡張して、fullname = “Pa Sm” という検索では、firstname が “Pa” で始まり、 lastname が “Sm” で始まることを意味する、というルールを加えるのも自由です。あるいは、fullname = “Martin” は、姓か名のどちらかが “Martin” で始まることを意味する、など…。
QUERY
計算属性で定義された “query” 関数を実行すると、渡される引数には、目的の検索を理解するために必要な 2つのプロパティが含まれます。これを使うことで、インデックス化された属性を使用するようにクエリを書き換えることができ、シーケンシャルな処理を避けることができます。
$event.operator は、文字列形式の演算子を格納します (“==”, “>=”, “<“, など)。
$event.value は、検索または比較する値を文字列として含んでいます。
query 関数の結果は、文字列 か オブジェクト のどちらかです。
もし関数が文字列を返す場合、その文字列は有効なクエリでなければなりません。
$result:="lastname = A@ and firstname = B@"
この関数がオブジェクトを返す場合、オブジェクトには 2つのプロパティが含まれていなければなりません。
- .query: 有効なクエリ文字列で、プレースホルダー (:1、:2 など) を含むことができます。
- .parameters: プレースホルダーに使用される値のコレクション
$result:=New object("query"; $query; "parameters"; $parameters)
それでは、データクラスを対象とした次のクエリを query関数内でどのように管理するかを見てみましょう。
$es:=ds.people.query("fullname = :1";"Paul Smith")
以下は、query関数の完全なコードです。
Function query fullname ($event : Object) -> $result: Object
$fullname:=$event.value
$operator:=$event.operator
$p:=Position(" "; $fullname)
If ($p>0)
$firstname:=Substring($fullname; 1; $p-1)+"@"
$lastname:=Substring($fullname; $p+1)+"@"
$parameters:=New collection($firstname; $lastname)
Else
$fullname:=$fullname+"@"
$parameters:=New collection($fullname)
End if
Case of
: ($operator="==") | ($operator="===")
If ($p>0)
$query:="firstname = :1 and lastname = :2"
Else
$query:="firstname = :1 or lastname = :1"
End if
: ($operator="!=")
If ($p>0)
$query:="firstname != :1 and lastname != :2"
Else
$query:="firstname != :1 and lastname != :1"
End if
End case
$result:=New object("query"; $query; "parameters"; $parameters)
orderby
ソートの処理も、同じような考え方を用います。年齢でソートする場合、正確に言えば、生年月日を逆にソートするのと同じことです。そのため、年齢によるソートが要求された場合、インデックス化されている生年月日を基準として使うのが賢いやり方でしょう。
- ソートの例:
$es:=ds.people.all().orderBy("age desc")
$es:=ds.people.all().orderBy("age asc")
- orderBy関数:
Function orderBy age($event : Object) -> $result: String
If($event.operator = "desc")
$result:="birthday asc"
Else
$result:="birthday desc"
End if
複合インデックスの利用
複合インデックスは、場合によっては非常に有効に使うことができます。lastname と firstname によるソートは、lastname や firstname のインデックスを使用しません。この種のソートが最適化されるためには、firstname + lastname のインデックス、あるいは lastname + firstname のインデックス…あるいはその両方が必要です。
fullname のような計算属性の場合、それが実際には firstname + lastname のソートであることを 4D が認識すれば、この複合インデックスを使用します。fullname が firstname を lastnameより先に表示していても、lastname + firstnameで強制的にソートすることも可能です!
- ソートの例:
Form.people:=ds.people.all().orderBy("fullname asc")
- orderBy関数:
Function orderBy fullname($event : Object)->$result : Text
If ($event.descending)
$result:="lastname desc, firstname desc"
Else
$result:="lastname asc, firstname asc"
End if
計算属性の可能な型
これまで、スカラー型 (テキスト、数値…) の計算属性を取り上げましたが、オブジェクト型、エンティティ型、エンティティセレクション型の計算属性も使用することができます。
考えられる例をいくつか挙げてみましょう:
- fullAddress: 必要な属性をすべて持つオブジェクト (fullname, street, zipcode, city, country など)
Function get fullAddress($event : Object) -> $result: Object
$result:=New object
$result.fullname:=This.fullname // 計算属性を別の計算属性の定義に使用できます
$result.address:=This.address
$result.zipCode:=This.zipCode
$result.city:=This.city
$result.state:=This.state
$result.country:=This.country
- bigBoss: 上司の上司を表すエンティティ
Function get bigBoss($event : Object) -> $result: cs.peopleEntity
$result:=this.manager.manager
- coworkers: 同じ上司を持つ同僚のエンティティセレクション
Function get coworkers($event : Object) -> $result: cs.peopleEntitySelection
$result:=this.manager.directReports.minus(this)
まとめ
計算属性は柔軟性とパワーの両方をもたらすほか、データアクセスの制御を強化します。計算属性へのアクセスは、メモリやネットワークアクセスに負担をかけないように最適化されています。これらはビジネスの要求に対するシンプルなソリューションであり、モダンなプログラミングの増加する要件を満たすものです。
詳しくは、このドキュメントを参照ください。