How to Read CSV File and Insert Records Using Apex/LWC in Salesforce
Uploading data through CSV files is one of the most common requirements in Salesforce projects. Instead of relying on Data Loader or manual imports, you can build a simple Lightning Web Component (LWC) that lets users upload a CSV file directly from the UI, then have an Apex controller parse that file and create or update records automatically.
In this post, we'll walk through a working example that:
- Lets a user upload a
.csvfile vialightning-file-upload - Reads the uploaded file's content in Apex
- Parses CSV rows — including values that contain commas inside quotes
- Uses the parsed data to create, update, and upsert related Salesforce records
Step 1: Build the LWC Upload Component
The front end is intentionally lightweight. It uses the standard lightning-file-upload base component wrapped inside a lightning-card, restricted to .csv files only.
HTML Template
<template>
<lightning-card title="CSV Uploader">
<div class="slds-box slds-m-around_medium">
<lightning-file-upload
accept={acceptedFormats}
label="Attach CSV File"
onuploadfinished={uploadFileHandler}>
</lightning-file-upload>
</div>
</lightning-card>
</template>
JavaScript Controller
import { LightningElement, track } from 'lwc';
import loadCSVData from '@salesforce/apex/CSVController.loadCSVData';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { showToast, handleArraySort } from 'c/cOP_LWC_Utils';
export default class FileUploadExcel extends LightningElement {
get acceptedFormats() {
return ['.csv'];
}
@track contentDocumentId;
@track recordCount;
uploadFileHandler(event) {
const uploadedFiles = event.detail.files;
this.contentDocumentId = uploadedFiles[0].documentId;
loadCSVData({ contentDocumentId: this.contentDocumentId })
.then((result) => {
this.recordCount = result;
})
.catch((error) => {
// handle error, e.g. show a toast
});
}
}
Once the file finishes uploading, Salesforce automatically stores it as a ContentVersion record and returns a documentId. That ID is passed straight to an Apex method, which does all the heavy lifting.
Step 2: Read the File Content in Apex
The uploaded file is stored as a Blob on the ContentVersion.VersionData field. Since Apex doesn't have a built-in "read as string with encoding" helper for every use case, a small utility method converts the blob into a readable string using a specified character set (here, ISO-8859-1, which is a safe default for most CSV exports):
public static String blobToString(Blob input, String inCharset) {
String hex = EncodingUtil.convertToHex(input);
System.assertEquals(0, hex.length() & 1);
final Integer bytesCount = hex.length() >> 1;
String[] bytes = new String[bytesCount];
for (Integer i = 0; i < bytesCount; ++i) {
bytes[i] = hex.mid(i << 1, 2);
}
return EncodingUtil.urlDecode('%' + String.join(bytes, '%'), inCharset);
}
Once you have the string, splitting it into rows is as simple as:
string[] csvFileLines = data.split('\n');
Step 3: Handle Commas Inside Quoted Values
A naive split(',') breaks down the moment a CSV value itself contains a comma — for example, an address field like "Springfield, IL". To handle this correctly, a helper method temporarily replaces quotes and internal commas with placeholder tokens (:quotes: and :comma:), performs the split, and then restores the original characters:
public static string csvdata(string csinfo) {
String csvLine = csinfo;
Integer startIndex;
Integer endIndex;
while (csvLine.indexOf('"') > -1) {
if (startIndex == null) {
startIndex = csvLine.indexOf('"');
csvLine = csvLine.substring(0, startIndex) + ':quotes:' + csvLine.substring(startIndex + 1);
} else if (endIndex == null) {
endIndex = csvLine.indexOf('"');
csvLine = csvLine.substring(0, endIndex) + ':quotes:' + csvLine.substring(endIndex + 1);
}
if (startIndex != null && endIndex != null) {
String sub = csvLine.substring(startIndex, endIndex).replaceAll(',', ':comma:');
csvLine = csvLine.substring(0, startIndex) + sub + csvLine.substring(endIndex);
startIndex = null;
endIndex = null;
}
}
return csvLine;
}
Each field is later restored with .replaceAll(':quotes:', '').replaceAll(':comma:', ',') before it's assigned to a record field.
Step 4: Map CSV Rows to Salesforce Records
With clean, split rows in hand, the controller builds sets and maps to minimize SOQL queries — a best practice to avoid governor limit issues when processing many rows:
set<string> supplierid = new set<string>();
map<Decimal, list<string>> mapincvdata = new map<Decimal, list<string>>();
for (Integer i = 1; i < csvFileLines.size(); i++) {
string row = CSVController.csvdata(csvFileLines[i]);
string[] csvRecordData = row.split(',');
supplierid.add(csvRecordData[0]);
mapincvdata.put(Decimal.valueOf(csvRecordData[0]), csvRecordData);
}
A single query then retrieves all matching Account records at once:
map<string, id> acMap = new map<string, id>();
for (Account acc : [SELECT Id, SupplierID__c FROM Account WHERE SupplierID__c IN :supplierid]) {
acMap.put(acc.SupplierID__c, acc.Id);
}
Step 5: Create, Update, and Upsert Records
From here, the controller builds three separate collections depending on the business need:
Program_Association__crecords are created fresh for each row, using an external key (SupplierNProgramId__c) so the same upsert call can safely run multiple times without creating duplicates.InfSupplierDetail__crecords are updated with new association start/end dates pulled from the CSV.InfRequestInfo__crecords are updated with the relevant program ID, using a set to avoid processing the same request twice.
if (!updateSupplier.isEmpty()) {
update updateSupplier;
}
if (!updateRequestProgramid.isEmpty()) {
update updateRequestProgramid;
}
if (!lstProgramAssociationsToUpsert.isEmpty()) {
upsert lstProgramAssociationsToUpsert SupplierNProgramId__c;
}
Finally, the method returns a short summary string so the LWC can display feedback to the user:
string res = 'Total Record: ' + (csvFileLines.size() - 1) +
'---Supplier Updated:' + updateSupplier.size() +
'--Program Association Created:' + lstProgramAssociationsToUpsert.size() +
'---Request Updated: ' + updateRequestProgramid.size();
return res;
A Simpler Visualforce Alternative
If you don't need the modern LWC experience, the same idea can be done with a classic Visualforce controller. This trimmed-down version reads an uploaded file and creates Account records directly from each CSV column:
public Pagereference ReadFile() {
try {
nameFile = blobToString(contentFile, 'ISO-8859-1');
filelines = nameFile.split('\n');
accstoupload = new List<Account>();
for (Integer i = 1; i < filelines.size(); i++) {
String[] inputvalues = filelines[i].split(',');
Account a = new Account();
a.Name = inputvalues[0];
a.ShippingStreet = inputvalues[1];
a.ShippingCity = inputvalues[2];
a.ShippingState = inputvalues[3];
a.ShippingPostalCode = inputvalues[4];
a.ShippingCountry = inputvalues[5];
accstoupload.add(a);
}
} catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.severity.ERROR,
'An error has occurred reading the CSV file: ' + e.getMessage()));
}
try {
insert accstoupload;
} catch (Exception e) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.severity.ERROR,
'An error has occurred inserting the records: ' + e.getMessage()));
}
return null;
}
This works fine for straightforward files without embedded commas or quotes, but it lacks the more robust CSV parsing shown in the LWC/Apex example above.
Key Takeaways
- Use
lightning-file-uploadfor a modern, drag-and-drop-friendly upload experience. - Convert the uploaded
Blobto a string with the correct character encoding before parsing. - Never assume a plain
split(',')is safe — always account for quoted fields containing commas. - Batch your SOQL queries using sets and maps instead of querying inside loops.
- Use external IDs with
upsertso repeated file uploads don't create duplicate records. - Wrap both the parsing and DML logic in try/catch blocks so partial failures are reported clearly to the user.
With this pattern, you get a reusable, safe, and user-friendly way to bulk-load data into Salesforce straight from a CSV file — no Data Loader required.
No comments:
Post a Comment