【RAP】RAP Generatorで生成したBOに子のエンティティを追加

## はじめに
ADTのRAP Generatorでは、複数階層を持つBOを作ることができません（2022年1月現在）。そこで今回は、RAP Generatorで生成したBOにマニュアルで子のエンティティ(FamilyMember)を追加します。ベースとするのは[前回](https://qiita.com/tami/items/96f20feb5c1f74b22803)の記事で作成したLate Numberingが実装されたBOです。
この記事の目的は、**RAP Generatorで生成したBOに子のエンティティを追加したいとき、どこに何を追加すればよいか**を説明することです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5fb0ce6d-4488-e194-c93b-1ed9361d9e9c.png)

## 子のエンティティを追加する手順
以下の図は子のエンティティ追加に伴って追加、更新が必要になるオブジェクトを表しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2f57661a-1e01-eaac-fd2f-6e9ff3606cf1.png)

＜手順＞
1. 子のエンティティのためのテーブルを登録
1. CDS View, Projection Viewを登録
1. Behavior Definitionの更新
1. Behavior Projectionの更新
1. Service Definitionの更新
1. Service Binidngの更新
1. Metadata Extensionの登録・更新
1. Behavior Implementationの更新


### 1. 子のエンティティのためのテーブルを登録
以下のテーブルを登録します。

```abap
@EndUserText.label : 'Family Member'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfamilymember_m {
  key client      : 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_lnum_m
      where person_id = zfamilymember_m.person_id;
  key member_id   : ze_member_id not null;
  firstname       : ze_firstname;
  lastname        : ze_lastname;
  last_changed_at : abp_lastchange_tstmpl;

}
```
###2. CDS View, Projection Viewを登録
上記で登録したテーブルを参照してCDS ViewおよびProjection Viewを登録します。

#### 2.1. CDS View
まず以下の状態で一旦有効化します。

```abap
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Family Member'
@ObjectModel.usageType:{
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
}
define view entity ZI_FAMILYMEMBER_M as select from zfamilymember_m {
    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
}

```

次に、親のCDS ViewであるZI_PERSON_LNUM_Mとのアソシエーションを定義します。親側にも子のビューであるZI_FAMILYMEMBER_Mをコンポジションとして定義します。2つのビューを同時に有効化します。

```abap:子のビュー
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Family Member'
@ObjectModel.usageType:{
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
}
define view entity ZI_FAMILYMEMBER_M 
  as select from zfamilymember_m 
  association to parent ZI_PERSON_LNUM_M 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 //追加
}
```

```abap:親のビュー
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '##GENERATED Managed Person for Late Numbering'
define root view entity ZI_PERSON_LNUM_M
  as select from zperson_lnum_m as Person
  composition [0..*] of ZI_FAMILYMEMBER_M 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.localInstanceLastChangedBy: 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 //追加
  
}
```

#### 2.2. Projection View
2.1.で作成したCDS Viewを参照したProjection Viewを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/10930f4a-3cab-7fe2-d556-4b0f0cc61fa5.png)

以下の状態で一旦有効化します。

```abap
@EndUserText.label: 'Family Member'
@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity ZC_FAMILYMEMBER_M as projection on ZI_FAMILYMEMBER_M {
    key PersonId,
    key MemberId,
    Firstname,
    Lastname,
    LastChangedAt,
    /* Associations */
    _Person
}
```

次に、親のProjection ViewであるZC_PERSON_LNUM_Mにコンポジションを定義します。また、子のViewの_Personが親のProjection Viewを指すようにアソシエーションをリダイレクトさせます。2つのビューを同時に有効化します。

```abap:親のView
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'Projection View for ZI_PERSON_LNUM_M'
define root view entity ZC_PERSON_LNUM_M
  as projection on ZI_PERSON_LNUM_M
{
  key personid,
  firstname,
  lastname,
  email,
  birthday,
  status,
  locallastchangedat,
  
  _FamilyMember: redirected to composition child ZC_FAMILYMEMBER_M //追加
  
}

```

```abap:子のView
@EndUserText.label: 'Family Member'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
define view entity ZC_FAMILYMEMBER_M as projection on ZI_FAMILYMEMBER_M {
    key PersonId,
    key MemberId,
    Firstname,
    Lastname,
    LastChangedAt,
    /* Associations */
    _Person: redirected to parent ZC_PERSON_LNUM_M //追加
}

```

