13

I am running into ToManyStatements Limits while creating a CSV blob for with the following code

for(SObject row: list) {
  for(String fieldName : allFields) {
    line += fieldName + ',';
  }
  csvAsString += '\n' + line;
}

What I need is probably a clever regexp that replaces Salesforce SObject String format like:

CustomObject__c: {field__c=value, field__c=value,...}

to a csv value line like:

value,value,value,....

Then I could change my code like this:

for(SObject row: list) {
  csvAsString += '\n' + row.doRegExpMagic();
}

To create the first line of the CSV I probably need another regexp to extract the field list

field1__c,field2_c,...

from my SOQL query

SELECT field1__c,field2_c,... FROM CustomObject__c ...

All you Regexp gurus out there.. please help ;-)

Robert Sösemann
  • 37,622
  • 26
  • 165
  • 495

5 Answers5

8

Creating a CSV file blob in Apex

My recommendations:

  1. use Batch Apex, passing in the initial Lists as stateful variables using Database.Stateful, and then creating a CSV row out of each SObject during the Batch Apex execute method. In the finish method of your Batch Apex, send an email containing the generated CSV. Using Batch Apex to do the CSV row generation will avoid the Too Many Script Statements error, since Batch Apex limits are reset for each Batch Apex execute method invocation. The resultant CSV file can be stored as a String instance variable, and converted to a Blob in the finish method, then emailed to the user requesting the export.
  2. Create the blob client-side using JavaScript (no script statement limits there!), and sent to Apex using one of the following
    • JS Remoting
    • VF ActionFunction
    • HTTP POST Request

Example of Batch Apex

Here is some sample code for the JavaScript method:

  1. To create CSV fields in JavaSCript, here's some a method that will work:

    // Escapes a String such that it can be used in a CSV cell
    // Based on Apache Commons StringEscapeUtils library
    // (see Wikipedia and RFC 4180)
    // Parameters:
    //  input - the input CSV column String, may be null
    //  Returns:
    //  the input String, enclosed in double quotes 
    //      if the value contains a comma, newline or double quote, 
    //      '' if null string input.
    //  Also, any double quote characters in the value
    // are escaped with another double quote.
    
    var QUOTE = '"',
        ESCAPED_QUOTE = "\"\"",
        CHARS_THAT_MUST_BE_QUOTED = [ ',' , '"' , '\n' ];
    
    var escapeCSVCell = function(c) {
       if ( c.indexOf( QUOTE ) > -1 ) c = c.replace(/"/g, ESCAPED_QUOTE );
          $.each(CHARS_THAT_MUST_BE_QUOTED,function(i,char){
             if (c.indexOf(char) > -1) {
                c = QUOTE + c + QUOTE;
                return false;
             }
          });
       return c;
    }
    
    // Call escapeCSVCell on each CSV field
    

Parsing CSV files in Apex

Number One: Do not roll your own CSV Parser: use someone else's! There are a lot of complexities to parsing IETF RFC 4180 - compliant CSV files that you will uncover, one after another, if you try to do roll your own parser. Trust me, not worth it.

And guess what? You don't have to!

Marty Chang has done the Salesforce/Apex developer commmunity a huge service by writing a rock-solid, IETF RFC 4180 compliant CSV Parser FOR APEX!!!

Here is the link to download the 2 files he wrote for this purpose, CSVParser and CSVReader. I've used it before, trust me, it handles everything, even newlines embedded in your CSV fields (try writing that yourself --- NOT straightforward.)

Here's how to use it:

// (Only necessary if your CSV file starts as a string)
Blob csvBlob = Blob.valueOf(csvString);

// Convert your csv file into a list of CSV fields
List<List<String>> fieldsByLine = CSVParser.readIETFRFC4180CSVFile(csvBlob);

Yeah, it's that easy!

His solution uses some good RegEx to bypass the "too many statements" limit.

Creating SObject records from CSV Rows in Apex

As far as creating records from these CSV rows, i've done this before using Batch Apex. Convert your CSV files into rows during the Batch Apex job and create rows one-by-one in the execute method. Also, shameless plug, there's actually an IETF RFC 4180-compliant CSV Import Wizard built-in to Skuid, so if you are open to using an external library, why roll your own parser?

Parsing CSV Rows in Apex

Winter 13 (API v26) has some awesome methods for escaping/unescaping CSV fields:

  • String.escapeCsv(String textToMakeIntoACSVField)
  • String.unescapeCsv(String csvField)
zachelrath
  • 9,533
  • 3
  • 39
  • 61
  • You're getting my +1 because it's a useful tool for future use. But I believe it won't help in this case. "I am running into ToManyStatements Limits while creating a CSV blob", the very first line in the question ;) – eyescream Nov 19 '12 at 12:32
  • 2
    Can you create the blob client-side? That's what I do. Create it using JavaScript, then sent to Apex using Visualforce Action, JS Remoting, or POST data. – zachelrath Nov 19 '12 at 12:35
  • 1
    zachelrath: I don't get your solution. I don't want to parse CSV. I just need to write a CSV and I need to use System code to not run into limits.. Creating a CSV in browser memory is a too hacky solution for me. – Robert Sösemann Nov 19 '12 at 12:43
  • @RobertS , see my clarification of the first solution. I think you want the "Export" button to kick off a Batch Apex process, whose finish method will send an Email with the generated CSV. Each execute method in Batch APex gets a fresh set of governor limits, so this will solve the Too Many Script Statements limit. To write an IETF RFC 4180 compliant CSV file (which you should do if you want your customers to actually be able to open your CSV files), you'll NEED to use something like String.escapeCsv() on each of your SObject rows' field values. – zachelrath Nov 19 '12 at 17:21
  • 1
    @zachelrath, to use Batch writing files would need to be chunkifiable in any way. But as Files don't allow an append there is no way to do this. Or do you see one? – Robert Sösemann Nov 19 '12 at 20:37
  • Yes Marty Chang did an excellent job! We extended his library to support parsing larger CSV files via Batch Apex Iterators here, http://developer.financialforce.com/customizations/importing-large-csv-files-via-batch-apex/ – Andrew Fawcett Nov 19 '12 at 23:49
  • BTW, I also just noticed some CSV escaping and unescaping functions on the String class in Apex, never seen those before! :) – Andrew Fawcett Nov 19 '12 at 23:50
  • 1
    @RobertS, as I mentioned at the top of my post, "Files" are just Blobs, and Blobs can be created just by doing Blob.valueOf(someString), and you don't have to do this until you're ready to send the final file, i.e. in the finish method of your Batch Apex. So store the String as an instance variable on your Batch class, and add to it during each iteration of the execute method. Then in the finish method, just call Blob.valueOf(csvFileString), and set this as the body of a Messaging.EmailFileAttachment – zachelrath Nov 20 '12 at 00:23
  • It doesn't exactly handle everything. I sadly bumped into a situation where its not parsing a CSV file correctly. The file can be opened without any issues in Excel/Pages/LibreOffice, but the CSV Parser seems to have issues with it from the second row onwards. It looks like it eats the first character of the second row, and from then on, the whole thing is a mess. Any alternatives for this? – Laureant Nov 28 '19 at 12:51
