【ワンランク上のPower Apps】 CSVインポート その② Power Automateで解析・登録編

# はじめに
こんにちは。ヨウセイです。
今回Microsoft Power Apps Advent Calendar 2022 に初参加しました。
カレンダー２の12/6用の記事です。

当初想定より大容量になってしまい時間も足りずで、記事を分割しての投稿となりました。今回はCSVインポート その② Automateで解析・登録編となります。

※作成後にPower Apps向けの記事だと思い出したので、Power Automateで解析してPower Apps側で登録（最近出てきたJSONParseを使って）編も近日中に投稿予定です。

前回の記事はこちらになります。
[【ワンランク上のPower Apps】 CSVインポート その① パターン紹介編](https://qiita.com/youseibubu/items/dac102f67993413c3428)

**★続きの12/11の記事を書きました。**
[【ワンランク上のPower Apps】 CSVインポート その③ Automateで解析・Appsで登録編 ＆プログレス表示](https://qiita.com/youseibubu/items/b04358342098648326eb)

ちなみに今回本機能を追加した自社アプリはこちらで紹介しております。

https://www.nsurf.co.jp/archives/1825

# 実装サンプル
今回作成したサンプル実装です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">CSVインポートサンプル <a href="https://t.co/UXKKmO5L2R">pic.twitter.com/UXKKmO5L2R</a></p>&mdash; ヨウセイ (@youseibubu) <a href="https://twitter.com/youseibubu/status/1598708509352562688?ref_src=twsrc%5Etfw">December 2, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 実装概要
今回は下のような実装サンプルを作成します。
本来はフローを先に作成しますが、記事ではアプリの取り込み部分から説明します。

+ ファイルの種類：CSV（UTF-8）
+ ファイルのアップ：Power Appsの添付ファイルコントロールを使ってアップし、Automateへ渡す（Power Apps (V2)トリガー）
+ ファイルの解析：Automateでカスタム実装（ループなし）
+ データの登録：AutomateでSPOへ登録（追加OR更新） 
+ 結果：結果をアプリへ返却しNotify表示（登録数やエラー有無レベル）
→今回はシンプルなCSV（文中改行はCR、ダブルクォーテ囲みはなし、文中のカンマもなし）という前提で取り込みサンプルを作ってみます。

# ① Power Apps上でのファイルアップロード用の実装概要
アプリへのファイルアップロードはSPOの添付ファイルコントロール部分のみ使用してダイアログを作ります。指定したファイルをPower Apps (V2)トリガーを使ってAutomateへ渡します。
Power Appsにはファイルを保存するコントロールは特にない（画像追加コントロールも利用可能だが、選択する際の既定が画像ファイルとなるためCSVやExcelインポートには向かない）ので、SPO（Dataverseも可）リストをデータソースにした際のフォームに自動追加される「添付ファイルコントロール」を活用します。

+ アプリにCSVインポートボタンを追加（クリック時にインポートダイアログを表示する）
※今回はメインで使うものではないので、別途メニューを追加しアイコンクリックでCSVインポートを表示とした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/4d488779-68a6-d317-d87a-10fd168d1adb.png)
+ インポートダイアログを作る（以下概要）
　・ フォーム（新規モード）を追加し、添付ファイルコントトールのみ残す
　・ プロパティは以下のような感じで設定（ファイル数１，最大容量：任意、NoAttachmentsTextなどをそれっぽく調整）
　・キャンセルとインポートのボタンを追加する。タイトルの見た目調整など任意に行う
　・表示制御は変数で（CSVインポートアイコン押したら表示、キャンセルやインポート完了時に非表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/8d360838-ce07-248b-dce7-86fc8f8eb2e2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/4fb9d8e3-4ab6-74e9-30df-4c9e6d37c8e6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/8713256a-2a88-0351-639d-3192965c4140.png)

という感じで、メッセージやダイアログタイトルなどを調整して、あたかも標準で用意されているファイル追加用のコントロールのように見せます。

次に、フローへファイルを渡し、結果を受け取り、アラートなどを出す処理をインポートボタンのOnSelectに入れます。

※自分がよく使うテクニックとして、ボタンのOnSelectに直接は書かずに、別で非表示のボタンを用意（ImportFunkボタンなどと名付け）し、それを「Select(ImportFunk);」として呼び出す実装をよく使います。こうすると上部にImportFunkボタンを置いておけばコード展開がしやすい＆他でも使う処理の場合は共通化できる。というメリットがあります
※保続画像
こっちには書かずに処理を記述したボタンを別で用意してSelectする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/d63c98f4-32da-9a5c-7365-484dbe7e4565.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/86071ffe-ec5a-9662-9267-73c24fa4f34f.png)


