ASTERIA Warp 2312　RecordGroup使ってみた

## はじめに
2023年12月に実装された新コンポーネント「RecordGroup」の登場で今まで少し複雑になっていたキーブレイクが簡単にできる様になりました。使いどころがわかりにくい感じのコンポーネントですが利用する事でロジックが簡略化できるコンポーネントですので簡単なサンプルで使ってみました。

## RecordGroupとは
<img title="RecordGroup" width="50" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/58ea0ad9-8f7c-d526-c47d-1ba1a489dea5.png"/>

[RecordGroup](https://help.asteria.com/documentation/warp/ja/2312/flow/designer/reference/component/RecordGroup.html)は、ASTERIA Warp 2312に実装された新コンポーネントです。入力されたストリーム（Record,CSV,FixedLength）を指定したカラムで複数のストリームに分け、それ毎にループを行います。ポイントは 
* グループキーとなるフィールドを１つ選択する
* 選択されたグループキーの順でストリーム化される
~~・ 分割されたグループ内のソート順は分割前の状態で保持される~~ [^1]
[^1]:2024/02/02追記
分割されたグループ内のソート順ですが追試した結果ソート順は保持されておらずランダムな並び順となっていました。結果、伝票明細など出力に並び順があるものは、グループ内でソートを行う必要がありました。


グループ分割する際のキーが１つのみ選択可という事で複数のキーがある場合はグループ化キーの結合が必要です。また出力時に並び順を考慮する必要がある場合は~~事前~~グループに分割されたストリームに対しソートが必要になります。このルールをベースに構築を行います。
２つのグループキーを利用する、といった場合、グループキーでまとまった小計が必要であれば、並び順を意識してグループキーを作成して1つのグループキーでループします。グループキーが複数必要な場合、例えば大分類、小分類などの複数のグルーピングが必要な場合では１つ目のグループキーでRecordGroupを開始後、そのループの最初んに小分類でソート、その後RecordGroupでループを行います。その場合でもソート順は保持されないため、明細出力が必要な場合は最初にソートを行います。
RecordGroupはあるキーを元にグループ化可能なため、明細データに対するヘッダー、フッターなどの挿入する処理や集計処理に向いています。

## 小計の挿入１
CSVデータの明細行の間に単一グループの小計行を挿入します。

入力データは以下のデータです。
```
日付,動物の種類,動物名,金額
2023/01/01,哺乳類,ライオン,100000
2023/01/01,哺乳類,トラ,80000
2023/01/01,哺乳類,キリン,50000
2023/01/01,鳥類,ペンギン,15000
2023/01/02,哺乳類,ライオン,100000
2023/01/02,哺乳類,トラ,80000
2023/01/02,鳥類,フラミンゴ,20000
2023/01/02,鳥類,ペンギン,15000
2023/01/03,哺乳類,トラ,80000
2023/01/03,哺乳類,キリン,50000
2023/01/03,鳥類,フラミンゴ,20000
2023/01/03,鳥類,ペンギン,15000
2023/01/04,哺乳類,ライオン,100000
2023/01/04,哺乳類,トラ,80000
2023/01/04,鳥類,ペンギン,15000
```

このファイルに「各日付の種類別小計を出力する」というケースを例とします。
通常このような処理であれば１レコード毎のループを使ったキーブレイク判定を行い処理を分岐したり、RecordSQLを使用する事が多いと思いますが、今回はRecordGroupのみで構築します。
事前にストリームがキー毎に分割されるRecordGroupを使用する事でキーブレイク判定が不要になります。
グループキー＝小計の単位としているため、１ストリームのすべての金額の集計を行う事で小計が生成できます。

### 処理フロー説明

<img title="フロー１" width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/15fe6ba7-6be5-803f-141f-6010d17d32d7.png"/>

【処理概要】
1. ファイルの読み込みと初期処理（グループキー設定、ソート）
1. RecordGroupのループ
1. 明細部、合計部を別々に処理するループ

#### ファイルの読み込みと初期処理
FileGetのファイルのストリームフィールドはすべてString型を指定しています。
<img title="FileGet1" width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/8fd735a3-13bf-41fe-0da2-1d82300f3df7.png"/>


キーとなるのは日付ですがこの例ではDateTime型とせずにそのままString型で処理します。

<img title="Mapper1" width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/7e8bb29e-6800-e91d-9fcc-c29882a8a383.png"/>

グループキーとなるのは日付と動物の種類ですのでRecordGroupの前にグループキーとして結合します。ソート順はそのままでも問題ありませんが、動物順でソートします。

グループキーをフィールド指定し、グループキーの毎のストリームループを開始します（ループ１）

#### RecordGroupのループ
このループでは、グループキー毎に分解されたストリームとなっていますので、それを「明細部」「小計部」として２回利用するためLoopStartにより２回のループ（ループ２）を行います。
ループ１のLoopEndのストリームの出力形態は"ストリームをまとめる"、ループ１のLoopEndのストリームの出力形態は"コンテナ"としておきます。（ストリームをまとめるでも問題ありません）

#### 明細部、合計部を別々に処理するループ
ループの実行回数を2でMODして余りの値が1か0かを判定し、BranchStartの条件に直接指定を行います。
<img title="イメージ5" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/1df8b477-ed26-b9f5-8ac5-98ecb98c01b1.png"/>

明細部、合計部はともにMapperで新しいストリームを生成しグループキーは除きます。明細部のストリームの「先頭行のフィールド名を出力」には"はい"を指定しておきます。
明細部Mapperはカラムの移送のみを行います。
合計部Mapperではマッパー変数「小計」を登録し初期値を0とします。通常レイヤー「加算」とストリームの最終レコードのみ処理を行う条件レイヤー「移送」を使用し合計金額のサマリーを行います。
通常レイヤー「加算」では、マッパー変数の加算のみを行います。

<img title="イメージ6" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/37b493d7-d056-7ae6-c9c8-eb76f4653dd2.png"/>

条件レイヤー「移送」では、条件式に＄Stream.RecordNo = $stream.RecordCount」（最初の＄は半角）を指定し、「小計」を出力ストリームの「金額」に、グループキーを見出して「動物の種類」に移送します。こうする事で、このMapperでは最終行にそのストリームの金額の合計が１レコードのみ格納されるストリームとなります。

<img title="イメージ7" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/c5fd0d8d-410c-7e2c-ea20-afca14538fc8.png"/>

この２つのストリームはBranchEndで合流し、明細、合計をグループキーの数だけ繰り返すコンテナとして保存され、それが最後にループ１のLoopEndで1つのストリームにまとめられて出力されます。

出力結果
```
日付,動物の種類,動物名,金額
2023/01/01,哺乳類,キリン,50000
2023/01/01,哺乳類,トラ,80000
2023/01/01,哺乳類,ライオン,100000
,【2023/01/01:哺乳類 小計】,,230000
2023/01/01,鳥類,ペンギン,15000
,【2023/01/01:鳥類 小計】,,15000
2023/01/02,哺乳類,トラ,80000
2023/01/02,哺乳類,ライオン,100000
,【2023/01/02:哺乳類 小計】,,180000
2023/01/02,鳥類,フラミンゴ,20000
2023/01/02,鳥類,ペンギン,15000
,【2023/01/02:鳥類 小計】,,35000
2023/01/03,哺乳類,キリン,50000
2023/01/03,哺乳類,トラ,80000
,【2023/01/03:哺乳類 小計】,,130000
2023/01/03,鳥類,フラミンゴ,20000
2023/01/03,鳥類,ペンギン,15000
,【2023/01/03:鳥類 小計】,,35000
2023/01/04,哺乳類,トラ,80000
2023/01/04,哺乳類,ライオン,100000
,【2023/01/04:哺乳類 小計】,,180000
2023/01/04,鳥類,ペンギン,15000
,【2023/01/04:鳥類 小計】,,15000
```

## 小計の挿入２
今度は単一グループではなく、２つのグルーピングでそれぞれの小計を出力するケースを考えます。上記のCSVであれば、日付、動物の種類をそれぞれグルーピングするものです。
基本的な構成は小計の挿入１と同じですが、明細部と小計部を２回のRecordGroupのループによりグループ化します。

### 処理フロー説明

<img title="フロー４" width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/defae3b9-2173-6fb2-212e-5e323137649f.jpeg"/>

【処理概要】
1. ファイルの読み込み
1. RecordGroup日付のループ
1. 日付部の明細部、小計部を別々に処理するループ
1. 日付の明細部を再度 RecordGroup動物の種類でループ
1. 動物名でソートし、動物の種類の明細部、小計部を別々に処理するループ
1. 明細部のストリーム出力
1. 動物の種類の小計出力
1. 日付の小計出力

出力結果は以下の様になります。

出力結果
```
"日付","動物の種類","動物名","金額"
"2023/01/01","哺乳類","キリン","50000"
"2023/01/01","哺乳類","トラ","80000"
"2023/01/01","哺乳類","ライオン","100000"
"","【哺乳類　計】","","230000"
"2023/01/01","鳥類","ペンギン","15000"
"","【鳥類　計】","","15000"
"","【2023/01/01　計】","","245000"
"2023/01/02","哺乳類","トラ","80000"
"2023/01/02","哺乳類","ライオン","100000"
"","【哺乳類　計】","","180000"
"2023/01/02","鳥類","フラミンゴ","20000"
"2023/01/02","鳥類","ペンギン","15000"
"","【鳥類　計】","","35000"
"","【2023/01/02　計】","","215000"
"2023/01/03","哺乳類","キリン","50000"
"2023/01/03","哺乳類","トラ","80000"
"","【哺乳類　計】","","130000"
"2023/01/03","鳥類","フラミンゴ","20000"
"2023/01/03","鳥類","ペンギン","15000"
"","【鳥類　計】","","35000"
"","【2023/01/03　計】","","165000"
"2023/01/04","哺乳類","トラ","80000"
"2023/01/04","哺乳類","ライオン","100000"
"","【哺乳類　計】","","180000"
"2023/01/04","鳥類","ペンギン","15000"
"","【鳥類　計】","","15000"
"","【2023/01/04　計】","","195000"
```
これではわかりにくいのでEXCELに展開するとこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/b6e8cb5c-59b2-be11-9946-b749cd29bc3a.png)