### 3. Behavior Definitionの更新
Behavior Definitionに子のエンティティを追加します。また、親のBehavior Definiionに`association _FamilyMember { create; }`を追加します。これにより、親の登録時にCreate Deepで子のエンティティも登録できるようになります。

```abap
managed implementation in class zbp_person_lnum_m unique;
strict;
with draft;

define behavior for ZI_PERSON_LNUM_M alias Person
persistent table zperson_lnum_m
draft table zperson_lnum_m_d
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master( global )
late numbering

{
  field ( readonly )
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy,
   PersonID;


  create;
  update;
  delete;
  association _FamilyMember { create; }　//追加

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

  mapping for ZPERSON_LNUM_M
  {
    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;
  }
}

//追加
define behavior for ZI_FAMILYMEMBER_M alias FamilyMember
persistent table zfamilymember_m
draft table zfamilym_m_d
lock dependent by _Person
authorization dependent by _Person
etag master LastChangedAt
late numbering
{
  field ( readonly )
    PersonId,
    MemberId,
    LastChangedAt;

  update;
  delete;
  association _Person { }

  mapping for zfamilymember_m
  {
    PersonId = person_id;
    MemberId = member_id;
    Firstname = firstname;
    Lastname = lastname;
    LastChangedAt = last_changed_at;
  }

}
```

ドラフトテーブルのところで、「テーブルが登録されていない」とエラーが出るのでQuick Fix(Ctrl + 1)で登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a74d9c4b-a479-e4a5-3b73-5b4600b3cf2d.png)

初期状態だとクライアントが"mandt"となっていて元のテーブルと一致しないため、"client"に変更します。

```abap
@EndUserText.label : 'Draft table for entity ZI_FAMILYMEMBER_M'
@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfamilym_m_d {
  key mandt       : mandt not null; //clientに変更
  key personid    : ze_person_id not null;
  key memberid    : ze_member_id not null;
  key draftuuid   : sdraft_uuid;
  parentdraftuuid : sdraft_uuid;
  firstname       : ze_firstname;
  lastname        : ze_lastname;
  lastchangedat   : abp_lastchange_tstmpl;
  "%admin"        : include sych_bdl_draft_admin_inc;

}
```
### 4. Behavior Projectionの更新
Behavior Projectionに子のエンティティを追加します。また、親のBehavior Projectionに`ssociation _FamilyMember { create; }`を追加します。

```abap
projection;
strict;
use draft;

define behavior for ZC_PERSON_LNUM_M alias Person
use etag

{
  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; } //追加
}

//追加
define behavior for ZC_FAMILYMEMBER_M alias FamilyMember
use etag
{
    use update;
    use delete;

    use association _Person;
}
```

###5. Service Definitionの更新
Service Definitionに子のエンティティを追加します。

```abap
@EndUserText.label: 'Service definition forZC_PERSON_LNUM_M'
define service ZPERSON_LNUM_M {
  expose ZC_PERSON_LNUM_M as Person;
  expose ZC_FAMILYMEMBER_M as FamilyMember; //追加
}
```
### 6. Service Binidngの更新
Service Bindingに追加したエンティティを反映させようと思い、"Unpublish"や"Add Service"など試してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e09f1410-0273-0711-96c3-a12cae38dc25.png)
結局何が効いたのかわからなかったのですが、しばらくすると子のエンティティが入ってきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e8c7ea6c-73fc-3330-3158-33264fc7036f.png)
思い当たるのは以下のようなことです。

- Service Definitionを有効化した
- 反映にやや時間がかかる？

### 7. Metadata Extensionの登録・更新
Object PageにFamily Memberをテーブルとして表示させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/da41bc0b-d342-13ba-ccee-41e51382c812.png)

#### 7.1. 子のMetadata Extensionを登録
子のMetadata Extensionを以下のように定義します。

