Pages

Friday, July 31, 2026

How to Read CSV file and Insert record using Apex/LWC in Salesforce

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 .csv file via lightning-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__c records 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__c records are updated with new association start/end dates pulled from the CSV.
  • InfRequestInfo__c records 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-upload for a modern, drag-and-drop-friendly upload experience.
  • Convert the uploaded Blob to 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 upsert so 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: