Apex

VisualforceでCSVを読み込みレコードを登録する手順

アップロード画面とApexコントローラで、CSVの行数・ヘッダー・文字コードを確認してから取引先責任者を登録する処理を説明します。

2021.06.04

データローダやインポートウィザードで足りるとき

決まった項目を丸ごと取り込むだけなら、データローダかインポートウィザードで足ります。どちらもデータローダの実行環境かインポート権限が要ります。

それでも画面を作り込むのは、データローダを入れられない現場の担当者に使わせたい、取り込む項目を姓・名・電話番号・住所の4つに固定したい、1回のアップロードを少量に抑える安全弁がほしい、といった事情があるときです。

取り込むCSVファイルを用意する

1行目をヘッダー行とし、次の4項目をこの順で並べます。

列項目
1列目姓
2列目名
3列目電話番号
4列目住所

サンプルデータです。

姓名電話番号住所
山田太郎123-4567-8901東京都
鈴木太郎234-5678-9012神奈川県
エクスプローラーのフォルダ内に、Excelアイコンの付いたCSVファイルが1つ置かれている
取り込むCSVファイルを1つ用意します。拡張子がcsvであることをここで確かめてください。
CSVを開いた表。1行目が姓・名・電話番号・住所の見出しで、2行目以降に山田と鈴木のデータが並ぶ
1行目の見出しの並びを見てください。この4つがこの順でないとヘッダー照合に失敗します。
このページはアップロードされたファイルの文字コードをShift_JISとして判定します。Salesforceは文字コードに特別な要件が無ければUTF-8を推奨していますが、Windows版Excelで名前を付けて保存したCSVは今もShift_JIS(実体はWindows-31J)になることが多く、その前提でこの画面は作られています。Spring '25以降、Shift_JISを暗黙にWindows-31Jとして読み替える処理が段階的に廃止されるため、機種依存文字を含むファイルは文字化けする場合があります。

アップロード画面のVisualforceページ

CSVUpload.Page

<apex:page controller="CSVUploadController" apiVersion="67.0" showHeader="false" sidebar="false" id="pageId">
<head>
    <title>CSVファイルアップロード画面</title>
