Showing posts with label Read CSV File and Insert Records in Salesforce. Show all posts
Showing posts with label Read CSV File and Insert Records in Salesforce. Show all posts

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.