+ Start a Discussion
Olavo ZapataOlavo Zapata 

Helping to fix an appex trigger that are acusing null accounts

Hello guys,

I would like your help in another case related to Apex.
I'm having a trigger in production that gets an API integration.

Basically, the Trigger finds the Account ID based on the External ID that comes from the integration.
The trigger is working "well" but every day I get an error email saying that an error occurred with null values.

I understand that the error is because some External ID coming from the API not found an Account.
But the behavior I expect in such cases is that the code accepted and go to the next record without returning an error.

I just can not do that on the trigger.
The last time I changed it. It registered all External IDs even without finding Account. (and it's not what I want)
Could someone help me?

Trigger:
trigger UpdateAccountId2 on nKPI__c (before update, before insert) {

    Map<String, nKPI__c> tintgreMap = new Map<String, nKPI__c>();
    
    for(nKPI__c ti : trigger.new) {
        
        if(trigger.isInsert || (trigger.isUpdate && ti.RDStationID__c != trigger.oldMap.get(ti.Id).rdstationid__c)) {
            tintgreMap.put(ti.rdstationid__c, ti);
        }
    }

    if(tintgreMap == null) return;
    
    Map<String, Contract> accountMap = new Map<String, Contract>();
    for (Contract account : [Select AccountId,externalCode__c FROM Contract WHERE Product_Group__c = 'RD Station' AND externalCode__c = :tintgreMap.KeySet()]) {
        accountMap.put(account.externalCode__c, account);
    }
    
    if(accountMap == null) return;
    
    for(nKPI__c tiRecord : tintgreMap.Values()) {
        
        tiRecord.Account__c = accountMap.get(tiRecord.rdstationid__c).AccountId;
        
    }

}
Error msg:
Apex script unhandled trigger exception by user/organization: 0051a000000ZQgs/00D1a000000I0UO
UpdateAccountId2: execution of BeforeInsert
caused by: System.NullPointerException: Attempt to de-reference a null object
Trigger.UpdateAccountId2: line 23, column 1
Thanks
Glyn Anderson (Slalom)Glyn Anderson (Slalom)
Olavo,  I've added a check to make sure that the 'tintgreMap' never contains a null key, as that would likely return a large number of Contracts in the subsequent query.  I also changed the tests on lines 12 and 19 to be "isEmpty()" rather then "== null", as neither map will be null - they are initialized on lines 3 and 14.  Let me know if this solves the problem.  Thanks!
trigger UpdateAccountId2 on nKPI__c ( before update, before insert )
{
    Map<String, nKPI__c> tintgreMap = new Map<String, nKPI__c>();
    
    for ( nKPI__c ti : Trigger.new )
    {
        if  (   (   Trigger.isInsert
                ||  (   Trigger.isUpdate
                    &&  ti.RDStationID__c != Trigger.oldMap.get(ti.Id).RDStationID__c
                    )
                )
            &&  ti.RDStationID__c != null
            )
        {
            tintgreMap.put( ti.RDStationID__c, ti );
        }
    }
    if ( tintgreMap.isEmpty() ) return;
    
    Map<String, Contract> contractMap = new Map<String, Contract>();
    for ( Contract contract :
        [   SELECT  AccountId, ExternalCode__c
            FROM    Contract
            WHERE   (   Product_Group__c = 'RD Station'
                    AND ExternalCode__c IN :tintgreMap.KeySet()
                    )
        ]
        )
    {
        contractMap.put( contract.ExternalCode__c, contract );
    }
    if ( contractMap.isEmpty() ) return;
    
    for ( nKPI__c tiRecord : tintgreMap.values() )
    {
        tiRecord.Account__c = contractMap.get( tiRecord.rdstationid__c ).AccountId;
    }
}

 
v varaprasadv varaprasad
Hi Olavo,
 
for ( nKPI__c tiRecord : tintgreMap.values() )
    {
	    if(contractMap.get( tiRecord.rdstationid__c ).AccountId != null) //check null exception
        tiRecord.Account__c = contractMap.get( tiRecord.rdstationid__c ).AccountId;
    }

Thanks
Varaprasad
@For  Support: varaprasad4sfdc@gmail.com
 
Olavo ZapataOlavo Zapata
Hi Glyn,

First of all thanks for your help.

Before I tried to use 'isEmpty()' instead of '==null' but the trigger act with different behavior that I expected.
I want to save the record on nKPI (custom object) only if API batch finds an RDStationID on Contract else I would like to discard that record from that day. Because If the API didn't find in that batch probably next day they will find and match.

When I used isEmpty the record that didn't find the external ID Match was saved without AccountId.

There is a way to "discard" the unmatched?
Thanks.
Maharajan CMaharajan C
Also you can use as per your trigger:

trigger UpdateAccountId2 on nKPI__c (before update, before insert) {

    Map<String, nKPI__c> tintgreMap = new Map<String, nKPI__c>();
    
    for(nKPI__c ti : trigger.new) {
        
        if(trigger.isInsert || (trigger.isUpdate && ti.RDStationID__c != trigger.oldMap.get(ti.Id).rdstationid__c)) {
            tintgreMap.put(ti.rdstationid__c, ti);
        }
    }

    if(tintgreMap.isEmpty()) return;
    
    Map<String, Contract> accountMap = new Map<String, Contract>();
    for (Contract account : [Select AccountId,externalCode__c FROM Contract WHERE Product_Group__c = 'RD Station' AND externalCode__c = :tintgreMap.KeySet()]) {
        if(contract.AccountId != null)
        accountMap.put(account.externalCode__c, account);
    }
    
    if(accountMap.isEmpty()) return;
    
    for(nKPI__c tiRecord : tintgreMap.Values()) {
        
        if(accountMap.containsKey(tiRecord.rdstationid__c))
        tiRecord.Account__c = accountMap.get(tiRecord.rdstationid__c).AccountId;
        
    }

}
Glyn Anderson (Slalom)Glyn Anderson (Slalom)
If you're already in the trigger, the only way not to save the record is to add an error to it - which will cause the entire DML to fail.  Whatever the code is that's creating/saving the nKPI records should not create/save records that it doesn't need.  So, you will have to move the logic to that part of the code.  There is no way to save some of the records and discard others once you are already in the trigger.

Having said that, there is one thing you could try, but it's kludgy.  In an "after" trigger, you could create a Set of the IDs of the nKPI records you don't want, and pass that Set of IDs to an @future method that deletes those records.  Let me know if you need a code example.
Glyn Anderson (Slalom)Glyn Anderson (Slalom)
Here's that kludgy code:
trigger UpdateAccountId2 on nKPI__c ( before update, before insert )
{
    if ( Trigger.isBefore )
    {
        Map<String, nKPI__c> tintgreMap = new Map<String, nKPI__c>();
        
        for ( nKPI__c ti : Trigger.new )
        {
            if  (   (   Trigger.isInsert
                    ||  (   Trigger.isUpdate
                        &&  ti.RDStationID__c != trigger.oldMap.get(ti.Id).rdstationid__c
                        )
                    )
                &&  ti.RDStationID__c != null
                )
            {
                tintgreMap.put( ti.rdstationid__c, ti );
            }
        }
        if ( tintgreMap.isEmpty() ) return;
        
        Map<String, Contract> contractMap = new Map<String, Contract>();
        for ( Contract contract :
            [   SELECT  AccountId, ExternalCode__c
                FROM    Contract
                WHERE   (   Product_Group__c = 'RD Station'
                        AND ExternalCode__c IN :tintgreMap.KeySet()
                        )
            ]
            )
        {
            contractMap.put( contract.ExternalCode__c, contract );
        }
        if ( contractMap.isEmpty() ) return;
        
        for ( nKPI__c tiRecord : tintgreMap.values() )
        {
            tiRecord.Account__c = contractMap.get( tiRecord.rdstationid__c ).AccountId;
        }
    }

    if ( Trigger.isAfter )
    {
        Set<Id> nkpisToDelete = new Set<Id>();
        for ( nKPI__c record : Trigger.new )
        {
            if ( record.Account__c == null ) nkpisToDelete.add( record.Id );
        }
        NKPIUtilities.deleteUnwantedRecords_future( nkpisToDelete );
    }
}

public class NKPIUtilities
{
    @future
    public static void deleteUnwantedRecords_future( Set<Id> nkpisToDelete )
    {
        delete [SELECT Id FROM nKPI__c WHERE Id IN :nkpisToDelete];
    }
}
Olavo ZapataOlavo Zapata
Hi Guys,
Thank you very much for your help.

I have tested all the suggestions they have suggested.
But none of them is what I expected.

@varaprasad gave a very lean suggestion.
That actually prevents records that do not have AccountId from being written.
(But I get the same error that I started the post).

@Maharajan
I've tried this method before, but that doesn't stop records from being created.

@Glyn
This is a super creative workaround. But writing to delete later is not quite what I wanted, because of performance issues.

From what I understood than Glyn explained I can't really get a "next" when the Match is not found and interpret this in a good way. Right?

If so, I will keep the code I have and live with the daily error.