【RAP】Early Numbering

## はじめに
この記事では、RAP(ABAP RESTful Application Programming Model)でキーの採番(Numbering)をする方法について紹介します。

シリーズの先頭はこちら☞ [【RAP】ADTのRAP Generatorを使ってみる](https://qiita.com/tami/items/a9f3d2d4afb28d53919b#%E7%B6%9A%E7%B7%A8)

## RAPでの採番方法
RAPでの採番方法には、以下のようなパターンがあります。
参考：[Numbering](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/d85aec25222145f0b0cbbe8b02db51f0.html)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f4923b22-4616-dafc-532a-d79f88f37fa1.png)
まず大きく**Early Numbering**と**Late Numbering**があり、Early Numberingはさらに枝分かれしています。これらの方法はBOのタイプ(Managed, Unmanaged)、ドラフトの有無にかかわらず使うことができます（※）。
Numberingの種類を説明するManaged、Unmagaedは、BOの種類とは関係なく、番号を自分で(Unmanaged)採番するか、フレームワークが(Managed)採番するかの違いを表しています。

※Late NumberingについてはUnmanagd BOでしか使えないという情報もあるので次回確認します。[ドキュメント](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/754482eb5e624aada5d17696a6ab11b6.html)を見ると、どちらでも使えそうですが…

2022/1/15 更新
BTPのABAP環境では、Managed BOでもLate Numberingを使うことができました。

### Early Numbering, Late Numberingとは
Early、Lateはキーの採番タイミングを指します。**Early Numbering**では、エンティティの登録リクエストがあると即座に採番を行います。このあとでチェックやDBへの保存が行われるため、登録がキャンセルされた場合はその番号は不使用となり、番号に抜けが発生する可能性があります。**Late Numbering**ではDBへの保存の直前に採番が行われ、この後にエラーは基本起こりえないため、番号に抜けが発生しません。

### Early Numberingの分類
Early Numberingはさらに枝分かれします。**External Early Numbering**とは、キーをユーザの入力など外部から受け取ることを指します。これに対して**Internal Early Numbering**とは、自動で採番を行うことを指します。**Managed Internal Early Numbering**はフレームワークに番号の採番を任せる方法で、この場合キーに使えるのはUUIDのみです。**Unmanaged Internal Early Numbering**は自分で番号を採番する方法です。

## Unmanaged Internal Early Numberingを実装
この記事では、Unmanaged Internal Early Numberingを実装してみます。この方法は、テーブルがUUIDでないキーを持ち、キーの採番を自動で行いたい場合に使用します。
ステップは以下のようになります。

1. UUIDでないキーを持つテーブルを登録
1. RAP GeneratorでBOを生成
1. Early Numberingの実装

### 1. UUIDでないキーを持つテーブルを登録
以下のテーブルを定義します。

```abap
@EndUserText.label : 'Managed Person with Numeric Key'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zperson_num_m {
  key client            : 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;

}
```

キーのために以下のデータエレメントを定義しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e43ac375-e76c-75b8-25a7-778ff4f05f4a.png)

### 2. RAP GeneratorでBOを生成
[こちら](https://qiita.com/tami/items/a9f3d2d4afb28d53919b)の記事の要領でBOを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6f91e6ec-86c5-aa22-6054-56bb9c2ccfdb.png)

Behavior Definitionを見てみると、UUIDをキーにしていたときとは以下の違いがあります。

- キー項目がreadonlyになっていない
- numberingに関する指定がない

UUIDがキーのとき

```abap
{
  field ( readonly )
   PersonUUID,
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy;

  field ( numbering : managed )
   PersonUUID;
```
UUIDがキーでないとき

```abap
{
  field ( readonly )
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy;
```
すなわちデフォルトでは、キーを外部から受け取るExternal Early Numberingになっているということです。

### 3. Early Numberingの実装
#### 3.1. Behavior Definitionの設定
Behavior Definitionの先頭に`early numbering`の指定を追加します。また、キー項目であるPersonIDをreadonlyに設定します。

```abap
define behavior for ZI_PERSON_NUM_M alias Person
persistent table zperson_num_m
draft table zperson_num_m_d
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master( global )
early numbering //追加

{
  field ( readonly )
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy,
   PersonID; //追加
```
createのところに警告が出るので、Quick Fix(Ctrl + 1)でメソッドを実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5cdd8568-44d0-781d-8530-d4df35b56ca1.png)