+ インポートボタンクリック時の実装例
ポイントを以下に記載します。

　・Withを使って何度も同じことは書かないようにしてます
　・拡張子でCSVか判定してCSV以外はエラーNotifyで終了
　・ローディングを表示　→終わったら非表示
　・インポートフローを呼び出し（ここはフロー未完成の場合はエラーになるので本来はフローを用意しておく）　
　└ファイルをフローへ渡す（Withで指定した添付ファイルコントロールのFirstの名前とValue）
　　※PowerAppsV2でのファイル渡しの書き方はググると出てきます
　└フローの結果を変数に入れる（フローからの戻り値はresult:True/False、msg:メッセージとしている）
　・IsBlankOrErrorまたはresultがFalseの場合は失敗Notify出す（かつメッセージもあれば出す）
　・成功の場合は完了Notifyとメッセージ（件数など）を出す
　・ダイアログを非表示、インポート用フォームをクリア、データソースをRefleshして最新化する
　※アプリでPatchすれば即時反映されるが、フロー（や他からの更新）だと即時反映はされないので、データソースのリフレッシュが必要
```
With({csv:First(DataCardValue.Attachments)}
    //ファイルチェック
    ,If(!EndsWith(csv.Name,".csv") && !EndsWith(csv.Name,".CSV")
        ,Notify("CSVファイルを選択してください。",NotificationType.Error);
        ,
        //ファイルインポートフロー実行
        UpdateContext({isLoading:true});
        UpdateContext({flowresult:
            人材スキルCSVインポート.Run(      
             {  name: csv.Name,
                contentBytes: csv.Value                
            }
        )
        });
        UpdateContext({isLoading:false});
        //結果判定
        If(IsBlankOrError(flowresult) || !flowresult.result,
        Notify("インポートに失敗しました。　" & If(!IsBlankOrError(flowresult), flowresult.msg),NotificationType.Error);
        ,              
        Notify("インポートが完了しました。　" & flowresult.msg ,NotificationType.Success);
        UpdateContext({ImportShow:false});
        ResetForm(FormImport); 
        );
        Select(RefleshIcon);        
    )
);
```
アプリ側はざっくりと上記のような感じです。

# ② PowerAutomateの実装概要
フロー側の実装です。ひとまず全容です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/035de2f4-2116-708b-3496-39b4cd29fd37.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/e8f80885-fe47-d44f-1d05-93081bc163fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/ccda9ecf-5447-5d90-bc37-e97bbdad427b.png)

上記のとおり、CSVの解析の部分ではループを使っていないので、APIコール数も少なく、選択やフィルターアクションのみなので処理も高速です（数千件データでも数秒で終わります）
順を追って解説していきます。

#### CSVファイルのサンプルはこんな感じ
複数選択肢があるためCSVファイルではその部分はカンマでなく ; 区切りにしてます。
文中の改行はCR（\n）でレコードの改行は（CRLF）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/eadc0a88-0cc4-6c68-21c2-0caeb6b0c3b5.png)

