普通にAPIで叩けばいいんじゃないのって話なんですが、APIコール数の問題だったり、そもそもお金がなかったり 、要件として度々話題に上がるので、自分のデベロッパー環境で試行錯誤したことをここにまとめます。
そもそもVisualForceページ経由だとなにが問題になるのか
よく起きるエラーはこの辺:
- ビューステートエラー(Apexクラスに値を渡すときに落ちる)
- ヒープサイズエラー or CPUタイムアウトエラー(取得したデータの処理中に落ちる)
-
<apex:form>
のファイルインプット上限が5MB
※LWCならlightning-file-uploadで回避できる
しかし、それで実装出来ればこの記事を2024年に書いていないのである - 文字コード変換問題(SJIS→UTF8やUTF8→SJISなど)
動いているサンプルコード
ApexクラスだけではどうしようもなかったのでJavascriptで書きました。
とりあえず120MB程度のSJISのCSVファイルで問題なく動いているコードを置いておきます。
みんな大好きKenAll.csvでチェックしてます。
VisualForceページ
使ったNPM=>jQuery,encord.js,PapaParse
test.page
<apex:page docType="html-5.0" id="page_id" controller="csvInputCon">
<apex:includeScript value="{!URLFOR($Resource.jQuery)}" />
<apex:includeScript value="{!URLFOR($Resource.encord)}" />
<apex:includeScript value="{!URLFOR($Resource.PapaParse)}" />
<script>
const MAX_STRINGS = 1000000;
function parseCSV(file) {
return new Promise((resolve, reject) => {
var parsedData = [];
Papa.parse(file, {
header: false, // ヘッダー行がある場合はtrue
encoding: 'Shift-JIS',
complete: function (results) {
parsedData = results.data;
var row = parsedData[0];
var confirmed = confirm('インポートを開始しますか?');
if (confirmed) {
resolve(parsedData);
}else{
return;
}
}
});
})
}
// toStringでUNICODE(UTF8)に変換
const convert2String = function (file) {
const encoding = Encoding.detect(file);
var unicodeString = Encoding.convert(file, {
to: 'UNICODE',
from: encoding
});
var utf8String = unicodeString.toString();
return utf8String;
}
// APEX実行
const REMOTEACTION_OPTIONS = { buffer: false, escape: true, timeout: 120000 };
function postApex(data) {
return new Promise((resolve, reject) => {
Visualforce.remoting.Manager.invokeAction('{!$RemoteAction.csvInputCon.execBatch}',
data,
(result, event) => {
if (event.status){
resolve(result);
}else {
alert("CSV取り込みが失敗しました");
reject(event);
return;
}
}, this.REMOTEACTION_OPTIONS);
});
}
// APEXコントローラーへ値を渡す処理
async function sendCSVData2Controller(_file) {
const noOfChunks = _file.length;
const promises = [];
var convertedData = '';
for (var i = 0; i < noOfChunks; i++) {
if (_file[i].length > 1) {
convertedData += await convert2String(_file[i]) + '\n';
if (convertedData.length >= MAX_STRINGS) {
var orderPromise = await postApex(convertedData);
convertedData = '';
promises.push(orderPromise);
}
}
}
if (convertedData .length >= 0) {
var lastorderPromise = await postApex(convertedData);
promises.push(lastorderPromise );
}
const result = Promise.all(promises);
return (console.log("JS処理終了"), alert("CSV取り込みが完了しました"));
}
// async処理全体概要
async function main(file) {
console.log("JS処理開始");
var parsedCSV = await parseCSV(file);
const result = sendCSVData2Controller(parsedCSV);
console.log("APEX処理開始");
}
main();
// 取得データチェック処理
function checkFile() {
const fileinput = document.getElementById('file_input');
const file = fileinput.files[0];
if (typeof file === "undefined") {
alert("CSVファイルが選択されていません。");
return;
} else {
main(file);
}
}
</script>
<input type="file" id="file_input" name="attFile" accept=".csv" />
<br />
<button onclick="checkFile(); return false;" value="CSV読み込み">Upload File</button>
</apex:page>
- 一時ファイルをContentVersionあたりにテンポラリーで置いて後で結合という挙動もMAX5MBまでなら可能
- 読み込み中にページを更新すると当たり前だけど処理が落ちるのでLoadingOverlay.js辺りでグルグル表示させると良さそう
- 実際は静的リソースなりに格納して使うと可読性高いかな?
- InputはJavascriptのフォームで用意してます
<input type="file" id="file_input" name="attFile" accept=".csv" />
Apexクラス周り
VFのコントローラー
csvInputCon.cls
public with sharing class csvInputCon{
public csvInputCon(){
}
// CSV読み込み
@RemoteAction
public static void execBatch(String dataStr){
CSVBatch batch = new CSVBatch(dataStr);
Database.executeBatch(batch, 100);
}
}
- JavascriptRemotingで渡せる最大文字数は1,000,000文字まで。オーバーすると「input too long」エラーになります
バッチクラス
CSVBatch.cls
global with sharing class CSVBatch implements Database.Batchable<String>, Database.Stateful{
private String csvFile;
private static final String CRLF = '\r\n';
public CSVBatch (String dataStr) {
this.csvFile = dataStr;
}
global Iterable<String> start(Database.batchableContext context) {
return new CSVIterator(csvFile, CRLF);
}
global void execute(Database.BatchableContext batchableContext, List<String> scope){
// execでバッチで行いたい処理を実行
}
global void finish(Database.BatchableContext BC){
// finishで行いたい処理を実行
}
}
CSVIterator.cls
global with sharing class CSVIterator implements Iterator<String>, Iterable<String> {
Private String csvData;
private String rowDelimiter;
public CSVIterator(String dataStr, String rowDelimiter) {
this.rowDelimiter = rowDelimiter;
this.csvData= dataStr;
}
global Boolean hasNext() {
return csvData.length() > 1 ? true : false;
}
global String next() {
String row = this.csvData.subString(0, this.csvData.indexOf(this.rowDelimiter));
this.csvData = this.csvData.subString(
this.csvData.indexOf(this.rowDelimiter) + this.rowDelimiter.length(),
this.csvData.length()
);
return row;
}
public Iterator<String> Iterator() {
return this;
}
}
- Iteratorクラスは、大容量CSV処理の場合は必須っぽい(Splitだと落ちます)
- もしCSVの値をごにょごにょしたい場合はParseクラスも別で持たないと、「regex too complicated」で落ちるので注意!
参考引用したブログ
一部丸パク引用させていただきました。いつも大変お世話になっております。
ちなみに、ダブルクオーテーション対応はこちらのコードでばっちりできてます。PapaParse様様です。感謝感謝!