見出し画像

Lightning Web ComponentsでCSVファイルを取り込む

今は、Web APIが流行っていますが、そうは言ってもCSVもまだまだあります(CSVに出会うことの方が多いですね)しかも、Shift-JISのCSVファイルばかりです。

Lighting Web ComponentsでCSVファイルを扱う方法をご紹介します。

前提条件

・CSVファイルはShift_JISです。
・CSVファイルには、内閣府が配付している祝日CSVを使います。
・CSVファイルの読み込みはLightning Web Componentsで行い、Salesforceの休日(Holiday)に登録します。

CSVファイルのインポート方法

効率的にかつSalesforceのガバナ制限の制約を受けにくくするために以下のような方法で取り込みます。

・Lignting Web Components側でCSVファイルをJSONに変換し、Apexクラスに一括で渡す

・ApexクラスではJSONを解析し、Holidayオブジェクトを必要数作成し、一括でインサートする

Lightning Web Componentsの実装

Lightning Web Componentsでは、祝日CSVファイルを指定するダイアログ、祝日CSVファイルの取込を実装します。

祝日CSVファイルを指定するダイアログ

以下のような祝日CSVファイルを指定するダイアログを作成します。

画像1

<template>
   <template if:true={visible}>
       <div style="height: 640px;">
           <section role="daialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open">
               <div class="slds-modal__container">
                   <header class="slds-modal__header">
                       <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="Close" onclick={handleCancelClick}>
                           <lightning-icon icon-name="utility:close" size="medium">
                           </lightning-icon>
                           <span class="slds-assistive-text">Close</span>
                       </button>
                       <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">{labels.ImportHolidayTitle}</h2>
                   </header>
                   <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                       <div class="slds-m-around_medium">
                           <lightning-spinner if:true={isProgress} size="large"></lightning-spinner>
                           <lightning-input
                               type="file"
                               label={labels.ImportHolidayFilename}
                               accept="text/csv"
                               onchange={handleChangeFile}>
                           </lightning-input>
                       </div>
                   </div>
                   <footer class="slds-modal__footer">
                       <lightning-button label={labels.ImportHolidayCancel} variant="neutral" onclick={handleCancelClick}></lightning-button>&nbsp;&nbsp;&nbsp;&nbsp;
                       <lightning-button disabled={isNotChooseFile} label={labels.ImportHolidayImport} variant="brand" onclick={handleConfirmImportClick}></lightning-button>
                   </footer>
               </div>
           </section>
           <div class="slds-backdrop slds-backdrop_open"></div>
       </div>
   </template>
</template>

ポイント

CSVファイルを選択するファイルを開くダイアログを表示します。

表示する文字は、カスタム表示ラベルを使っています。

<lightning-input
type="file"
label={labels.ImportHolidayFilename}
accept="text/csv"
onchange={handleChangeFile}>
</lightning-input>

CSVファイルを取り込んでいる間にスピナーを表示します。

<lightning-spinner if:true={isProgress} size="large"></lightning-spinner>

祝日CSVファイルを指定するダイアログの実装

Lightning Web Componentsの全体です。

import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

// カスタム表示ラベル
import ImportHolidayCancel from '@salesforce/label/c.ImportHolidayCancel';
import ImportHolidayConfirm from '@salesforce/label/c.ImportHolidayConfirm';
import ImportHolidayError from '@salesforce/label/c.ImportHolidayError';
import ImportHolidayFilename from '@salesforce/label/c.ImportHolidayFilename';
import ImportHolidayImport from '@salesforce/label/c.ImportHolidayImport';
import ImportHolidaySuccess from '@salesforce/label/c.ImportHolidaySuccess';
import ImportHolidayTitle from '@salesforce/label/c.ImportHolidayTitle';
import ImportHolidayBadFile from '@salesforce/label/c.ImportHolidayBadFile';

import importHoliday from '@salesforce/apex/EventCalendar.importHoliday';

export default class ImportHoliday extends LightningElement {
   /**
    * 表示フラグ
    */
   visible = false;
   
   /**
    * ファイル非選択フラグ
    */
   isNotChooseFile = true;

   /**
    * 取り込みダイアログ表示
    */
   @api show () {
       this.visible = true;
   }

   /**
    * 読み込んだファイルデータ
    */
   data;

   /**
    * 処理中フラグ
    */
   isProgress = false;

   /**
    * 表示ラベル
    */
   labels = {
       ImportHolidayCancel,
       ImportHolidayConfirm,
       ImportHolidayError,
       ImportHolidayFilename,
       ImportHolidayImport,
       ImportHolidaySuccess,
       ImportHolidayTitle,
       ImportHolidayBadFile,
   };

   /**
    * ファイルが選択された時の処理
    */
   handleChangeFile (event) {
       // 指定されたファイルを読み込む
       const fileReader = new FileReader();

       // ファイルの読み込みが完了したら
       fileReader.onloadend = () => {
           // 読み込んだデータを設定する
           this.data = fileReader.result;
           // ファイル非選択フラグを設定する
           this.isNotChooseFile = false;
       }

       // ファイルを読み込む
       fileReader.readAsText(event.detail.files[0], 'Shift_JIS');
   }