正直なところ、非常に難解になってしまうため、２つのグループキーが必要なケースでは、別の手法を選択したほうがシンブルになるかと思います。

## 縦横変換
データ変換データの縦横変換が必要な場合でRecordGroupを使用する事で簡単な縦横変換を行えます。今回は先ほどのグループキー毎に明細の動物を最大５まで横に展開するケースで説明します。入力データは小計と同じものを使用します。

### 処理フロー説明

<img title="mRecordGroupSample2-1" width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/126e3452-4b1b-cb2d-b46b-bbd243be6cad.png"/>

【処理概要】
1. ファイルの読み込みと初期処理（グループキー設定、ソート）
1. RecordGroupのループ
1. ストリーム型の変換

#### ファイルの読み込みと初期処理
小計と同様で変更はありません

#### RecordGroupのループ
縦横変換のストリーム型は先ほどのフローの合計部のみを残したものを加工し、日付、動物の種類、動物名と設定します。囲み文字は”(None)”を指定します。（ヘッダの有無はどれでも構いません）

<img title="Mapper3" width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/fec2b086-6111-7295-59e6-cb99c506031b.png"/>


Mapper内では、マッパー変数「動物」と通常レイヤー「結合」、条件レイヤー「移送」を使用します。
通常レイヤー「結合」ではConcatenateを使用しカンマ結合のみを行います。