+ PowerAppsV2トリガー／変数定義
トリガーで渡されたファイルのコンテンツを変数：文字列に入れて文字列にする。
併せて後で使うカウント用の変数を用意
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/e771e4b8-8d84-ee2f-6f94-962898f02154.png)
+ 作成アクションで改行（CRLF）でスプリット→行単位に分ける
　その際にUTF-8BOM付の場合化けるので、その部分をデコードしたものを使います。
```
@{split(replace(variables('CSV文字列'),decodeUriComponent('%EF%BB%BF'),''),decodeUriComponent('%0D%0A'))}
```
+ CSVのヘッダ行のみを作成アクションで保持（1行目）
```
@{outputs('作成_CSV_改行でスプリット（CRLF）')[0]}
```
+ CSVのヘッダ行のデータ行を保持（２行目～）
```
@{skip(outputs('作成_CSV_改行でスプリット（CRLF）'),1)}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/54e52ba2-3158-692d-e638-62fdefb1293d.png)
+ アレイのフィルター処理で空白データを除く。※データ件数確認用にLengthを取る（任意）
```
差出人：　@outputs('作成_CSV_データ行')
@{item()}　：次に等しくない　：ブランク
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/18e01093-0a68-57f2-f258-130e711bb81a.png)
+ 選択アクションでCSVをJSONに変換する 
マップの部分はコード入力に変換して以下のように実装（件数はファイルに応じて調整）する。インデックスを変えて複製すればいいのでメモ帳なので作成して貼り付ければ楽
→ヘッダをカンマで分割したものとデータ行をカンマで分割したものの同じ列の値を順番にはめてます。
→ここでCSVのヘッダとデータの列数が足りない場合はエラーとなります（想定外のフォーマットなのでエラーでOK）
```
本文：　@{body('アレイのフィルター処理　CSV_データ行_空白除去')}　
マップ：　
{
    "@{split(outputs('作成_CSV_ヘッダ行'),',')[0]}": "@{split(item(),',')[0]}",
    "@{split(outputs('作成_CSV_ヘッダ行'),',')[1]}": "@{split(item(),',')[1]}",
    "@{split(outputs('作成_CSV_ヘッダ行'),',')[2]}": "@{split(item(),',')[2]}",
    "@{split(outputs('作成_CSV_ヘッダ行'),',')[3]}": "@{split(item(),',')[3]}",
    "@{split(outputs('作成_CSV_ヘッダ行'),',')[4]}": "@{split(item(),',')[4]}"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/f6496099-3519-dc46-7d26-f67fd760530a.png)
+ JSONの解析
 登録時に使いやすいようにJSON解析アクションを入れる。→いったん上記までをテスト実行して選択アクションの結果をサンプルから生成すればOK（Requied部分の記載は空の場合にエラーにされるので削除する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/2dd4eb7c-4fba-243b-e170-0e1756838933.png)

+ この段階でJSON解析結果は以下のようになる。
※複数選択肢があるため、CSVファイルではその部分はカンマでなく、;区切りにしてます。
※文中の改行はCR（\n）としているので（CRLF）でのスプリットでは分割されずそのまま入ります。
```
[
  {
    "氏名": "〇山 〇太郎",
    "メールアドレス": "mmmmxxx@nsurf.test.com",
    "区分": "社員",
    "所属/部署": "ICT営業部",
    "スキル（言語）": "Java",
    "スキル（環境）": "Windows Server;SQL Server;unix",
    "保有資格": "基本情報処理;応用情報処理",
    "スキル（工程）": "テスト;製造;詳細設計;基本設計",
    "スキル（ポスト）": "テスター;PG;SE",
    "勤務先": "日本橋",
    "プロフィール備考": "コミュニケーションスキル：メンタリスト資格保有",
    "コメント": "現在空き状態\n○○入場調整中",
    "稼働": "いいえ",
    "開始時期": "",
    "最寄り駅": "",
    "スキルシート": ""
  },
  {
    "氏名": "△田△郎",
    "メールアドレス": "sankakuxxx@nsurf.co.jp",
    "区分": "社員",
    "所属/部署": "ICTソリューション部",
    "スキル（言語）": "C#;asp.net;asp.net MVC;HTML;CSS;JavaScript;Power Apps;Power Automate",
    "スキル（環境）": "Windows Server;SQL Server;Azure;Microsoft365",
    "保有資格": "基本情報処理",
    "スキル（工程）": "製造;詳細設計;基本設計;要件定義;顧客折衝;運用保守",
    "スキル（ポスト）": "PG;SE;PL",
    "勤務先": "〇〇ソリューションズ株式会社",
    "プロフィール備考": "Microsoft365、SharePoint、Power Apps、Power Automate、他",
    "コメント": "SharePoint案件、PowerPlatform案件メイン",
    "稼働": "はい",
    "開始時期": "2013/10/01",
    "最寄り駅": "平和島",
    "スキルシート": "https://nsurf.sharepoint.com/xxxxxxxxxxx/XXXXXXXXXXNumOVaDg?e=dL6MOA"
  },
・・・・省略・・・・
]
```
+ ここまででエラーの場合にファイルチェックエラーとしてアプリに返す
→PowerAppsへ応答のアクションは実行条件として「失敗した、タイムアウト」にする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/07d15f80-6cb4-e6cc-812d-142dcf3fb9ec.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/7fd0e74c-c0a2-82cd-39cc-ce09faa8b394.png)
+ 終了アクションがスキップされた（正常に通ってきた）を実行条件にして、登録先のSPOリストからデータを取得する
（今回は追加と更新の両方やるので比較用に取得している（データ量を抑えるためビューによる制限などしたほうがよい）

+ JSONの解析を本文に入れてApply to eachアクション（CSVのデータ行ループ）
★Apply to eachは並列処理（コンカレンシーを20など）にして平行で処理させます。通常よりかなり高速化できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/1bdfbde8-1253-88b4-8aaa-8a5cbfe53c3a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/344ad7e5-f877-a33d-df0c-ef6154813aac.png)

+ SPOの列が複数選択肢列があるので入力用に変換アクションを入れている（なければ不要）
```
開始：@{split(items('Apply_to_each')?['スキル（言語）'],';')}
マップ：
{
    "Value": "@{item()}",
    "": ""
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/58f4e05e-0ea4-6b0d-33ca-9e7821a1a03a.png)