</head>
<apex:includeScript value="{!$Resource.jQuery}"/>
<apex:includeScript value="{!$Resource.CSVEncoding}"/>
<style>
  .csvDiv { border:5px solid #DDDDDD; border-radius:5px; margin:30px 0 20px 35px; width:90%; height:35px; }
  .csvFile { margin-left:80px; margin-top:5px; }
  .upload { margin-left:90px; width:200px; height:26px; }
</style>
<script>
  function doCsvFileCheck(){
    if(window.File && window.FileReader && window.FileList && window.Blob) {
      var selectedFile = document.getElementById("Inputtedfile").files[0];
      //ファイル未選択
      if(selectedFile == null || typeof selectedFile === "undefined"){
          $('.errorMessage').text('アップロードするファイルを選択してください。');
          $('.errorMessage2').css('display','block');
          return null;
      }
      //拡張子がcsv以外
      if(!selectedFile.name.match('.csv$')){
          $('.errorMessage').text('アップロード可能なファイル形式はCSVのみです。');
          $('.errorMessage2').css('display','block');
          return null;
      }
      readFIle(selectedFile);
      return null;
    }
    return null;
  }
  function readFIle(file){
      var reader = new FileReader();
      reader.onload = (function(rdFile) {
          return function(ev) {
              var csvBody = encodingString(ev.target.result);
              if(typeof csvBody === "string" && csvBody !== ""){
                  csvBody = csvBody.replace(/\r\n?/g, '<br/>').replace(/\n/g, '<br/>');
                  csvBody = csvBody.replace(//g, ''); //BOM除去
                  $('.csvBody').val(csvBody);
                  uploadCsvFile();
              } else {
                  //文字コードがSJISと判定できなかった
                  $('.errorMessage').text('アップロード可能な文字コードはSJISのみです。');
                  $('.errorMessage2').css('display','block');
              }
          };
      })(file);
      reader.readAsArrayBuffer(file);
  }
  function encodingString(str){
      var array = new Uint8Array(str);
      if(Encoding.detect(array) != 'SJIS'){
          return null;
      }
      var unicodeArray = Encoding.convert(array, 'UNICODE');
      return Encoding.codeToString(unicodeArray);
  }
</script>
<apex:form id="formId">
  <h1>CSVファイルアップロード</h1>
  <div class="errorMessage2" role="alert" style="display:none;">
      <span style="color:#cc0000"><h4>Error:</h4></span>
      <span class="errorMessage"></span>
  </div>
  <apex:pageMessages id="errorMessage" escape="false"/>
  <div class="csvDiv">
    <input type="file" id="Inputtedfile" name="files[]" accept=".csv" class="csvFile" />
    <input type="button" class="upload" value="CSVファイルアップロード" onclick="doCsvFileCheck();"/>
  </div>
  <apex:actionFunction name="uploadCsvFile" action="{!uploadFile}" reRender="formId" />
  <apex:inputText value="{!csvBody}" styleClass="csvBody" style="visibility:hidden;" />
</apex:form>
</apex:page>

ファイルを選んでボタンを押すと、JavaScriptでCSVを読み込み文字コードを判定してからApexへ渡します。jQueryは外部CDNではなく静的リソースから読み込みます。

以前の書き方ではjQueryをhttps://ajax.googleapis.com/から直接読み込んでいました。SalesforceはVisualforceページにもCSPを適用しており、既定では自組織以外から読み込むスクリプトをブロックします。そのためjQuery本体を静的リソースとしてアップロードし、$Resource.jQueryとして読み込んでいます。文字コード判定用のCSVEncodingも静的リソースから読み込みます。

アップロードを処理するApexコントローラ

CSVUploadController.cls

public with sharing class CSVUploadController {
    public static final String CSVSPLITTER = ',';
    public static final String CSVESCAPE = '"';
    public static final String LINESPLITTER = '<br/>';
    public static final Integer HEADER_LASTNAME_INT = 0;
    public static final Integer HEADER_FIRSTNAME_INT = 1;
    public static final Integer HEADER_TELEPHONE_INT = 2;
    public static final Integer HEADER_ADDRESS_INT = 3;
    public static final Map<Integer,String> HEADER_CONTACT = new Map<Integer,String>{
        HEADER_LASTNAME_INT  => '姓',
        HEADER_FIRSTNAME_INT => '名',
        HEADER_TELEPHONE_INT => '電話番号',
        HEADER_ADDRESS_INT   => '住所'
    };
    public String csvBody { get; set; }
    public String errorMessage { get; set; }

    public PageReference uploadFile(){
        this.errorMessage = '';
        List<String> csvFileRows = csvBody.split(LINESPLITTER);
        //ヘッダー含め2行未満はエラー
        if(csvFileRows.isEmpty() || csvFileRows.size() < 2){
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
                'CSVファイルの行数はヘッダー含め最低2行以上必要です。'));
            return null;
        }
        //この記事ではヘッダー含め6行までに制限
        if(csvFileRows.size() > 6){
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
                'アップロード可能な行数はヘッダー含む6行までです。'));
            return null;
        }
        List<String> csvHeader = new List<String>(csvFileRows.get(0).split(','));
        if(csvHeader.size() != HEADER_CONTACT.size()){
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
                'CSVファイルのヘッダー項目が一致していません。CSVファイルを作成し直してください。'));
            return null;
        }
        for(Integer keyNo : HEADER_CONTACT.keySet()){
            if(!HEADER_CONTACT.get(keyNo).equals(csvHeader.get(keyNo))){
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
                    'CSVファイルのヘッダー項目が異なっています。CSVファイルを作成し直してください。'));
                return null;
            }
        }
        Map<Integer,Contact> contactMap = createContactForCSV(csvFileRows);
        if(contactMap == null || String.isNotBlank(errorMessage)){
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
                String.isNotBlank(errorMessage) ? errorMessage : 'CSVファイルに不正な値が含まれています。CSVファイルをご確認ください。'));
            return null;
        }
        Savepoint sp = Database.setSavepoint();
        try {
            insert contactMap.values();
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM, 'CSVファイルアップロードが完了しました。'));
        } catch (Exception e) {
            Database.rollback(sp);
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'システムエラーが発生しました。管理者にご連絡ください。'));
        }
        return null;
    }

    private Map<Integer,Contact> createContactForCSV(List<String> csvBody){
        Map<Integer,Contact> returnContactMap = new Map<Integer,Contact>();
        List<String> errorMessageList = new List<String>();
        for(Integer i = 1; i < csvBody.size(); i++){
            List<String> csvValues = CSVSplit(csvBody.get(i));
            if(csvValues.size() != HEADER_CONTACT.size()){
                errorMessage = 'CSVファイルに不正な値が含まれています。CSVファイルをご確認ください。';
                return null;
            }
            Contact createContact = new Contact();
            if(String.isNotBlank(csvValues.get(HEADER_LASTNAME_INT))){
                createContact.LastName = csvValues.get(HEADER_LASTNAME_INT);
            } else {
                errorMessageList.add(i + '行目の姓の値が不備です。');
            }
            createContact.FirstName = csvValues.get(HEADER_FIRSTNAME_INT);
            createContact.Phone = csvValues.get(HEADER_TELEPHONE_INT);
            createContact.MailingState = csvValues.get(HEADER_ADDRESS_INT);
            returnContactMap.put(i, createContact);
        }
        errorMessage = String.join(errorMessageList, '<br/>');
        return returnContactMap;
    }

    private List<String> CSVSplit(String csvBody){
        //ダブルクォートで囲まれた値の中のカンマは区切り文字として扱わない
        List<String> csvValues = new List<String>();
        List<String> tmpCsvValues = csvBody.split(CSVSPLITTER, -1);
        Boolean escapeFlg = false;
        String buffer = '';
        for(String str : tmpCsvValues){
            if(!escapeFlg){
                if(str.startsWith(CSVESCAPE)){
                    if(str.endsWith(CSVESCAPE)){
                        csvValues.add(str);
                    } else {
                        buffer = str.substring(1);
                        escapeFlg = true;
                    }
                } else {
                    csvValues.add(str);
                }
            } else if(str.endsWith(CSVESCAPE)){
                escapeFlg = false;
                buffer += CSVSPLITTER + str.substring(0, str.length() - 1);
                csvValues.add(buffer);
                buffer = '';
            } else {
                buffer += CSVSPLITTER + str;
            }
        }
        if(String.isNotBlank(buffer)){
            csvValues.add(buffer);
        }
        return csvValues;
    }
}

