VisualforceでCSVを読み込みレコードを登録する手順
アップロード画面とApexコントローラで、CSVの行数・ヘッダー・文字コードを確認してから取引先責任者を登録する処理を説明します。
データローダやインポートウィザードで足りるとき
決まった項目を丸ごと取り込むだけなら、データローダかインポートウィザードで足ります。どちらもデータローダの実行環境かインポート権限が要ります。
それでも画面を作り込むのは、データローダを入れられない現場の担当者に使わせたい、取り込む項目を姓・名・電話番号・住所の4つに固定したい、1回のアップロードを少量に抑える安全弁がほしい、といった事情があるときです。
取り込むCSVファイルを用意する
1行目をヘッダー行とし、次の4項目をこの順で並べます。
| 列 | 項目 |
|---|---|
| 1列目 | 姓 |
| 2列目 | 名 |
| 3列目 | 電話番号 |
| 4列目 | 住所 |
サンプルデータです。
| 姓 | 名 | 電話番号 | 住所 |
|---|---|---|---|
| 山田 | 太郎 | 123-4567-8901 | 東京都 |
| 鈴木 | 太郎 | 234-5678-9012 | 神奈川県 |
アップロード画面の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へロールバックします。
公開までの手順
jQueryを静的リソースに登録する
「設定」の「静的リソース」から、公式サイトで取得したjQueryのファイルを名前「jQuery」でアップロードします。1リソースの上限は5MBです。
Visualforceページとコントローラを配置する
CSVUpload.Pageと CSVUploadController.clsを作成し、カスタムタブなどからページを開けるようにします。
アクセス権限を絞る
アップロード画面を使わせたいプロファイルまたは権限セットだけに、タブとページへのアクセスを許可します。
動作確認したときに見えるもの
「ファイルを選択」でCSVを選び、「CSVファイルアップロード」ボタンを押します。行数やヘッダーが条件どおりでない場合や、文字コードがShift_JISと判定できない場合は、画面上部に赤字のエラーメッセージが表示されます。すべて通ると取引先責任者が作成され、「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へ切り替えてください
参考:当時の画面
記事を最初に書いた当時の画面です。いまの手順と違うところは、各画像の説明に書いています。
Salesforceの導入・運用についてご相談ください
導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。