+ 存在チェックする（新規OR更新）
・メールアドレスをキーに存在有無を確認　
　差出人：SPOリストの複数項目の取得のValue、メールアドレスが今のApplytoeachのメールアドレスのものをフィルターアクションで取得
取得した結果が0件かそもそもメールアドレスが空の場合は新規、それ以外は更新
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/7e308735-ba56-ce10-5f07-a5d60700ba62.png)

+ 作成および更新
普通の項目はJSONの解析でとれる各項目をいれればOK
他はカスタム値として以下のような感じで入力
※テキスト以外への変換時、CSVだとデータなしは’’なのでIf文で空はNULL、値あればデータ変換するなどで実装する
更新の場合のIDは前段のアレイのフィルター処理_存在チェックの取れた結果をFirstで囲んでそのIDを指定する

```
はい／いいえ：　@{if(equals(items('Apply_to_each')?['稼働'],'はい'),true,false)}
選択肢：　@{items('Apply_to_each')?['区分']}
複数選択肢：前途した複数選択肢用に変換した作成アクションの出力
日付：@{if(equals(items('Apply_to_each')?['開始時期'],''),null,parseDateTime(items('Apply_to_each')?['開始時期'],'ja-jp'))}
更新アクションのID:@{first(body('アレイのフィルター処理_存在チェック'))?['ID']}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/4fba6dbf-c07c-50b1-8fff-f399a2b5d4b0.png)
+ 新規カウント、更新カウントをそれぞれ増やす
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/8ca56001-1467-dc7b-c7d5-433042a22f29.png)

+ 最後にPowerAppsへ応答で返却（resultとメッセージ）
Applytoeachでエラー、タイムアウトがある場合はエラーを返すよう実行条件を調整する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/d7ef9e1a-2042-26d6-43d8-9293f94aa242.png)

正常時とエラー時のメッセージ調整
```　
→新規、更新、元々のデータ行数-新規-更新の件数を失敗としてメッセージを返却する
新規追加：@{variables('newCount')}件　更新：@{variables('updCount')}件　失敗：@{sub(sub(outputs('作成_取り込みデータ件数：確認用'),variables('newCount')),variables('updCount'))}件
```

+ エラーハンドル関連
今回は結果の概要がわかるレベルの実装です。さらにしっかりやろうとすると、Applytoeach内でデータの整合性などチェックしたり、細かくスコープで囲ってResultを取ってエラーハンドルしたりとできますが大変です。。
上記実装だととりあえず成功した追加と更新、失敗の件数は取れるという感じです。これでも何もしないよりは面倒ですが。

# ③ 実装後の画面ショット
+ CSVファイルのインポート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/276a0d8c-955f-246b-a145-53783fcbe7f8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/25c0a3f0-50ef-6de8-b118-ce83a71ca23d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/f6307a14-0d25-c44e-0931-a813424e12f9.png)
→〇山 〇太郎などが入っている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/58d8fb34-e98c-bcfc-592f-3297192d3b07.png)
SPOリスト上での確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249807/743f64ca-fc5f-d4c1-180a-7b60e410bbf6.png)

# おわりに
今回はPowerAppsからAutomateへCSVを渡して、Automateで解析・登録するサンプルをご紹介しました。
自社アプリ用の実装なのでそのままの活用は難しいと思いますが、ポイント的に参考になる部分などあれば、上記サンプルをベースにお試しください。（サンプルなので不備もあるかと思いますのでご容赦ください）

→近日中（12月カレンダー中を想定）に、第３弾、CSV→PowerAppsのJSONParse関数での実装の記事も投稿しようと思います。

**★続きの12/11の記事を書きました。**
[【ワンランク上のPower Apps】 CSVインポート その③ Automateで解析・Appsで登録編 ＆プログレス表示](https://qiita.com/youseibubu/items/b04358342098648326eb)