1

I'm working on a batch which getting a params from another batch in the finish method.

The Params are :

  1. A Map - Map<Sobject,Set<String>>

  2. A Set - Set<String>

I lost the values of my Map on the second run of the execution method - Means - Map.get(Sobject) returned with null although it had values in it.

That happened only within the second to the last run of the batch (from the second till the end of the chunks).

The first chunk getting the values as expected.

I guess it could be because defined a Sobject as a Key in a Map is not Best Practice because Hashing issue - Because the second param that I'm sending to the batch is a Set<String> - And it keeps its values for all the chunks.

Because of this issue, I decided to send a Set<Id> of all the Sobjects from the first batch, and retrieve the records in the second batch with SOQL query. and I wanted to know what are the best practices to do so.

the queryLocatoer in the Start method is needed to go over another Sobject without any related to the Sobject that I sent.

Where can I put this Query? Can I put it inside the Constructor and assign it to a global variable? Inside the Start method before I set the query for the queryLocator? - since it always will be the same results, I don't want to put it on the execute method.

What do you think? Why we have hashing issue when the key of a Map is a Sobject?

Thanks!

1 Answer 1

3

(When you ask a question it is best to include the important parts of the code as well, so the community can best understand and help you.)

I'm assuming that the batch is declared as stateful.

I think it fairly clear this is a problem related to you using an SObject as a key in a map. In this context the SObject's state (not its identity) is what matters - it is the hashCode of the SObject that makes the key value work, and this is computed from the internal state of the SObject - which fields are populated and what those fields' values are. Use of a mutable value as a key is generally a bad idea.

The next thing to think about is - just what values do you need in your SObject in order to use it in the second batch? Could you use the SObject IDs, so have a Map<Id, Set<String>>? If you can't, could you create yourself an immutable Apex class with attributes to hold the important field values, have a constructor to initialize it from an SObject of the required type and implement equals and hashCode methods to make it usable as a key in the map?

Sort these out and you'll probably find it all works without having to re-query the data in the second batch at all.

UPDATE based on the related question (see comment from OP); this includes some additional tips and an example of the immutable data object I mentioned, above:

There are some odd typos and spellings plus some unnecessary "global" usages (assuming you are not exporting these batches from a package) and likely an unnecessary statefulness on BatchB (a batch only needs to be stateful if the state is to change between invocation of start and finish; any state you have when you call executeBatch is maintained as-is otherwise).

I suggest you try the following:

public class BatchA implements Database.Batchable<SObject>, Database.Stateful {
    private Map<Id, Set<String>> pMap = new Map<Id, Set<String>>();
    private Set<String> bigSet = new Set<String>();

    public BatchA() {
    }

    public Database.QueryLocator start(Database.BatchableContext context) {
        return Database.getQueryLocator('SomeQuery FROM SobjectA');
    }

    public void execute(Database.BatchableContext context, List<SobjectA> scope) {
        for (SobjectA a : scope) {
            Set<String> interests = new Set<String>();

            interests.add(a.SomeField_1__c);
            interests.add(a.SomeField_2__c);
            interests.add(a.SomeField_3__c);

            bigSet.addAll(interests);

            pMap.put(a.Id, interests);
        }
    } 

    public void finish(Database.BatchableContext context) {
        // Chain BatchA to BatchB
        Database.executeBatch(new BatchB(pMap, bigSet), 200);
    }
}

Now BatchB:

public class BatchB implements Database.Batchable<SObject> {
    private Map<Id, Set<String>> pMap;
    private List<String> bigSet = new List<String>(); // Not used

    public BatchB(Map<Id, Set<String>> pMap, Set<String> bigSet) { 
        // Done this way to perform type conversion and no other reason
        this.bigSet.addAll(bigSet);

        // You don't need to worry about changes to the pMap in the BatchA
        // context - BatchA changing the pMap after the Database.executeBatch
        // would have no impact because BatchB gets serialized to the database
        // at the time Database.executeBatch is called and gets deserialized
        // asynchronously at some later point
        this.pMap = pMap;
    }

    public Database.QueryLocator start(Database.BatchableContext context) {  
        return Database.getQueryLocator('SomeQuery FROM SobjectB');
    }

    public void execute(Database.BatchableContext context, List<SobjectB> scope) {
        System.debug('pMap values   => ' + pMap); // Demonstrates the full content of the pMap

        for (SobjectB b : scope) {
            // Try to avoid nested loops - these are really inefficient in Apex. If
            // possible with the required logic use a first loop to turn some data
            // into an appropriate map then have a second (not nested) loop that
            // iterates the target data using map queries to get the relevant data
            for (Id curPmap : pMap.keySet()) {
                System.debug(pMap.get(curPmap)); // Shows the content of the entry for the current ID, repeatedly for every ID and every "b" instance
        }
    }

    public void finish(Database.BatchableContext context) {
    }
}

There is nothing in the code you provided that would cause the pMap to be emptied in BatchB between the first and subsequent chunks. However, since you had it Stateful, if you happened to have some code that you called from BatchB taking the Set<String> obtained from the pMap ("pMap.get(curPmap)" in the code above) where it then called theSet.clear(), the emptied set would be re-persisted along with the map at the end of execute, so on the next iteration it would be a map full of empty sets.

If that is the only reason I would say it is still worth ditching the use of SobjectA as the map key - we know, from the context of use, every one of those SobjectA instances will already have an ID, so the ID is a good way to identify the detail for each instance. If you want other values out of the SobjectA instances for later, you could add something like:

public class SobjectAData {
    Id id { public get; private set }
    SomeType someValueOfInterest { public get; private set }
    SomeOtherType someOtherValue { public get; private set }

    public SobjectAData(SobjectA a) {
        id = a.Id;
        someValueOfInterest = a.Some_Field__c;
        someOtherValue = a.Some_Other_Field__c;
    }

    public Integer hashCode() {
        return id.hashCode();
    }

    public Boolean equals(Object obj) {
        if (obj instanceof SobjectAData) {
            SobjectAData other = (SobjectAData) obj;

            return (id.equals(other.id));
        }

        return false;
    }
}

This implementation assumes that only the ID is relevant for use as a key in any map were this class to be used in that way. This may or may not be reasonable depending on how it is to be used.

Anyway, you could then maintain this data in BatchA, with a member like:

private Map<Id, SobjectAData> aData = new Map<Id, SobjectAData>();

Adding data to this map in BatchA is easy:

aData.put(a.Id, new SobjectAData(a));

You could then pass this data to BatchB via its constructor as needed.

I think this answer covers all the bases. Let me know if I missed anything.

2
  • Yes you right! While you answered I tried to send a Set of Ids and retrieve them with a SOQL query inside the second batch. But it didn't work also and my assuming appeared to be not true, so I change my question a bit, and I wrote a new one : salesforce.stackexchange.com/questions/267702/… and It more detailed. About your answer, did you mean to handle a wrapper and send it to the second batch? If you do, what is the difference between wrapper and Sobject in hashing, Except the number of fields and the data inside?
    – Salvation
    Commented Jun 30, 2019 at 10:36
  • I don't mean a wrapper. I mean you have attributes you initialize from the SObject during construction and then forget about the SObject, holding just the attribute values initialized from the SObject.
    – Phil W
    Commented Jun 30, 2019 at 12:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .