【UI5】OData V2 + Smart ControlでDeep createする方法

## この記事を書いたきっかけ
OData V2 + Smart Controlを使った画面で、ヘッダ + 明細という構造のデータを入力します。「登録」ボタンを押したらヘッダ、明細データを同時にDeep createでバックエンドに送信したいという要件がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9995af67-6b8b-ed8d-bd79-14da5b941996.png)

## 課題
JSONモデルであれば、「登録」ボタンを押したタイミングでODataModelの[create](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.model.odata.v2.ODataModel%23methods/create)メソッドを使ってディープな構造を渡すことができます。

```javascript:JSONモデルでの実装イメージ
const model = this.getView().getMoel()
model.create("/EntitiySet", {
  field1: "...",
  field2: "...",
  items: [{
    field3: "",
    field4: ""
  }]
});
```
しかし、Smart ControlではOData Modelをバインドする必要があるため上記の方法が使えません。[SAPUI5 SDK](https://sapui5.hana.ondemand.com/sdk/#/topic/6c47b2b39db9404582994070ec3d57a2.html#loio4c4cd99af9b14e08bb72470cc7cabff4/section_DCR)に参考になりそうなコードサンプルがあったのでこれを利用して実装してみようと思いました。

## 方法
サンプルアプリを使ってDeep createする方法について説明します。
ソースコード：https://github.com/miyasuta/ui5-deep-create/tree/main

使用したメタデータ（抜粋）は以下です。

```xml
            <EntityType Name="Order">
                <Key>
                    <PropertyRef Name="ID"/>
                </Key>
                <Property Name="createdAt" Type="Edm.DateTimeOffset" Precision="7"/>
                <Property Name="createdBy" Type="Edm.String" MaxLength="255"/>
                <Property Name="modifiedAt" Type="Edm.DateTimeOffset" Precision="7"/>
                <Property Name="modifiedBy" Type="Edm.String" MaxLength="255"/>
                <Property Name="ID" Type="Edm.Guid" Nullable="false"/>
                <Property Name="customer" Type="Edm.String"/>
                <NavigationProperty Name="items" Type="Collection(CatalogService.Item)" Partner="order">
                    <OnDelete Action="Cascade"/>
                </NavigationProperty>
            </EntityType>
            <EntityType Name="Item">
                <Key>
                    <PropertyRef Name="order_ID"/>
                </Key>
                <NavigationProperty Name="order" Type="CatalogService.Order" Partner="items">
                    <ReferentialConstraint Property="order_ID" ReferencedProperty="ID"/>
                </NavigationProperty>
                <Property Name="order_ID" Type="Edm.Guid" Nullable="false"/>
                <Property Name="product" Type="Edm.String"/>
                <Property Name="quantity" Type="Edm.Int32"/>
            </EntityType>
```


### View
以下のようなビューを作成します。明細にはListBindingができるテーブル系のコントロールを使用します。ポイントは、`rows="{items}"`のようにリストの部分にナビゲーションプロパティを指定することです。

```xml
<mvc:View controllerName="demo.ui.controller.View1"
    xmlns:mvc="sap.ui.core.mvc" displayBlock="true"
    xmlns:m="sap.m"
    xmlns:smartForm="sap.ui.comp.smartform"
	xmlns:smartField="sap.ui.comp.smartfield"
    xmlns:smartTable="sap.ui.comp.smarttable"
    xmlns:table="sap.ui.table">
    
    <m:Page id="page" title="{i18n>title}">

        <smartForm:SmartForm id="smartForm"
			editable="true"
			validationMode="Async"
			title="{Name}">
			<smartForm:Group label="Order">
				<smartForm:GroupElement>
					<smartField:SmartField value="{customer}" />
				</smartForm:GroupElement>
			</smartForm:Group>
		</smartForm:SmartForm>  

        <table:Table
                id="table"
				rows="{items}">
			<table:extension>
				<m:Toolbar >
					<m:ToolbarSpacer />
					<m:Button id="addButton" text="Add" press="onAdd" />
					<m:Button id="deleteButton" text="Delete" press="onDelete" />
				</m:Toolbar>
			</table:extension>
            <table:columns>
                    <table:Column width="11rem">
						<m:Label text="Item No." />
						<table:template>
							<m:Text text="{itemNo}" wrapping="false" />
						</table:template>
					</table:Column>
					<table:Column width="11rem">
						<m:Label text="Product" />
						<table:template>
							<m:Input value="{product}" wrapping="false" />
						</table:template>
					</table:Column>
                    <table:Column width="11rem">
						<m:Label text="Quantity" />
						<table:template>
							<m:Input value="{quantity}" wrapping="false" />
						</table:template>
					</table:Column>    
            </table:columns>        
        </table:Table>
        

        <m:footer>
            <m:OverflowToolbar >
                <m:ToolbarSpacer />
                <m:Button text="Save" press="onSave" />
            </m:OverflowToolbar>
        </m:footer>
    </m:Page>
</mvc:View>
```

:::note warn
OData V2の場合、**Smart Tableを使って入力させることができません**。Smart Tableに表示するレコードはバックエンドに保存済みである必要があるためです。そのためここではsap.ui.table.Tableを使用しています。

参考
https://answers.sap.com/questions/13097516/add-new-row-in-the-smart-table.html
:::

### Controller
以下のステップでディープなリクエストを送ることができます。

1. ODataModelの[createEntry](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.model.odata.v2.ODataModel%23methods/createEntry)メソッドでヘッダのコンテキストを作り、ビューにバインドする
1. テーブルコントロールからlistBindingを取得し、[create](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.model.odata.v2.ODataListBinding%23methods/create)メソッドで明細を作成する
1. ODataModelの[submitChanges](https://sapui5.hana.ondemand.com/sdk/#/api/sap.ui.model.odata.v2.ODataModel%23methods/submitChanges)メソッドでバックエンドにリクエストを送信する

```javascript
sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/m/MessageToast",
    "sap/m/MessageBox"
],
    /**
     * @param {typeof sap.ui.core.mvc.Controller} Controller
     */
    function (Controller, MessageToast, MessageBox) {
        "use strict";

        return Controller.extend("demo.ui.controller.View1", {
            onInit: function () {
                const router = this.getOwnerComponent().getRouter();
                router.getRoute("RouteView1").attachPatternMatched(this._onObjectMatched, this);
                this._itemIndex = 0;
               
            },

            onSave: function () {
                //3. バックエンドにリクエストを送信
                const that = this;
                this.getView().getModel().submitChanges({
                    success: function () {
                        if (that.getView().getModel().hasPendingChanges()) {
                            MessageBox.error("Data was not fully saved");
                        } else {
                            MessageToast.show("Data saved successfully.");
                        }
                    },
                    error: function (error) {
                        MessageBox.error(JSON.stringify(error));
                    }
                })
            },

            onAdd: function () {
                //2. 明細を作成
                this._itemIndex ++;
                const table = this.getView().byId("table");                                            
                const itemsBinding = table.getBinding("rows");
                itemsBinding.create({itemNo: this._itemIndex, product: "", quantity: 1}, true /*create at end*/);
            },

            onDelete: function () {
                const table = this.getView().byId("table");    
                const selectedIndices = table.getSelectedIndices();
                const rows = table.getRows();
                selectedIndices.map(index => {
                    rows[index].getBindingContext().delete();
                });
            },

            _onObjectMatched: function () {
                //1. ヘッダのコンテキストを作りビューにバインド
                const model = this.getOwnerComponent().getModel();
                const context = model.createEntry("/Order", { 
                                        properties: {
                                            customer        : 'default'
                                        } })                    
                this.getView().setBindingContext(context);
            }
        });
    });
```

## おわりに
OData V2では全体をSmart Controlでは作成できないことがわかりました。Smart Controlを使えば検索ヘルプが自動で（アノテーションだけで）付くのでできればSmart Controlで統一したかったのですが。
なお、OData V4 + ドラフトが使えるODataサービスであれば、[Flexible Programming Model](https://qiita.com/tami/items/5205f84137b5455c74cc)によって入力可能なテーブルを作成することもできます。