行数・ヘッダー・文字コードを確認してから取引先責任者を作成します。DMLはループの外で1回だけ実行し、失敗時はSavepointへロールバックします。

公開までの手順

  1. jQueryを静的リソースに登録する

    「設定」の「静的リソース」から、公式サイトで取得したjQueryのファイルを名前「jQuery」でアップロードします。1リソースの上限は5MBです。

  2. Visualforceページとコントローラを配置する

    CSVUpload.Pageと CSVUploadController.clsを作成し、カスタムタブなどからページを開けるようにします。

  3. アクセス権限を絞る

    アップロード画面を使わせたいプロファイルまたは権限セットだけに、タブとページへのアクセスを許可します。

動作確認したときに見えるもの

CSVファイルアップロード画面。ファイル選択欄は「選択されていません」でボタンが並ぶ
開いた直後の画面です。左でファイルを選んでから右のボタンを押す流れになります。
アップロード画面でCSVファイルを選んだ状態。ファイル名が赤枠で示されている
ファイルを選ぶと名前が表示されます。赤枠の位置にファイル名が出ているか確認します。

「ファイルを選択」でCSVを選び、「CSVファイルアップロード」ボタンを押します。行数やヘッダーが条件どおりでない場合や、文字コードがShift_JISと判定できない場合は、画面上部に赤字のエラーメッセージが表示されます。すべて通ると取引先責任者が作成され、「CSVファイルアップロードが完了しました。」という確認メッセージが表示されます。

画面上部に「成功: CSVファイルアップロードが完了しました。」という緑チェック付きのメッセージが出ている
すべての検証を通ると、この確認メッセージが画面上部に出ます。ここが成否の目印です。

ここで間違えやすい

間違い何が起きるか
ヘッダー含め7行以上のCSVを選ぶ「アップロード可能な行数は…」のエラーになり、登録されません
列の並びや見出し文字列を変えるヘッダー照合に失敗し、登録されません
UTF-8で保存したCSVを選ぶ文字コード判定でShift_JISと認識されず、エラーになります
jQueryを外部CDNから読み込むVisualforceのCSPにブロックされ、画面のボタンが動きません

大量件数を取り込むとき

この画面はデモとして、ヘッダーを含め6行(データ5件)までに制限しています。DML自体はループの外で1回だけ実行しているため件数を増やしても仕組みは崩れませんが、Apex同期処理のヒープサイズ上限は6MBです。数百件を超えるような取り込みには、この画面を広げるのではなく、データローダかBulk APIを使ってください。

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、CSPと静的リソース、Shift_JISの扱い、ヒープサイズの上限を確認しています
  • コードはAPIバージョン67.0で書いています

まとめ

  • 決まった項目を丸ごと取り込むだけなら、データローダかインポートウィザードで足ります
  • この画面は行数・ヘッダー・文字コードを確認してから、取引先責任者を1回のDMLでまとめて登録します
  • jQueryは外部CDNからではなく、静的リソースにアップロードして読み込みます。VisualforceにもCSPが適用され、既定では外部スクリプトがブロックされます
  • アップロードを受け付ける文字コードはShift_JISです。UTF-8で保存したファイルはエラーになります
  • 数百件を超える取り込みには向きません。データローダかBulk APIへ切り替えてください

参考:当時の画面

記事を最初に書いた当時の画面です。いまの手順と違うところは、各画像の説明に書いています。

取引先責任者の一覧に、山田太郎と鈴木太郎の2件が電話と都道府県つきで登録されている
CSVの2行が2件のレコードになったことを確認します。

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

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