Salesforce: Acquiring Dependent Picklists In Apex Contd.

Introduction

Greetings my fellow earth dwellers.  Previously, I blogged about the acquisition of dependent picklists in Apex (as the title implies).  Although the algorithm works, I managed to later identify what seemed to be 3 bottlenecks in the code.  So I’m going to do what I think is “the right thing to do” and share my solutions to them 🙂 .  I will include the final code snippet at the bottom so that you may feel free to copy and paste.

In total, these optimizations provided me with approximately 76% reduction in CPU Time.

Bottleneck 1

The first one I identified is the continuous serialization and deserialization of the dependent picklist entry in the for loop.

I figured it’s better to add all the entries to a list first, then serialize and deserialize it once to get the attributes instead of incurring the overhead of multiple calls.

 

Bottleneck 2

The next one is the sub loop which tests each dependent picklist entry against each controlling value.

This caused the overhead of calling the “testBit” function for every Controlling Index.  I analyzed the testBit function and realized there’s only a small portion which depends on the controlling index, the rest is constant for the validBit.  With this information, I added a subsequent function called “testBits” which takes the list of Controlling Indexes to test against.  With this information, I’m now able to isolate the portion unique to the controlling index.  This allows me to remove an extra 2 for loops per that were incurring unnecessary CPU cycles and place them outside the context of the same said index.

 Bottleneck 3

The last one is the call to Math.Pow in the testBit function.  I realized, that when getting the testBit value, part of that function is dependent on the value 2 being raised to a power of the shiftBits.  The overhead from this function was unnecessary, and so I optimized Math.Pow(2,shiftBits) to 2 << (shiftBits -1) which is already included in the above snippet.

Other fine tuning

I also added the option to call the method GetDependentOptions explicitly with the object type (GetDependentOptionsImpl) or with the object name (GetDependentOptions).   I realized that at moments it’s unnecessary to ask the system to find the object type by name if the caller function already has the object type available.  In order to make the changes backward compatible, I kept the original method parameters and just forwarded it to the submethod that uses this optimal route.  At the end of the day, either method can be used by the caller.

The Full Code Change

Step 1: Add the new testBits function.  No need to remove the older one, it is however deprecated and no longer used.

 Step 2: Add the new GetDependentOptions and GetDependentOptionsImpl functions

There you go guys.  Let me know if you run into any issues.  And remember – always test in sandbox first ;).  Stay thirsty my friends.

titancronusSalesforce: Acquiring Dependent Picklists In Apex Contd.

