やりたいこと
Object Pageで"status"に2が設定された場合、"stock"を入力不可にします。
ドメインモデル
namespace my.bookshop;
entity Books {
key ID : Integer;
title : localized String;
stock : Integer;
status : Association to Status;
}
entity Status {
key ID : Integer enum {
OnSale = 1;
Discontinued = 2; };
name : String;
}
サービスモデル
using my.bookshop as my from '../db/data-model';
service CatalogService {
@odata.draft.enabled
entity Books as projection on my.Books;
entity Status as projection on my.Status;
}
方法
ステップは以下の通りです。
- 制御用の項目を用意する
- 制御したい項目にアノテーションを設定する
- Side Effectの設定により、ある項目が変更されたら制御用の項目を再読み込みする
制御用の項目
制御用の項目は、Edm.Byteというデータ型で定義します。値の意味は以下のようになっています。
| 値 | 意味 |
|---|---|
| 7 | 必須 |
| 3 | 任意 |
| 1 | 照会のみ |
| 0 | 現在のエンティティの状態では意味を持たない(いわゆるN/A) |
CAPにはByteというデータ型はないので、@odata.TypeというアノテーションによりODataとして公開されるデータ型を指定します。
fieldControl: Int16 @odata.Type:'Edm.Byte'
項目制御用のアノテーション
制御対象項目に対し@Common.FieldControlというアノテーションで制御用の項目を指定します。
annotate CatalogService.Books with {
stock @Common.FieldControl: fieldControl;
}
Side Effectの設定
Side Effectは項目が変更されたときやアクションが実行されたときに、他の項目またはエンティティを再取得する目的で使用します。
SourcePropertiesにSide Effectのトリガとなる項目、TargetPropertiesに再取得する項目または、1:1のアソシエーション先であるエンティティを指定します。
※1:nのアソシエーション先のエンティティについては、TargetEntitesで指定します。
参考:Side Effects
annotate CatalogService.Books with @(Common: {
SideEffects #changeStatus : {
$Type : 'Common.SideEffectsType',
SourceProperties : ['status_ID'],
TargetProperties : ['fieldControl'],
},
});
制御用の項目の持たせ方
調べた限りでは2つのアプローチがありました。いずれも制御用の項目はDBに持たせません。
1. 制御用項目にデフォルト値を設定する
項目制御のロジックが比較的単純な式で表せる場合に使える方法です。
1.1. デフォルト値の設定
例として、(ID = #Discontinued ? #ReadOnly : #Optional )という式によって、IDが2のときにReadOnly (1)、そうでないときにOptional (3) を設定しています。
extend my.Status with {
fieldControl: Int16 @odata.Type:'Edm.Byte' enum {Inapplicable = 0; ReadOnly = 1; Optional = 3; Mandatory = 7;}
= (ID = #Discontinued ? #ReadOnly : #Optional );
}
参考リポジトリ:SAP-samples/cap-sflight
補足
-
#Discontinuedはデータモデルで定義されたenumのシンボルを参照しています
key ID : Integer enum {
OnSale = 1;
Discontinued = 2; };
- ここでは
extend my.Statusというキーワードにより、データモデルで定義したエンティティを拡張していますが、もともとのStatusエンティティの中で制御用の項目を定義することも可能です。制御用であることをわかりやすくするために拡張という形をとっています
1.2. Side Effectの設定
Side Effectを以下のように設定します。TargetPropertiesにアソシエーション先のエンティティであるstatusを指定しています。
annotate CatalogService.Books with @(Common: {
SideEffects #changeStatus : {
$Type : 'Common.SideEffectsType',
SourceProperties : ['status_ID'],
TargetProperties : ['status'],
},
});
制御対象項目と同じエンティティに制御用項目を持たせられない?
上の例では、Books(制御対象のstock項目が含まれる)と1:1のアソシエーション関係にあるStatusというエンティティに制御用の項目を持たせています。
Booksエンティティに制御用の項目を持たせてみると、Side EffectによりfieldControlが再取得されたときに古い値のままだったため、入力可否が切り替わりませんでした。一旦保存し、再度編集モードにしたときには正しく制御が行われました。

statusを2に変更したが、fieldContorlが切り替わらない
2. Virtual Elementを作成し、イベントハンドラでコントロールする
項目制御のロジックが複雑な場合、virtual elementとイベントハンドラ(After READ)の組み合わせで実現することが可能です。
2.1 Virtual Elementの追加
Booksエンティティにvirtual elementとして制御用の項目を追加します。
extend my.Books with {
virtual fieldControl : Int16 @odata.Type: 'Edm.Byte' enum {
Inapplicable = 0;
ReadOnly = 1;
Optional = 3;
Mandatory = 7;
};
};
補足
- ここではextend my.Booksというキーワードにより、データモデルで定義したエンティティを拡張していますが、もともとのBooksエンティティの中で制御用の項目を定義することも可能です
2.2 Side Effectのアノテーション
ポイントは、TargetPropertiesにfieldControlの設定に必要なstatus_IDも含めることです。これがないと、After READのイベントハンドラにstatus_IDが入ってきません。
annotate CatalogService.Books with @(Common: {SideEffects #changeStatus: {
$Type : 'Common.SideEffectsType',
SourceProperties: ['status_ID'],
TargetProperties: ['fieldControl','status_ID'],
}, });
イベントハンドラ
イベントハンドラを以下のように実装します。
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void updateFieldControl(Stream<Books> books) {
// 各bookのfieldControlを設定
books.forEach(book -> {
Integer statusId = book.getStatusId();
if (statusId != null) {
if (statusId == 2) { // Discontinued
book.setFieldControl((short) 1); // ReadOnly
} else {
book.setFieldControl((short) 3); // Optional
}
}
});
}
サンプルリポジトリ
以下のリポジトリにサンプルコードを格納しています。
ブランチ
- main: 制御用項目にデフォルト値を設定する方法
- virtual_element: Virtual Elementsにより入力制御を行う方法
- fieldcontrol_on_same_entity: アノテーションのみで入力制御(制御項目が同一のエンティティにあるパターン)⇒うまくいっていない