4

Here's a rather rough way of achieving what you want, though remember that if you need the fields in a certain order you're going to have trouble with splitting the string representation of the object in this way!

String theObject = 'CustomObject__c: {field__c=value, field__c=value}';
theObject = theObject.subString(theObject.indexOf('=') + 1, theObject.length() - 1);
String output = theObject.replaceAll(',.*=', ',');

Probably not fool proof*, but hopefully will get you on your way.



  • Definitely not fool proof, you wouldn't want to work on some data where a fool has put an comma in a field, for example.
Matt Lacey
  • 25,618
  • 7
  • 66
  • 149
  • 1
    That looks good and easy. But I'm just thinking of the case when fields contain one of those chars: ,=... – Robert Sösemann Nov 19 '12 at 11:38
  • Doh, that's an altogether much more complicated problem! Would it be easier to fire off a batch job for this? :) – Matt Lacey Nov 19 '12 at 11:44
  • What I am doing is (mis)using email services to generate big CSV files. Because their heap is 36 MB and not 6 or 12 like in Batch. But I see that this replace thing won't work. – Robert Sösemann Nov 19 '12 at 11:56
  • My other post about the CSV thing is here http://salesforce.stackexchange.com/questions/3894/create-csv-file-bigger-than-15-mb-from-within-force-application-no-dataloader – Robert Sösemann Nov 19 '12 at 11:56
  • If you could create the CSV data in a formula field (or even a trigger?) then you could then extract this data with dataloader to build up the files. – Matt Lacey Nov 19 '12 at 12:03
  • I need to create it in Apex automatically and can't use external or manual means. Sorry... – Robert Sösemann Nov 19 '12 at 12:40
3

You can also read and process the CSV file in the execute method. http://developer.financialforce.com/customizations/importing-large-csv-files-via-batch-apex/

Agustina García
  • 723
  • 1
  • 10
  • 21
0

I'm going to guess there's a simple reason for this - but instead of Apex, what about doing a weekly export of the data (Data Export under Setup)? Auto-converts to CSV. I'm assuming you need it more on-demand?

joshbirk
  • 3,400
  • 13
  • 20
0

I would do this client side.

You can perform multiple queries and save all the results in a single array, and generate a csv for thousands and thousands of records this way because limits won't get in the way.

You don't need to involve the server at all, except for fetching the data. Use downloadify (https://github.com/dcneiner/Downloadify) to initiate a purely client side download.

Phil Rymek
  • 6,159
  • 1
  • 33
  • 41