36 Comments on “Salesforce: Acquiring Dependent Picklists In Apex Contd.”

  1. Pingback: Salesforce: Acquiring Dependent Picklists in Apex | Tales of an earthling

  2. erik

    close – at least it runs in apex vs a VF/js hack BUT – the values are not correct in the several instances i tried – close but not correct – i suspect something in the base64 bit math but i lack the skills to troubleshoot it 🙂

    1. titancronus

      Hey Erik, I think there are a few existing solutions out there for the VF/js hack. This one is exclusively for apex because it’s based on different bit shifting that’s done on the server end and not the client end (JS)

  3. Jennifer O'Neill

    Love this code but for some reason it is not returning all my dependent values, only a handful. Any ideas why this is happening? It works find for another field but just this one.

  4. Marko Tomic

    I like the idea of making it in apex, good job!

    However, I found some cases where code doesn’t work. For example, if the validFor is”///////////////A” code breaks in the TestBit method line
    pFullValue = pFullValue + (pBytes[i] << (pShiftAmount));
    because pBytes[i] is null.

    Unfortunatelly, I am not really good at those encodings and shiftings so I could't figure out where the problem is. Will continue however to learn and try in order to figure out how to make it work.

    1. Marko Tomic

      Ok, I figured that out. The problem was because Base64CharCodes was initialized without’/’ character which is a valid base64 character. Adding this character to Base64CharCodes solves this issue.

      However, there is one more. Something is wrong with last for loop in testBits() method. I noticed that for some controlling fields I don’t have any dependent values but I should. After debugging I found that for those controlling fields tBitVal is “-1” and thus code don’t fall int results.add(n). I suppose that “-1” should not be expected value here, it should be 0 or 1

      1. Paul N

        How do you add ‘/’ to the Base64CharCodes?
        Did you figure out what the other issue was? I’m getting incorrect results for some of mine. Majority of them are working, but some are returning values that aren’t set?

      2. Paul N

        Added
        Base64CharCodes.put(‘+’,62);
        Base64CharCodes.put(‘/’,63);
        to the initialization. (But didn’t solve my issue of it returning incorrect results… So close and yet so far.)

  5. Alta Fisk

    Woah! I’m really loving the template/theme of this site. It’s simple, yet effective. A lot of times it’s challenging to get that “perfect balance” between user friendliness and visual appeal. I must say you’ve done a amazing job with this. Additionally, the blog loads super quick for me on Safari. Excellent Blog!

  6. Yehho

    Thanks for this tutorial. Would be great if there is some example of how to use this code in practice.
    How can I pass controlling and dependent fields names?

    Thank you

  7. Paul N

    I believe the issue with this not always returning the correct value is because when SFDC converts base64 to string, it doesn’t handle non-ascii well (converts to UTF-8) and those characters out of normal range will always return char value of 65535. So this doesn’t allow you to use the converted string to find set bits.

    I created code to convert base64 into an integer list, then used that integer list (instead of strings) to create the listing of “set bits” in the “validFor”. This new code appears to work for me — hopefully it will work for you and anyone else coming to this page. (Note, the class I put this code in is called “Globals” — you will need to adjust the references to “Globals.TPicklistEntry” appropriately.

    // Converts a base64 string into a list of integers representing the encoded bytes
    public static List B64ToBytes (String sIn) {
    Map base64 = new Map{65=>0,66=>1,67=>2,68=>3,69=>4,70=>5,71=>6,72=>7,73=>8,74=>9,75=>10,76=>11,77=>12,78=>13,79=>14,80=>15,81=>16,82=>17,83=>18,84=>19,85=>20,86=>21,87=>22,88=>23,89=>24,90=>25,97=>26,98=>27,99=>28,100=>29,101=>30,102=>31,103=>32,104=>33,105=>34,106=>35,107=>36,108=>37,109=>38,110=>39,111=>40,112=>41,113=>42,114=>43,115=>44,116=>45,117=>46,118=>47,119=>48,120=>49,121=>50,122=>51,48=>52,49=>53,50=>54,51=>55,52=>56,53=>57,54=>58,55=>59,56=>60,57=>61,43=>62,47=>63};

    List lstOut = new List();
    if ( sIn == null || sIn == ” ) return lstOut;

    sIn += ‘=’.repeat( 4 – Math.mod( sIn.length(), 4) );

    for ( Integer idx=0; idx < sIn.length(); idx += 4 ) {
    if ( base64.get(sIn.charAt(idx+1)) != null ) lstOut.add( (base64.get(sIn.charAt(idx)) <>> 4) );
    if ( base64.get(sIn.charAt(idx+2)) != null ) lstOut.add( ((base64.get(sIn.charAt(idx+1)) & 15)<>> 2) );
    if ( base64.get(sIn.charAt(idx+3)) != null ) lstOut.add( ((base64.get(sIn.charAt(idx+2)) & 3)<<6) | base64.get(sIn.charAt(idx+3)) );
    }

    //System.Debug('B64ToBytes: [' + sIn + '] = ' + lstOut);
    return lstOut;
    }//B64ToBytes
    public static List BlobToBytes (Blob input) {
    return B64ToBytes( EncodingUtil.base64Encode(input) );
    }//BlobToBytes

    // Converts a base64 string into a list of integers indicating at which position the bits are on
    public static List cnvBits (String b64Str) {
    List lstOut = new List();
    if ( b64Str == null || b64Str == ” ) return lstOut;

    List lstBytes = B64ToBytes(b64Str);

    Integer i, b, v;
    for ( i = 0; i < lstBytes.size(); i++ ) {
    v = lstBytes[i];
    //System.debug ( 'i['+i+'] v['+v+']' );
    for ( b = 1; b <= 8; b++ ) {
    //System.debug ( 'i['+i+'] b['+b+'] v['+v+'] = ['+(v & 128)+']' );
    if ( ( v & 128 ) == 128 ) lstOut.add( (i*8) + b );
    v <<= 1;
    }
    }

    //System.Debug('cnvBits: [' + b64Str + '] = ' + lstOut);
    return lstOut;
    }//cnvBits

    public class TPicklistEntry{
    public string active {get;set;}
    public string defaultValue {get;set;}
    public string label {get;set;}
    public string value {get;set;}
    public string validFor {get;set;}
    public TPicklistEntry(){
    }
    }//TPicklistEntry

    public static Map<String,List> GetDependentOptions(String pObjName, String pControllingFieldName, String pDependentFieldName) {
    Map<String,List> mapResults = new Map<String,List>();

    //verify/get object schema
    Schema.SObjectType pType = Schema.getGlobalDescribe().get(pObjName);
    if ( pType == null ) return mapResults;
    Map objFieldMap = pType.getDescribe().fields.getMap();

    //verify field names
    if (!objFieldMap.containsKey(pControllingFieldName) || !objFieldMap.containsKey(pDependentFieldName)) return mapResults;

    //get the control & dependent values
    List ctrl_ple = objFieldMap.get(pControllingFieldName).getDescribe().getPicklistValues();
    List dep_ple = objFieldMap.get(pDependentFieldName).getDescribe().getPicklistValues();

    //clear heap
    objFieldMap = null;

    //initialize results mapping
    for(Integer pControllingIndex=0; pControllingIndex<ctrl_ple.size(); pControllingIndex++){
    mapResults.put( ctrl_ple[pControllingIndex].getLabel(), new List());
    }
    //cater for null and empty
    mapResults.put(”, new List());
    mapResults.put(null, new List());

    //serialize dep entries
    List objDS_Entries = new List();
    objDS_Entries = (List)JSON.deserialize(JSON.serialize(dep_ple), List.class);

    List validIndexes;
    for (Globals.TPicklistEntry objDepPLE : objDS_Entries){

    validIndexes = cnvBits(objDepPLE.validFor);
    //System.Debug(‘cnvBits: [‘ + objDepPLE.label + ‘] = ‘ + validIndexes);

    for (Integer validIndex : validIndexes){
    mapResults.get( ctrl_ple[validIndex-1].getLabel() ).add( objDepPLE.label );
    }
    }

    //clear heap
    objDS_Entries = null;

    return mapResults;
    }//GetDependentOptions

    1. titancronus

      Good find Paul N. I couldn’t comment much because I don’t use Salesforce anymore in my current job and as a result, it’s a tad bit harder for me to not just find the time to stay up to speed with it, but to test any changes I would make to the code. But I’m glad you figured it out and that should help other readers as well!

    2. Aarthi Ramasamy Chidambaram

      Hi Paul
      I am trying to implement your logic bu t the content you put on this webpage was missing or misinteruppted the logic behind the encoding.
      would you mind please pass down the code if you have it in git? Thanks

  8. yo

    Hi Paul,

    could you please correct the following code lines?
    if ( base64.get(sIn.charAt(idx+1)) != null ) lstOut.add( (base64.get(sIn.charAt(idx)) > 4) );
    if ( base64.get(sIn.charAt(idx+2)) != null ) lstOut.add( ((base64.get(sIn.charAt(idx+1)) & 15)> 2) );

    Even though I used your code in the comment, still it doesn’t pickup correct values from the dependent list. 🙁

    1. Paul N

      I’m really sorry, not sure why the code is coming out wrong in the posting above… there should be three greater-than symbols in a row at the end of those lines:

      if ( base64.get(sIn.charAt(idx+1)) != null ) lstOut.add( (base64.get(sIn.charAt(idx)) <>> 4) );
      if ( base64.get(sIn.charAt(idx+2)) != null ) lstOut.add( ((base64.get(sIn.charAt(idx+1)) & 15)<>> 2) );

      Hopefully that works…
      Here’s a link to a “pastie” of the code to ensure it’s right:
      http://pastie.org/private/ykjr9ircfh08d8iygfliyg

  9. yo

    Hi Paul,

    Now this is fantastic. I’ve got over 150 controlling picklist values and over 5000 dependent values.. This works like a charm. I found another javascript solution on the net and still its not reliable as this and fails to get the correct values. Thank you for this.

  10. Manuel Maqueda

    Many thanks Paul N!
    I had some issues with the previous code when obtaining the dependencies between States and Countries. This seems work fine for me for this picklists 🙂

  11. Himanshu

    Hi Paul,

    Appreciated for your help!!

    Its working perfectly for me. Keep posting valuable stuffs.

    Thanks
    Himanshu Kalra

  12. Sabina

    Hi, thanks for this, nicely done!
    I have some suggestions:

    1. It looks like you are copying dep_ple to objEntries. Is there any reason why you are not serialising straight from dep_ple? objDS_Entries = (List)JSON.deserialize(JSON.serialize(dep_ple), List.class);

    2. Is there any difference between if(objDepPLE.validFor==null || objDepPLE.validFor==”){ continue; } // do code AND if(objDepPLE.validFor!=null && objDepPLE.validFor!=”){ //do code } ?

    3. The List nList in testBits will always be 0, 1…. ctrl_ple.size(). I would maybe change the nList to an Integer ctrlListSize, change for(Integer i=0; i<nListSize; i++) to for(Integer i=0; i<ctrlListSize; i++) and inside the for loop replace n with i; also , in GetDependentOptionsImpl, I would change:

    for(Integer pControllingIndex=0; pControllingIndex<ctrl_ple.size(); pControllingIndex++){
    //get the pointer to the entry
    Schema.PicklistEntry ctrl_entry = ctrl_ple[pControllingIndex];
    //get the label
    String pControllingLabel = ctrl_entry.getLabel();
    //create the entry with the label
    objResults.put(pControllingLabel,new List());
    //keep track of the controlling indexes
    lstControllingIndexes.add(pControllingIndex);
    }

    to

    for( Schema.PicklistEntry ctrl_entry : ctrl_ple){
    //get the label
    String pControllingLabel = ctrl_entry.getLabel();
    //create the entry with the label
    objResults.put(pControllingLabel,new List());
    }

    AND

    validIndexes = BitSetInstance.testBits(objDepPLE.validFor,lstControllingIndexes);

    to

    validIndexes = BitSetInstance.testBits(objDepPLE.validFor,ctrl_ple.size());

    1. Paul N

      Sabina,
      Did you see my version (linked to above at: https://www.dropbox.com/s/4gi7m38sr4blqqo/GetDependentOptions.txt)? The original version doesn’t work correctly for all situations. I also addressed some on the concerns you had.

      1) I did leave this unchanged in my code… dep_ple is type “List”, while objDS_Entries is of type “List” (which is a wrapper class. The serialize/deserialize converts the object from a PicklistEntry to our customize “TPicklistEntry” type which allows us to get the “ValidFor” portion which cannot be accessed using standard PicklistEntry.
      I use “objDS_Entries” just to make the code a bit more readable. I clear it at the end to remove the memory usage. If you wanted to eliminate “objDS_Entries” and just put the serialize/deserialize in the “for” loop, this shouldn’t cause any issues.

      2) I don’t think there would be much difference in how you suggest other than readability / personal preference. Obviously, the “continue” would prevent any other code being done after the “if”, but if there isn’t anything else to do, then it shouldn’t make a difference.

      3) I completely changed the way the bitset is determined — as the original had issues with non-standard characters when converted using the Base64 conversion functions (which are intended for character conversion).

      1. Sabina

        Hi Paul,

        Thanks, I will give it a try 🙂 To reply to your remarks:

        1) I was talking about objEntries, not objDS_Entries, which is indeed a different type of List. The code is first copying dep_ple into objEntries and THEN serialising to objDS_Entries. I was just wondering why not skip objEntries completely.

        But I see that in your code (objDS_Entries = (List)JSON.deserialize(JSON.serialize(dep_ple), List.class);) you did just that 🙂

        2) There is no difference, but I always care about readability 😉

  13. manoj

    I have a controlling pick list with 240 values and a dependent pick list with length of validfor string as 30. Is this normal as target octet in the algorithm will always be bytesbeingused -1. If yes, is there a better way to handle the scenario?

Leave a Reply

Your email address will not be published. Required fields are marked *