<img title="イメージ9" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/e49f7fb3-e20a-456c-5bbe-e024d1ba2ef0.png"/>

<img title="イメージ10" width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/5f8f36be-0302-b80f-7cb0-907f186c769d.png"/>

条件レイヤー「移送」の条件は、小計と同様最終レコードの場合のみのマッピング条件とし、日付、動物の種類と動物名マッパー変数に移送します。
<img title="イメージ11" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/20ccd989-8fc0-ca3b-e59d-030199ff6216.png"/>

ここでCSVの型指定で、文字囲みをしていません。囲み文字が最終イメージで必要な場合でもこの段階では囲み文字なしで処理し、ループ終了後に文字囲みの有るストリームにコンバートする必要があります。

#### ストリーム型の変換
<img title="イメージ11" width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680444/b22aac50-2724-3228-f6fc-8e8235bbc689.png"/>
CSVを一旦Text型にコンバートし、さらにそのストリームをCSV型にコンバートします。これにより、再度CSV化の判定が行われます。ループの段階で囲み文字を指定していると、動物名カラムが再分割されません。今回のサンプルではループ内のストリームにはヘッダ出力指定にしていますのでTextへのコンバート時にヘッダが出力されます。CSV型へのコンバート時に読み込み開始行に2を指定します。

出力結果
```
"日付","動物の種類","動物名1","動物名2","動物名3","動物名4","動物名5"
"2023/01/01","哺乳類","ライオン","トラ","キリン","",""
"2023/01/01","鳥類","ペンギン","","","",""
"2023/01/02","哺乳類","ライオン","トラ","","",""
"2023/01/02","鳥類","ペンギン","フラミンゴ","","",""
"2023/01/03","哺乳類","トラ","キリン","","",""
"2023/01/03","鳥類","ペンギン","フラミンゴ","","",""
"2023/01/04","哺乳類","ライオン","トラ","","",""
"2023/01/04","鳥類","ペンギン","","","",""
```
:::note
RecordGroupの入力ストリームのレコードが無い場合でもこのコンポーネントはループを開始します。レコードが無い場合のエラーを設定しない場合はエラーが発生します。レコードが無い場合の対応を”エラーを無視する”などに設定をすればフローが止まることはありませんが、ループ内の後続の処理でレコードが無い場合を想定する必要があります。そのため、RecordGroupの入力ストリームにレコードが無い場合は、RecordGroupに入らずループを回避する流れとしたほうが確実かと思います。
:::

## 最後に
RecordGroupは「使おう！」と思って使うものでもないため、使用ケースが限られると思いますが、仕様と使い方を知っておくことで、思わぬところで利用が可能となります。筆者自身も当初ヘッダ・フッタの作成処理利用しか思いついていませんでしたが、レコードカラムの一部縦横変換が必要な構築の中で使う事を思いつき簡単な形で実装ができました。キーブレイクを意識しない構築を行う事で使用コンポーネントの大幅な減少が見込めます。
コンポーネントとしては１つのグループキーのみ指定できる事、グループ内のソートキーを指定する事ができない等の懸念点はありますが、他の現行コンポネントでカバーできる範囲かと思います。
今回はストリームをまとめる方法で簡単な説明としましたが、大量データを扱う場合は処理性能に影響を及ぼします。データベースへのレコード挿入や出力内容のTextファイルへの随時出力などでストリームデータ量を減らす必要があります。