Behavior InplementationクラスのLocal Typeにメソッド`earlynumbering_create`が追加されます。

```abap
CLASS LCL_HANDLER DEFINITION INHERITING FROM CL_ABAP_BEHAVIOR_HANDLER.
  PRIVATE SECTION.
    METHODS:
      GET_GLOBAL_AUTHORIZATIONS FOR GLOBAL AUTHORIZATION
        IMPORTING
           REQUEST requested_authorizations FOR Person
        RESULT result,
      earlynumbering_create FOR NUMBERING
            IMPORTING entities FOR CREATE Person.
ENDCLASS.

CLASS LCL_HANDLER IMPLEMENTATION.
  METHOD GET_GLOBAL_AUTHORIZATIONS.
  ENDMETHOD.
  METHOD earlynumbering_create.
  ENDMETHOD.

ENDCLASS.
```

#### 3.2. Behavior Implementationの実装
メソッド`earlynumbering_create`を以下のように実装します。メソッドのリターンパラメータは`failed`、`reported`、`mapped`という3つのテーブルです。failed、reportedはエラーの場合に返すもので、今回重要なのがmappedです。mappedにはフレームワークで採番される仮のキー(%cid)と自分で採番したキーのマッピングを返します。
デバッグで見ると、mapped-personは以下の構造でした。つまり、ドラフトかどうかもキーに含まれるということです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/48ec0f87-a89b-37d0-c051-7feb0a795cce.png)

**最初の考え**
最初は以下のような実装を考えました。簡単にするためDBから最大のキーを取得してそれをインクリメントします。
参考：BTPで番号範囲を使うには、以下の方法があります。
https://blogs.sap.com/2020/09/03/number-ranges-sap-cloud-platform/

```abap
  METHOD earlynumbering_create.
    "Get max person id from db
    select max( person_id ) from zperson_num_m into @data(new_id).

    loop at entities ASSIGNING FIELD-SYMBOL(<person>).
      new_id += 1.
      append value #(
        %cid = <person>-%cid
        %is_draft = <person>-%is_draft
        personid = new_id
       ) to mapped-person.
    endloop.

  ENDMETHOD.
```
しかし、このメソッドはドラフトを登録したときと保存したときの2回動くことがわかりました。ドラフト状態のオブジェクトが複数作られるとキーがかぶってしまう可能性があります。そこで、以下のようにドラフトテーブルと本物のテーブルの両方を見に行くように変更しました。
また、保存時にドラフトと同じようにキーを採番すると、ドラフトのときのキーを上書きしてキーが変わってしまうこともわかったので、キーがすでに設定されている場合はその値をそのままmappedに渡すこととしました。

**修正後**

```abap
  METHOD earlynumbering_create.
    DATA new_id TYPE zperson_num_m-person_id.

    LOOP AT entities ASSIGNING FIELD-SYMBOL(<person>).
      if <person>-PersonID is not initial.
        new_id = <person>-PersonID.
      else.
        new_id = get_newid(  ).
      endif.

      APPEND VALUE #(
        %cid = <person>-%cid
        %is_draft = <person>-%is_draft
        personid = new_id
       ) TO mapped-person.

    ENDLOOP.

  ENDMETHOD.

  METHOD get_newid.
    "Get max person id from db and draft
    SELECT MAX( person_id ) FROM zperson_num_m INTO @DATA(max_id).
    SELECT MAX( personid ) FROM zperson_num_m_d INTO @DATA(max_id_draft).

    new_id = COND #( WHEN max_id > max_id_draft
                       THEN max_id
                       ELSE max_id_draft  ).
    new_id += 1.

  ENDMETHOD.
```

### 4. 動作確認
"Create"を押して新規データを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/69798dbb-9fd6-52c0-e666-800d812d0f5a.png)
ドラフトの状態でキーが採番されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/14e2e5e3-8ed1-5c25-5c06-69e25c022900.png)
保存せずに戻り、もう一つデータを登録します。正しくキーがインクリメントされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/89bea1a1-502c-a5b8-8d49-f36096f0e728.png)



## 参考
[How to use Early Numbering with Semantic Keys – RAP Managed BO](https://blogs.sap.com/2021/11/23/how-to-use-early-numbering-with-semantic-keys-rap-managed-bo/)