LWC

LWCでCSVを読み込みApexで一括登録する実装

CSVを画面で選ぶとブラウザ側で行ごとにJSONへ変換し、Apexへ1回だけ渡してDMLも1回で済ませます。文字コードと件数の落とし穴を説明します。

2021.06.04

CSVインポートの流れ

CSVを1行ずつApexへ送って1件ずつ登録すると、ループの中でDMLを発行する形になります。件数が増えた日に、DML発行回数の上限に当たります。

この実装は、CSVの中身をブラウザ側でJSONの配列に変換してから、Apexへは1回だけ渡します。Apex側もリストへまとめてからinsertを1回だけ実行します。読み込みはブラウザ、登録はApexという役割分担です。

用意するCSVの形

姓・名・電話番号の3列を持つCSVを想定します。1行目はヘッダーとして扱い、2行目以降をデータとして読み込みます。

姓名電話番号
YamadaTaro123-4567-8901
SuzukiJiro234-5678-9012

画面にファイル選択とアップロードボタンを置く

lightning-inputのtype="file"でCSVを選ばせます。acceptをMIMEタイプのtext/csvにして絞り込みます。

<template>
    <lightning-card
    title="LwcCSVファイルアップロード"
    icon-name="custom:custom15"
  >
    <div style="padding: 0 20px;">
      <div style="
        padding: 10px 0 0 0;
      ">
        <lightning-input
          type="file"
          name="csv"
          label="CSVファイルアップロード"
          onchange={onChangeLwcCSVUpload}
          accept="text/csv"
        ></lightning-input>
      </div>
      <p style="padding: 10px 0 0 0;">{LwcCSVUploadFileName}</p>
      <div style="
        padding: 20px 0 10px 0;
        margin: 0 0 0 -5px;
      ">
        <lightning-button
            variant="brand"
            label="CSVファイルアップロード"
            title="Primary action"
            onclick={onClickLwcCSVUpload}
            class="slds-m-left_x-small">
        </lightning-button>
      </div>
    </div>
  </lightning-card>
</template>

CSVを1件選び、選択後にファイル名を表示してからアップロードボタンで送信します。

選んだCSVを行ごとにJSONへ変換してApexへ渡す

FileReaderでCSVをテキストとして読み込み、改行で行に分け、カンマで列に分けてJSONの配列に組み立てます。

import { LightningElement, api } from 'lwc';
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
import uploadCSVFile from "@salesforce/apex/LwcCSVUploadController.uploadCSVFile";
export default class LwcCSVUpload extends LightningElement {
  @api file = null;
  @api LwcCSVUploadFileName;
  data;
  onChangeLwcCSVUpload(event) {
    this.file = event.detail.files;
    this.LwcCSVUploadFileName = this.file[0].name;
    const fileReader = new FileReader();
    fileReader.onloadend = () => {
      this.data = fileReader.result;
    }
    fileReader.readAsText(this.file[0]);
  }
  onClickLwcCSVUpload() {
    if (!this.file) {
      return
    }
    Promise.resolve().then(() => {
      return new Promise((resolve, reject) => {
        const rows = this.data.split(/\r\n|\n/);
        const outputJson = [];
        for(let i=1 ; i<rows.length ; i++) {
          const cols = rows[i].split(',');
          outputJson.push({
            LastName: cols[0],
            FirstName: cols[1],
            Phone: cols[2],
          });
        }
        uploadCSVFile({
          jsonData: JSON.stringify(outputJson)
        })
        .then(result => {
          this.data = result;
          resolve(this.data);
          this.dispatchEvent(
            new ShowToastEvent({
              title: 'ファイルアップロード成功',
              message: 'ファイルアップロード成功しました。',
              variant: 'success',
            }),
          );
        })
        .catch(error => {
          this.dispatchEvent(
            new ShowToastEvent({
              title: 'ファイルアップロード失敗',
              message: error,
              variant: 'error',
            }),
          );
        })
      });
    })
    .catch(error => {
      console.log(error)
    })
    .finally(() => {
      this.template.querySelectorAll('lightning-input').forEach(each => {
        each.value = "";
      });
      this.file = null;
    });
  }
}