```abap
@Metadata.layer: #CUSTOMER
annotate view ZC_FAMILYMEMBER_M
    with 
{
  @UI.facet: [ {
    id: 'idIdentification', 
    type: #IDENTIFICATION_REFERENCE, 
    label: 'Family Member', 
    position: 10 
  }]    
  @UI.lineItem: [ {
    position: 10 , 
    importance: #MEDIUM
  } ]
  @UI.identification: [ {
    position: 10 
  } ]  
    MemberId;
  @UI.lineItem: [ {
    position: 20 , 
    importance: #MEDIUM
  } ]    
  @UI.identification: [ {
    position: 20 
  } ]  
    Firstname;
  @UI.lineItem: [ {
    position: 30 , 
    importance: #MEDIUM
  } ]    
  @UI.identification: [ {
    position: 30 
  } ]  
    Lastname;
    
}
```

#### 7.2. 親のMetadata Extensionを更新
親のMetadata ExtensionにFacetを追加します。

```abap
@Metadata.layer: #CUSTOMER
@UI: {
  headerInfo: {
    typeName: 'Person', 
    typeNamePlural: 'Persons'
  }
}

annotate view ZC_PERSON_LNUM_M with
{
  @UI.facet: [ {
    id: 'idIdentification', 
    type: #IDENTIFICATION_REFERENCE, 
    label: 'Person', 
    position: 10 
  },{ 
    id: 'idFamilyMembers', //追加
    type: #LINEITEM_REFERENCE,
    label: 'Family Members',
    targetElement: '_FamilyMember',
    position: 20
   } ]    
  @UI.lineItem: [ {
    position: 10 , 
    importance: #MEDIUM
  } ]
  @UI.identification: [ {
    position: 10 
  } ]
  personid;
```

プレビューを見てみると、Object PageにFamily Membersのテーブルが追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1468e446-78a9-7af6-5a7a-d054d187d6f6.png)

### 8. Behavior Implementationの更新
メソッド`adjust_numbers`のchangingパラメータ`mapped`は、新規Person登録時と既存のPersonにFamily Memberを追加した時に、それぞれ以下のような入り方をします。

新規Person登録時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8f674b51-845d-3355-b301-01a67f1163b6.png)

既存のPersonにFamily Memberを追加した時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e2e8c075-3e44-ab2f-24f6-ec9ea2e2bc04.png)

動作確認してわかったのは、

- 新規Person登録時、子のエンティティであるFamily MemberはMemberIdのみ埋めればよく、
- 既存のPersonにFamily Memberを追加するときは、PersonIdとMemberIdとの両方を埋める必要がある

ということです。
既存のPersonにFamily Memberを追加するとき、%tmpの構造にPersonIdが入ってくるので、それを利用することができます。


メソッド`adjust_numbers`を以下のように変更します。

```abap
  METHOD adjust_numbers.

    DATA new_id TYPE zperson_lnum_m-person_id.

    LOOP AT mapped-person ASSIGNING FIELD-SYMBOL(<person>).
      IF new_id IS INITIAL.
        "Get max person id from db
        SELECT MAX( person_id ) FROM zperson_lnum_m INTO @new_id.
      ENDIF.
      new_id += 1.
      <person>-PersonID = new_id.
    ENDLOOP.

    DATA new_member_id TYPE zfamilymember_m-member_id.

    LOOP AT mapped-familymember ASSIGNING FIELD-SYMBOL(<member>).
      "set parent id
      <member>-PersonId = <member>-%tmp-PersonId.

      IF new_member_id IS INITIAL.
        new_member_id = get_max_member_id( <member>-PersonId ).
      ENDIF.
      new_member_id += 1.
      <member>-MemberId = new_member_id.
    ENDLOOP.

  ENDMETHOD.

  METHOD get_max_member_id.

    IF person_id IS NOT INITIAL.
      "get max member id from db
      SELECT MAX( member_id ) FROM zfamilymember_m
        WHERE person_id = @person_id
        INTO @member_id.
    ELSE.
      member_id = 0.
    ENDIF.

  ENDMETHOD.
```

## 動作確認
新規登録時、変更時で子のID（Member ID）が採番されます。Late Numberingなので、Createを押したタイミングではIDがなく、保存してからIDが採番されるという動きです。明細行を追加したときにIDがないと変な感じなので子のエンティティは[Early Numbering](https://qiita.com/tami/items/e152bad7f82bce45ba7d)でもよかったと思いました。
![rap_save.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/780d76be-7418-243d-0c08-c05ce5e8f434.gif)