【RAP】Unmanaged BOの実装 (1)

##はじめに
RAP(ABAP RESTful Application Programming Model)には、ManagedとUnmanagedという2種類のBOがあります。ManagedはCRUD処理やトランザクションバッファとのやりとりがフレームワークによって提供され、Unmagedはそれらの処理を全て自前で実装する必要があります。

Managed, Unmanagedについては以下の記事で説明しています。

https://qiita.com/tami/items/f8407c0373e64d23b23e#managed--unmanaged-bo%E3%81%A8%E3%81%AF

これまで何回かにわたりManaged BOでできることを紹介してきましたが、今回はUnmanaged BOはどうやって実装するかについて書きたいと思います。[ADTのRAP Generator](https://github.com/SAP-samples/cloud-abap-rap)はManaged BOに対応していない（2022/1月現在）ので、Andre Fischer氏の[RAP Generator](https://blogs.sap.com/2020/05/17/the-rap-generator/)を使うことにします。
Unmanagedの実装はステップが多いので、この記事ではRAP Generatorを使い骨組みを生成するところまで実施します。

**後続の記事**

- [【RAP】Unmanaged BOの実装 (2) Create処理](https://qiita.com/tami/items/d9f6cda276c0be3b7ca9)
- [【RAP】Unmanaged BOの実装 (3) Update処理](https://qiita.com/tami/items/3c60a323de816fba1001)
- [【RAP】Unmanaged BOの実装 (4) Delete処理](https://qiita.com/tami/items/88e669af3e4f5c216194)

###シナリオ
これまでの記事でも使ってきた、PersonとFamilyMemberという2つのエンティティを持つBOを実装します。
キーはPersonについては[Late Numbering](https://qiita.com/tami/items/96f20feb5c1f74b22803)、~~FamilyMemberは[Early Numbering](https://qiita.com/tami/items/e152bad7f82bce45ba7d)~~で採番します。
2022/2/16 訂正：子のキーに親のキーが含まれていると両方Late Numberingにする必要があるため、全てLate Numberingで実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/677090ba-e457-e80f-2675-72130f157748.png)

###環境
BTP ABAP Envoronmentのトライアル版を使用します。

Andre Fischer氏のRAP GeneratorはGitリポジトリからインストールして使いますが、クラウド版とオンプレ版でブランチが分かれているので使用する環境に合ったブランチを選択してください。BTP ABAP Environmentのトライアル版には、すでにRAP Generatorがインストールされています。

##手順

1. テーブルを作成
1. RAP Generatorで使用するJSONファイルを準備
1. RAP Generator用のワンタイムで使うクラスを作成
1. BOを生成
1. プレビューを見る

###1. テーブルを作成
以下のテーブルを作成します。クライアントは'mandt'ではなく'client'にしないとGeneratorでエラーが出ます。

**Person**

```abap
@EndUserText.label : 'Unmanaged Person'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zperson_u {
  key cient            : abap.clnt not null;
  key person_id         : ze_person_id not null;
  first_name            : ze_firstname;
  last_name             : ze_lastname;
  email                 : ze_email120;
  birthday              : ze_birthday;
  status                : ze_status;
  created_by            : abp_creation_user;
  created_at            : abp_creation_tstmpl;
  last_changed_by       : abp_locinst_lastchange_user;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
  last_changed_at       : abp_lastchange_tstmpl;

}
```

**FamilyMember**

```abap
@EndUserText.label : 'Unmanaged Family Member'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfamilymember_u {
  key cient       : abap.clnt not null;
  @AbapCatalog.foreignKey.label : 'Person'
  @AbapCatalog.foreignKey.screenCheck : false
  key person_id   : ze_person_id not null
    with foreign key [0..*,1] zperson_u
      where person_id = zfamilymember_u.person_id;
  key member_id   : ze_member_id not null;
  firstname       : ze_firstname;
  lastname        : ze_lastname;
  last_changed_at : abp_lastchange_tstmpl;

}
```

###2.RAP Generatorで使用するJSONファイルを準備
Andre Fischer氏のRAP Generatorでは、JSONファイルで設定を作成しそれをABAPクラスに読み込ませてRAPオブジェクトを生成します。
設定方法については[こちら](https://github.com/SAP-samples/cloud-abap-rap/tree/main)に説明があります。今回は以下のような設定ファイルを作成しました。

```json
{
    "$schema": "https://raw.githubusercontent.com/SAP-samples/cloud-abap-rap/main/json_schemas/RAPGenerator-schema-all.json",
    "namespace": "Z",
    "dataSourceType": "table",
    "implementationtype": "unmanaged_semantic",
    "bindingType": "odata_v4_ui",
    "package": "ZYASU2112_2",
    "draftenabled": true,
    "prefix": "",
    "suffix": "_u",
    "hierarchy": {
        "entityName": "Person",
        "dataSource": "zperson_u",
        "objectId": "person_id",
        "etagMaster": "local_last_changed_at",
        "totalEtag": "last_changed_at",
        "children": [
            {
                "entityName": "FamilyMember",
                "dataSource": "zfamilymember_u",
                "objectId": "member_id",
                "etagMaster": "local_last_changed_at"
            }
        ]
    }
}
```

ポイントは、**implementationType**に`unmanaged_semantic`を指定するところです。現状3つのimplementationTypeがサポートされています。

- managed_uuid：uuid（内部採番）をキーとするManaged BOを生成
- managed_semantic：外部採番のキーを持つManaged BOを生成
- unmanaged_semantic：Unmanaged BOを生成

prefix, suffixは既存のオブジェクトと名前がかぶらないようにするために設定することができます。

###3. RAP Generator用のワンタイムで使うクラスを作成
以下のようなクラスを作成します。このあとメソッド`get_json_string`に用意したJSONオブジェクトを貼り付けます。
※クラスの定義は[GitHub](https://github.com/SAP-samples/cloud-abap-rap/tree/main#how-to-use-the-rap-generator)よりコピー

```abap
CLASS zcl_rap_generator_console_01 DEFINITION
  PUBLIC
  INHERITING FROM cl_xco_cp_adt_simple_classrun
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
  PROTECTED SECTION.
    METHODS main REDEFINITION.
    METHODS get_json_string
      RETURNING VALUE(json_string) TYPE string.  
  PRIVATE SECTION.
ENDCLASS.



CLASS zcl_rap_generator_console_01 IMPLEMENTATION.
  METHOD main.
    TRY.
        DATA(json_string) = get_json_string(  ).
        DATA(rap_generator) = /dmo/cl_rap_generator=>create_for_cloud_development( json_string ).

        DATA(framework_messages) = rap_generator->generate_bo( ).
        IF rap_generator->exception_occured( ).
          out->write( |Caution: Exception occured | ) .
          out->write( |Check repository objects of RAP BO { rap_generator->get_rap_bo_name(  ) }.| ) .
        ELSE.
          out->write( |RAP BO { rap_generator->get_rap_bo_name(  ) }  generated successfully| ) .
        ENDIF.
        out->write( |Messages from framework:| ) .
        LOOP AT framework_messages INTO DATA(framework_message).
          out->write( framework_message ).
        ENDLOOP.
      CATCH /dmo/cx_rap_generator INTO DATA(rap_generator_exception).
        out->write( 'RAP Generator has raised the following exception:' ) .
        out->write( rap_generator_exception->get_text(  ) ).
    ENDTRY.
  ENDMETHOD.

  METHOD get_json_string.
    json_string = '{ "Info" : "to be replaced with your JSON string" }' . "ここにJSONを貼り付け
  ENDMETHOD.
ENDCLASS.
```
JSONを貼り付ける前に、Eclipseの設定を確認しておきます。
Window>PreferenceからSource Code Editorの設定を開き、"**Wrap and escape text when pasting into string literal**"にチェックがついていることを確認します（ついていなければ設定）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/922f685f-7220-153d-3087-d56f3b657c8c.png)

`json_string =''`の状態にして、シングルクォート（''）の間にJSONを貼り付けます。

```abap
  METHOD get_json_string.
    json_string = '' . "ここにJSONを貼り付け
  ENDMETHOD.
```

貼り付けたあとは以下のようになります。

```abap
  METHOD get_json_string.
    json_string = '{' && |\r\n|  &&
                  '    "$schema": "https://raw.githubusercontent.com/SAP-samples/cloud-abap-rap/main/json_schemas/RAPGenerator-schema-all.json",' && |\r\n|  &&
                  '    "namespace": "Z",' && |\r\n|  &&
                  '    "dataSourceType": "table",' && |\r\n|  &&
                  '    "implementationtype": "unmanaged_semantic",' && |\r\n|  &&
                  '    "bindingType": "odata_v4_ui",' && |\r\n|  &&
                  '    "package": "ZYASU2112_2",' && |\r\n|  &&
                  '    "draftenabled": true,' && |\r\n|  &&
                  '    "prefix": "",' && |\r\n|  &&
                  '    "suffix": "",' && |\r\n|  &&
                  '    "hierarchy": {' && |\r\n|  &&
                  '        "entityName": "Person",' && |\r\n|  &&
                  '        "dataSource": "zperson_u",' && |\r\n|  &&
                  '        "objectId": "person_id",' && |\r\n|  &&
                  '        "etagMaster": "local_last_changed_at",' && |\r\n|  &&
                  '        "totalEtag": "last_changed_at",' && |\r\n|  &&
                  '        "children": [' && |\r\n|  &&
                  '            {' && |\r\n|  &&
                  '                "entityName": "FamilyMember",' && |\r\n|  &&
                  '                "dataSource": "zfamilymember_u",' && |\r\n|  &&
                  '                "objectId": "member_id",' && |\r\n|  &&
                  '                "etagMaster": "local_last_changed_at"' && |\r\n|  &&
                  '            }' && |\r\n|  &&
                  '        ]' && |\r\n|  &&
                  '    }' && |\r\n|  &&
                  '}' . 
  ENDMETHOD.
```

貼り付け後、クラスを有効化します。

###4. BOを生成
ソースコードを右クリックし、Run As>ABAP Application (Console)をクリックして実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f531fcf7-80f7-b510-d69a-5349ab8ec486.png)

うまくいけば、`RAP BO xxxx generated successfully`というメッセージがコンソールに表示されます。ワーニングが出ていますが無視します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6351553b-6016-0912-ca8c-3d8b1e01704a.png)

生成されたオブジェクトの一部を見てみます。

####Behavior Definiiton
Behavior Definitionを見ると、まだimplementation classの定義がありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ac37cea2-cfaa-9666-625f-b074f33a3673.png)

Source Code Libraryのフォルダを見ると、エンティティに対応したクラスが生成されていました。これらのクラスをBehavior Definitionとひもづける必要がありますが、これは次回実施することにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b23c7d47-a009-0b86-cf64-259c08cafa78.png)

####Service Binding
Service BindingができているのででPublishします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8cc45d3f-0db7-d387-903c-aae66ca7a269.png)