   /**
    * 取り込み確認ダイアログにOKした場合の処理
    */
   async handleConfirmImportClick () {
       // 読み込んだデータを改行で分割する
       const rows = this.data.split(/\r\n|\n/);

       // 先頭行が正しいかをチェックする
       if (rows[0] !== '国民の祝日・休日月日,国民の祝日・休日名称') {
           // エラーメッセージを表示する
           const t = new ShowToastEvent({
               title: 'Error',
               message: this.labels.ImportHolidayBadFile,
               variant: 'error',
           });
           this.dispatchEvent(t);
           return;
       }

       // 処理中フラグを設定する
       this.isProgress = true;

       // 当年の開始日を算出する
       const startDate = new Date();
       startDate.setMonth(0);
       startDate.setDate(1);

       // 取り込んだ祝日情報をJSONに変換する
       const holidays = [];
       for(let i=1 ; i<rows.length ; i++){
           const cols = rows[i].split(',');

           // 2つに分割されない行は含めない
           if (cols.length !== 2) {
               continue;
           }

           // 当年開始日以前のデータは含めない
           const day = new Date(cols[0].replace(/\//g, '-'));
           if (day < startDate) {
               continue;
           }

           holidays.push({
               date: cols[0].replace(/\//g, '-'),
               holiday: cols[1],
           });
       }

       // 祝日情報を登録する
       try {
           await importHoliday({holiday: JSON.stringify(holidays)});
           // 処理中フラグを初期化する
           this.isProgress = false;
           // 取り込み終了のメッセージを表示する
           const t = new ShowToastEvent({
               title: 'Info',
               message: this.labels.ImportHolidaySuccess,
               variant: 'success',
           });
           this.dispatchEvent(t);
           this.dispatchEvent(new CustomEvent('imported'));
           // ダイアログを閉じる
           this.closeDialog();
       }
       catch(e) {
           // 処理中フラグを初期化する
           this.isProgress = false;
           // エラーメッセージを表示する
           const t = new ShowToastEvent({
               title: 'Error',
               message: e.body.message,
               variant: 'error',
           });
           this.dispatchEvent(t);
       }
   }

   /**
    * ダイアログを閉じる
    */
   closeDialog () {
       // ダイアログを閉じる
       this.isNotChooseFile = true;
       this.data = undefined;
       this.visible = false;
   }
}

ポイント

祝日CSVファイルが選択されたら、handleChangeFileが呼び出されます。

handleChangeFileではCSVファイルをすべて読み込みます。

    /**
    * ファイルが選択された時の処理
    */
   handleChangeFile (event) {
       // 指定されたファイルを読み込む
       const fileReader = new FileReader();

       // ファイルの読み込みが完了したら
       fileReader.onloadend = () => {
           // 読み込んだデータを設定する
           this.data = fileReader.result;
           // ファイル非選択フラグを設定する
           this.isNotChooseFile = false;
       }

       // ファイルを読み込む
       fileReader.readAsText(event.detail.files[0], 'Shift_JIS');
   }

取込ボタンをクリックするとhandleConfirmImportClickが呼び出されます。

読み込んだCSVファイルを行分割、列分割し、{date: 日付, holiday: 祝日名}の配列をJSONで作成します。

        // 読み込んだデータを改行で分割する
       const rows = this.data.split(/\r\n|\n/);

       // 先頭行が正しいかをチェックする
       if (rows[0] !== '国民の祝日・休日月日,国民の祝日・休日名称') {
           // エラーメッセージを表示する
           const t = new ShowToastEvent({
               title: 'Error',
               message: this.labels.ImportHolidayBadFile,
               variant: 'error',
           });
           this.dispatchEvent(t);
           return;
       }

       // 処理中フラグを設定する
       this.isProgress = true;

       // 当年の開始日を算出する
       const startDate = new Date();
       startDate.setMonth(0);
       startDate.setDate(1);

       // 取り込んだ祝日情報をJSONに変換する
       const holidays = [];
       for(let i=1 ; i<rows.length ; i++){
           const cols = rows[i].split(',');

           // 2つに分割されない行は含めない
           if (cols.length !== 2) {
               continue;
           }

           // 当年開始日以前のデータは含めない
           const day = new Date(cols[0].replace(/\//g, '-'));
           if (day < startDate) {
               continue;
           }

           holidays.push({
               date: cols[0].replace(/\//g, '-'),
               holiday: cols[1],
           });
       }

作成したJSONファイルを文字列に変換(JSON.stringify)して、Apexクラスを呼び出します。

            await importHoliday({holiday: JSON.stringify(holidays)});
           // 処理中フラグを初期化する
           this.isProgress = false;
           // 取り込み終了のメッセージを表示する
           const t = new ShowToastEvent({
               title: 'Info',
               message: this.labels.ImportHolidaySuccess,
               variant: 'success',
           });
           this.dispatchEvent(t);
           this.dispatchEvent(new CustomEvent('imported'));
           // ダイアログを閉じる
           this.closeDialog();

休日(Holiday)に登録するApexクラスの実装

次に休日(Holiday)に登録するApexクラスを実装します。

    /**
    * 祝日情報を登録する
    * 祝日情報は以下のJSON形式で渡される
    * [
    *   {
    *     date: 日付
    *     holiday: 祝日名
    *    }
    * ]
    * 登録する際、Descriptionに<!----------PlusOne Holiday---------->を設定する
    * 登録範囲内(当年以降)のDescriptionが同じものをすべて削除して渡された祝日を登録する
    */
   private static final String PLUSONE_HOLIDAY_DESCRIPTION = '<!----------PlusOne Holiday---------->'; 
   @AuraEnabled
   public static void importHoliday(String holiday){
       // 当年開始日を取得する
       Date today = Date.today();
       Date startDate = Date.newInstance(today.year(), 1, 1);

       // 当年開始日以降かつ本アプリで登録した祝日を削除する
       List<Holiday> lstDeleteHoliday = [SELECT Id FROM Holiday WHERE IsAllDay=true AND ActivityDate>=:startDate AND Description=:PLUSONE_HOLIDAY_DESCRIPTION];
       try {
           delete lstDeleteHoliday;
       }
       catch(DmlException e) {
           throw e;
       }

       // 渡された祝日情報を登録する
       List<Holiday> lstImportHoliday = new List<Holiday>();
       List<Object> lstHoliday = (List<Object>)JSON.deserializeUntyped(holiday);
       for(Object o : lstHoliday) {
           Map<String, Object> m = (Map<String, Object>)o;
           Holiday h = new Holiday();
           List<String> lstActivityDate = ((String)m.get('date')).split('-');
           h.ActivityDate = Date.newInstance(Integer.valueOf(lstActivityDate[0]), Integer.valueOf(lstActivityDate[1]), Integer.valueOf(lstActivityDate[2]));
           h.Name = (String)m.get('holiday');
           h.IsAllDay = true;
           h.Description = PLUSONE_HOLIDAY_DESCRIPTION;
           lstImportHoliday.add(h);
       }
       try {
           insert lstImportHoliday;
       }
       catch(DmlException e) {
           throw e;
       }
   }

ポイント

休日はUIからも登録できますので、それに影響を与えないように、取り込んだ休日の説明に「<!----------PlusOne Holiday---------->」を設定しています。

説明が「<!----------PlusOne Holiday---------->」になっているものを一旦削除します。

        // 当年開始日以降かつ本アプリで登録した祝日を削除する
       List<Holiday> lstDeleteHoliday = [SELECT Id FROM Holiday WHERE IsAllDay=true AND ActivityDate>=:startDate AND Description=:PLUSONE_HOLIDAY_DESCRIPTION];
       try {
           delete lstDeleteHoliday;
       }
       catch(DmlException e) {
           throw e;
       }

次にJSONで渡された祝日情報を解析して登録していきます。

{date: 日付, holiday: 祝日名}の配列ですので、以下のようになっています。

これをしっかりイメージしましょう。

[
    {"date": "2021-05-05", "holiday": "こどもの日" },
    {"date": "2021-05-04", "holiday": "みどりの日" }
]

JSONでは「{}」はオブジェクトになりますので、上記JSONを解析するには、List<Object>に変換します。

こうすると、{"date": "2021-05-05", "holiday": "こどもの日" }のListになります。

        List<Object> lstHoliday = (List<Object>)JSON.deserializeUntyped(holiday);

次に、{"date": "2021-05-05", "holiday": "こどもの日" }を1つずつ解析していきます。

date、holidayをラベルとして、202-05-05とこどもの日をオブジェクトとして解析していきます。

            Map<String, Object> m = (Map<String, Object>)o;

このようにすると、m.get('date')で日付が、m.get('holiday')で祝日の名前が取得できます。

これを元にHolidayオブジェクトを作成していきます。

        for(Object o : lstHoliday) {
           Map<String, Object> m = (Map<String, Object>)o;
           Holiday h = new Holiday();
           List<String> lstActivityDate = ((String)m.get('date')).split('-');
           h.ActivityDate = Date.newInstance(Integer.valueOf(lstActivityDate[0]), Integer.valueOf(lstActivityDate[1]), Integer.valueOf(lstActivityDate[2]));
           h.Name = (String)m.get('holiday');
           h.IsAllDay = true;
           h.Description = PLUSONE_HOLIDAY_DESCRIPTION;
           lstImportHoliday.add(h);
       }

最後に、Holidayオブジェクトにinsertします。

        try {
           insert lstImportHoliday;
       }
       catch(DmlException e) {
           throw e;
       }

このようにすると、一度の取込でSOQL2回、DML2回でCSVファイルを取り込むことができます。

巨大なCSVファイルを取り込むときは、処理を分割するなどの工夫が必要ですが、ちょっとしたCSVであればこれで実装できます(大抵のCSVはこれで対応できます)


この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

3
生涯現役なデベロッパーを目指しています。Typescript、Javascript、Salesforceのフルスタックデベロッパーとして日々コードを綴っています。コードを綴る中で、「これは!?」と思ったものを記事にしています。
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。