CSVを1行ずつJSONへ変換し、配列にまとめてから一括でApexへ渡します。Apexの呼び出しは1回だけです。

rows[i].split(',')は、値の中にカンマが含まれていない前提の簡易な分割です。住所や備考のように値そのものにカンマが入りうる列を扱うなら、CSVパーサーを別途用意してください。

メタデータでAPIバージョンを指定する

当時の記事では51.0でしたが、2026年9月時点の最新であるSummer '26 = 67.0へ上げます。

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>67.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

コンポーネントをホームページに配置できるように公開設定を行います。apiVersionは51.0から67.0へ上げています。

Apexで一括登録する

受け取ったJSONをList<Contact>に組み立ててから、insertを1回だけ実行します。ループの中にDMLはありません。

public with sharing class LwcCSVUploadController {
    @AuraEnabled
    public static void uploadCSVFile(String jsonData) {
        List<Contact> uploadConList = new List<Contact>();
        List<Object> jsonDataList = (List<Object>)JSON.deserializeUntyped(jsonData);
        for(Object obj : jsonDataList) {
            Map<String, Object> mapStrObj = (Map<String, Object>)obj;
            Contact con = new Contact();
            con.LastName = (String)mapStrObj.get('LastName');
            con.FirstName = (String)mapStrObj.get('FirstName');
            con.Phone= (String)mapStrObj.get('Phone');
            uploadConList.add(con);
        }
        SavePoint sp = Database.setSavepoint();
        try {
            insert uploadConList;
        }
        catch(DmlException e) {
            Database.rollback(sp);
            throw e;
        }
    }
}

JSONをContactのリストへ組み立て、1回のinsertでまとめて登録します。失敗時はSavepointへロールバックしてから例外を投げ直します。

ホームページに配置して有効化する

  1. ホームページを編集モードで開く

    「ホーム」タブを開き、右上の歯車アイコンから「ページを編集」を選びます。

  2. コンポーネントを配置する

    作成したLWCをキャンバスへドラッグし、「保存」を押します。

  3. 有効化する

    「有効化」から、このページを表示するユーザーやアプリを選びます。

動作確認

「ホーム」タブから「CSVファイルアップロード」欄でCSVを選びます。ファイル名が表示されたら「CSVファイルアップロード」ボタンを押します。「ファイルアップロード成功」のトーストが表示されたら、取引先責任者の一覧を開き、CSVの行数と同じだけレコードが増えていることを確認します。

ここで間違えやすい

間違い何が起きるか
ExcelでCSVを保存してそのまま使うExcelの既定の保存形式はUTF-8ではないため、readAsTextで読み込むと日本語が文字化けします
値の中にカンマが含まれるsplit(',')は単純な分割なので、列がずれて登録されます
1回のCSVに数万件を詰め込むブラウザ側でJSON文字列を組み立ててから1回のApex呼び出しに渡すため、件数が多いとヒープサイズや呼び出しのペイロード上限に当たります。大量件数はデータローダやBulk APIを使う方が安全です
ヘッダー行も登録してしまうiを0から回すと、1行目の見出しまでレコードとして登録されます

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、Bulk APIを使うべき場面とCSVの文字コードの扱いを確認しています

まとめ

  • CSVはブラウザ側でJSONへ変換し、Apexへは1回だけ渡します。Apex側のinsertもループの外で1回だけです
  • ExcelのCSVはUTF-8で保存されないことが多く、そのままだと文字化けします
  • split(',')は値の中のカンマに対応していません。複雑なCSVには専用のパーサーが必要です
  • js-meta.xmlのapiVersionは、当時の51.0からSummer '26の67.0へ上げています
  • 件数が多いCSVは、この実装のようにブラウザで組み立てて1回で送る方式ではなく、データローダやBulk APIを検討します

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。