###5. プレビューを見る
Service BindingからPersonを選択してPreviewをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b7048c0a-58fc-67ab-cb6b-2608839fd07b.png)

List Reprotの画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6fd3a375-c2f9-02b2-bb28-5a16976dcb9b.png)
Createをクリックし、項目を入力していくと、フッタのツールバーに"Saving draft..."と表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/15c3da5a-3b87-c3ee-d9f8-38cfe43b0dc1.png)
保存せずに前画面の戻ってもドラフトデータは保存されています。Unmanagedでもドラフトは自動的に保存してくれるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ba8c3b30-9dfd-4744-b017-d141735a658a.png)
ドラフト画面で"Ceate"ボタンを押すと、Object createdというメッセージが表示されますが、保存処理を実装していないのでデータは消えてしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f0ad1a61-58c2-7dca-3728-9a3667ab049c.png)
次回はBehavior Implementationを実装し、データが保存されるようにします。

##参考

[How to use the RAP Generator](https://blogs.sap.com/2020/09/11/how-to-use-the-rap-generator/)


##Appendix:生成されたオブジェクト
以下に生成されたオブジェクトを載せておきます。

###CDS View

```abap:ZI_PERSON_U
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'CDS View forPerson'
define root view entity ZI_PERSON_U
  as select from ZPERSON_U
  composition [0..*] of ZI_FamilyMember_u as _FamilyMember
{
  key PERSON_ID as PersonID,
  FIRST_NAME as FirstName,
  LAST_NAME as LastName,
  EMAIL as Email,
  BIRTHDAY as Birthday,
  STATUS as Status,
  @Semantics.user.createdBy: true
  CREATED_BY as CreatedBy,
  @Semantics.systemDateTime.createdAt: true
  CREATED_AT as CreatedAt,
  @Semantics.user.lastChangedBy: true
  LAST_CHANGED_BY as LastChangedBy,
  @Semantics.systemDateTime.localInstanceLastChangedAt: true
  LOCAL_LAST_CHANGED_AT as LocalLastChangedAt,
  @Semantics.systemDateTime.lastChangedAt: true
  LAST_CHANGED_AT as LastChangedAt,
  _FamilyMember
  
}
```

```abap:ZI_FAMILYMEMBER_U
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'CDS View forFamilyMember'
define view entity ZI_FAMILYMEMBER_U
  as select from ZFAMILYMEMBER_U
  association to parent ZI_Person_u as _Person on $projection.PersonID = _Person.PersonID
{
  key PERSON_ID as PersonID,
  key MEMBER_ID as MemberID,
  FIRSTNAME as Firstname,
  LASTNAME as Lastname,
  @Semantics.systemDateTime.lastChangedAt: true
  LAST_CHANGED_AT as LastChangedAt,
  _Person
  
}
```

###Projection View

```abap:ZC_PERSON_U
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Projection View forPerson'
@ObjectModel.semanticKey: [ 'PersonID' ]
@Search.searchable: true
define root view entity ZC_PERSON_U
  as projection on ZI_Person_u
{
  @Search.defaultSearchElement: true
  @Search.fuzzinessThreshold: 0.90 
  key PersonID,
  FirstName,
  LastName,
  Email,
  Birthday,
  Status,
  CreatedBy,
  CreatedAt,
  LastChangedBy,
  LocalLastChangedAt,
  LastChangedAt,
  _FamilyMember : redirected to composition child ZC_FamilyMember_u
  
}
```

```abap:ZC_FAMILYMEMBER_U
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Projection View forFamilyMember'
@ObjectModel.semanticKey: [ 'MemberID' ]
@Search.searchable: true
define view entity ZC_FAMILYMEMBER_U
  as projection on ZI_FamilyMember_u
{
  @Search.defaultSearchElement: true
  @Search.fuzzinessThreshold: 0.90 
  key PersonID,
  @Search.defaultSearchElement: true
  @Search.fuzzinessThreshold: 0.90 
  key MemberID,
  Firstname,
  Lastname,
  LastChangedAt,
  _Person : redirected to parent ZC_Person_u
  
}
```

###Behavior Definition

```abap:ZI_PERSON_U
unmanaged;
with draft;

define behavior for ZI_PERSON_U alias Person
implementation in class ZBP_I_Person_u unique
draft table ZPERSON00D_U
etag master LocalLastChangedAt
lock master total etag LastChangedAt

{
  field ( readonly )
   PersonID;


  create;
  update;
  delete;

  draft action Edit;
  draft action Activate;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare;

  mapping for ZPERSON_U control ZSPerson_X_u
  {
    PersonID = PERSON_ID;
    FirstName = FIRST_NAME;
    LastName = LAST_NAME;
    Email = EMAIL;
    Birthday = BIRTHDAY;
    Status = STATUS;
    CreatedBy = CREATED_BY;
    CreatedAt = CREATED_AT;
    LastChangedBy = LAST_CHANGED_BY;
    LocalLastChangedAt = LOCAL_LAST_CHANGED_AT;
    LastChangedAt = LAST_CHANGED_AT;
  }

  association _FamilyMember { create; with draft; }
}

define behavior for ZI_FAMILYMEMBER_U alias FamilyMember
implementation in class ZBP_I_FamilyMember_u unique
draft table ZFAMILYMEMB00D_U
etag dependent by _Person
lock dependent by _Person

{
  field ( readonly )
   PersonID,
   MemberID;


  update;
  delete;

  mapping for ZFAMILYMEMBER_U control ZSFamilyMember_X_u
  {
    PersonID = PERSON_ID;
    MemberID = MEMBER_ID;
    Firstname = FIRSTNAME;
    Lastname = LASTNAME;
    LastChangedAt = LAST_CHANGED_AT;
  }

  association _Person { with draft; }
}
```

###Behavior Projection

```abap:ZC_PERSON_U
projection;
use draft;

define behavior for ZC_PERSON_U alias Person

{
  use create;
  use update;
  use delete;

  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;

  use association _FamilyMember { create; with draft; }
}

define behavior for ZC_FAMILYMEMBER_U alias FamilyMember

{
  use update;
  use delete;

  use association _Person { with draft; }
}
```

###Service Definition

```abap:ZPERSON_U
define service ZPERSON_U {
  expose ZC_PERSON_U as Person;
  expose ZC_FAMILYMEMBER_U as FamilyMember;
}
```

###Behavior Inplementation

```abap:ZBP_I_PERSON_U（グローバルクラス）
class ZBP_I_PERSON_U definition
  public
  abstract
  final
  for behavior of ZI_PERSON_U .

public section.
protected section.
private section.
ENDCLASS.



CLASS ZBP_I_PERSON_U IMPLEMENTATION.
ENDCLASS.
```

```abap:BP_I_FAMILYMEMBER_U（グローバルクラス）
class ZBP_I_FAMILYMEMBER_U definition
  public
  abstract
  final
  for behavior of ZI_PERSON_U .

public section.
protected section.
private section.
ENDCLASS.



CLASS ZBP_I_FAMILYMEMBER_U IMPLEMENTATION.
ENDCLASS.
```

```abap:ZBP_I_PERSON_U（ローカルクラス）
CLASS LHC_PERSON DEFINITION INHERITING FROM CL_ABAP_BEHAVIOR_HANDLER.
  PRIVATE SECTION.
    METHODS:
      CREATE FOR MODIFY
        IMPORTING
          ENTITIES FOR CREATE  Person,
      UPDATE FOR MODIFY
        IMPORTING
          ENTITIES FOR UPDATE  Person,
      DELETE FOR MODIFY
        IMPORTING
          KEYS FOR DELETE  Person,
      LOCK FOR LOCK
        IMPORTING
          KEYS FOR LOCK  Person,
      READ FOR READ
        IMPORTING
          KEYS FOR READ  Person
        RESULT result,
      CBA_FAMILYMEMBER FOR MODIFY
        IMPORTING
          ENTITIES_CBA FOR CREATE  Person\_FamilyMember,
      RBA_FAMILYMEMBER FOR READ
        IMPORTING
          KEYS_RBA FOR READ  Person\_FamilyMember
        FULL result_requested
        RESULT result
        LINK association_links.
ENDCLASS.

CLASS LHC_PERSON IMPLEMENTATION.
  METHOD CREATE.
  ENDMETHOD.
  METHOD UPDATE.
  ENDMETHOD.
  METHOD DELETE.
  ENDMETHOD.
  METHOD LOCK.
  ENDMETHOD.
  METHOD READ.
  ENDMETHOD.
  METHOD CBA_FAMILYMEMBER.
  ENDMETHOD.
  METHOD RBA_FAMILYMEMBER.
  ENDMETHOD.
ENDCLASS.
CLASS LCL_ZI_PERSON_U DEFINITION INHERITING FROM CL_ABAP_BEHAVIOR_SAVER.
  PROTECTED SECTION.
    METHODS:
      FINALIZE REDEFINITION,
      CHECK_BEFORE_SAVE REDEFINITION,
      SAVE REDEFINITION,
      CLEANUP REDEFINITION,
      CLEANUP_FINALIZE REDEFINITION.
ENDCLASS.

CLASS LCL_ZI_PERSON_U IMPLEMENTATION.
  METHOD FINALIZE.
  ENDMETHOD.
  METHOD CHECK_BEFORE_SAVE.
  ENDMETHOD.
  METHOD SAVE.
  ENDMETHOD.
  METHOD CLEANUP.
  ENDMETHOD.
  METHOD CLEANUP_FINALIZE.
  ENDMETHOD.
ENDCLASS.
```

```abap:ZBP_I_FAMILYMEMBER_U（ローカルクラス）
CLASS LHC_FAMILYMEMBER DEFINITION INHERITING FROM CL_ABAP_BEHAVIOR_HANDLER.
  PRIVATE SECTION.
    METHODS:
      UPDATE FOR MODIFY
        IMPORTING
          ENTITIES FOR UPDATE  FamilyMember,
      DELETE FOR MODIFY
        IMPORTING
          KEYS FOR DELETE  FamilyMember,
      READ FOR READ
        IMPORTING
          KEYS FOR READ  FamilyMember
        RESULT result,
      RBA_PERSON FOR READ
        IMPORTING
          KEYS_RBA FOR READ  FamilyMember\_Person
        FULL result_requested
        RESULT result
        LINK association_links.
ENDCLASS.

CLASS LHC_FAMILYMEMBER IMPLEMENTATION.
  METHOD UPDATE.
  ENDMETHOD.
  METHOD DELETE.
  ENDMETHOD.
  METHOD READ.
  ENDMETHOD.
  METHOD RBA_PERSON.
  ENDMETHOD.
ENDCLASS.
```