Expect the expected behaviour?

Published in Articles

1. What happens if compare the NULL value with the Date/Datetime value?

Let's try to execute the next code:

if ( null > System.now() ) {} // throw Exception
You will receive an exception that the NULL value cannot be compared with the Datetime value, as expected:
Comparison arguments must be compatible types: NULL, Datetime


But what happens if we try to get the Datetime value from the NULL SObject using the Safe Navigation Operator?
Account a = NULL;
System.assertEquals(NULL, a?.CreatedDate);
System.assertEquals(false, a?.CreatedDate > System.now());
System.assertEquals(false, a?.CreatedDate < System.now());
System.assertEquals(false, a?.CreatedDate == System.now());
We have the same situation where we compare the NULL value with the Datetime value, but this time it is executed without any exceptions. Why behaviour is different?

Reveal explanation

Close

The Safe Navigation Operator helps to operate on a null value and returns null instead of throwing a NullPointerException. The type of the expression depends on the return type of the method being referred, or the data type of the field being referred in the variable.

Account a = NULL;
if ( a?.CreatedDate > System.now() ) {} // No Exception
Here, the NULL variable "a" is of type "Account". And once we use "a?.FieldName", the type of this expression becomes the type of the field being referred here. So while using "a?.CreatedDate", the type of this expression becomes "Datetime". This is just like using "Datetime d = null". Ref article for type of expression: Use the Safe Navigation Operator to Avoid Null Pointer Exceptions
Datetime dt = NULL;
if ( dt > System.now() ) {} // No Exception

Due to this reason, the following exception doesn't occur while using "if (a?.CreatedDate > System.now())". Comparison arguments must be compatible types.

As stated above, type of first expression "a?.CreatedDate" is Datetime and its value is NULL. And the type of second expression "System.now()" is also Datetime which is the same type as first expression (though its value is not NULL).

In the first example while using "if (null > System.now()) { }", the type of first expression remains NULL because we are not using any method/field for reference from where the type can be inherited. This is why the "compatible types" exception is observed here.

2. Unexpected exception with SObject.getSObjects() method

To reproduce this behavior please prepare an account record without any child contacts and after that try to execute the next code:

final Id accountId = 'REPLACE_WITH_ACCOUNT_ID';

List<Account> accounts = [SELECT Id, (SELECT Name FROM Contacts LIMIT 1) FROM Account WHERE Id = :accountId];
System.assertEquals(1, accounts.size());
System.assertEquals(0, accounts[0].Contacts.size(), 'Issue reproduced only on parents without any child records');

List<Contact> contacts1 = (Contact[]) accounts[0].getSObjects('Contacts');
System.assertEquals(null, contacts1);

List<Contact> contacts2 = (Contact[]) accounts[0].getSObjects(Contact.AccountId);
System.assertEquals(null, contacts2); // <-- this line produce unexpected exception
You will receive an unexpected exception. Why does it throw unexpected when using the getSObjects() method for the field specified by the field token?
System.UnexpectedException: Salesforce System Error: 2331558-51027 (966141544) (966141544)

Reveal explanation

Close

This issue has been identified as a bug at salesforce end when using sObject.getSObjects(FieldName) for parent to child relationships and when the Account has no contacts. Salesforce R&D team is working on high priority and would be fixed soon in the upcoming patches/releases. Following is the Known issue link:
Salesforce internal error thrown when trying to access related list using getSobjects if there are no records in the related list

Please click the 'This Issue Affects me' checkbox in article to receive the updates on this issue when it is fixed.

3. Custom Settings, Custom Metadata: what are the uses of limits?

We all know that custom settings and custom metadata records are exposed in the application cache. But do you know that querying custom settings data using SOQL doesn't use the application cache and is similar to querying a custom object:

List<ListCustomSetting__c> customSettings = [SELECT Id FROM ListCustomSetting__c];
System.assertNotEquals(0, customSettings.size());
System.assertEquals(4, customSettings.size());
// Number of SOQL queries: 1 out of 100
// Number of query rows: 4 out of 50000
Let's try to do the same but with using Apex Custom Settings methods:
List<ListCustomSetting__c> customSettings = ListCustomSetting__c.getAll().values();
System.assertNotEquals(0, customSettings.size());
System.assertEquals(4, customSettings.size());
// Number of SOQL queries: 0 out of 100
// Number of query rows: 0 out of 50000
What about custom metadata? Is it works in the same way? Let's try and see.
List<CustomMetadata__mdt> customMetadata = [SELECT Id FROM CustomMetadata__mdt];
System.assertNotEquals(0, customMetadata.size());
System.assertEquals(4, customMetadata.size());
// Number of SOQL queries: 0 out of 100
// Number of query rows: 4 out of 50000
List<CustomMetadata__mdt> customMetadata = CustomMetadata__mdt.getAll().values();
System.assertNotEquals(0, customMetadata.size());
System.assertEquals(4, customMetadata.size());
// Number of SOQL queries: 0 out of 100
// Number of query rows: 0 out of 50000
Apex Custom Metadata methods work the same as Apex Custom Settings methods. It uses the application cache and allows us to access data without the cost of repeated queries to the database. But SOQL approach works differently. While for the custom settings it uses SOQL queries and query rows limits, for the custom metadata it uses only query rows limits. Looks like for the custom metadata salesforce still tries to use application cache but something going wrong...
Please be worried when using Apex Custom Metadata methods. Only the first 255 characters are returned for any field in a custom metadata type record, so longer text fields get truncated. If you want all the field data from a custom metadata type record, use a SOQL query.

4. Framework serialization vs custom apex serialization

When an instance of an Apex class is returned from a server-side action, the instance is serialized to JSON by the framework. But do you know that framework hates NULL-values in lists and remove them? Let's check it.

// testComponent.cmp
<aura:component controller="TestCmpController">
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
</aura:component>

// testComponentController.js
({
    doInit: function(cmp) {
        let action = cmp.get("c.getValues");
        action.setCallback(this, function(response) {
            console.log( response.getReturnValue() );
        });
        $A.enqueueAction(action);
    }
})

// TestCmpController.apex
public with sharing class TestCmpController {
    @AuraEnabled
    public static List<String> getValues() {
        return new List<String> {
            'hello', null, 'world!', null
        };
    }
}
You would be surprised to see list only with two values in browser console:
> (2) ['hello', 'world!']
Let's try to do the same but instead of the framework serialization use custom apex serialization:
// testComponentController.js
({
    doInit: function(cmp) {
        let action = cmp.get("c.getValues");
        action.setCallback(this, function(response) {
            let returnValue = response.getReturnValue();
            console.log( JSON.parse( returnValue ) );
        });
        $A.enqueueAction(action);
    }
})

// TestCmpController.apex
public with sharing class TestCmpController {
    @AuraEnabled
    public static String getValues() {
        List<String> result = new List<String> {
            'hello', null, 'world!', null
        };
        return JSON.serialize(result);
    }
}
No surprise here as we receive result exactly as it is. But why the framework remove null values from list? We may never know the true reason...
> (4) ['hello', null, 'world!', null]

Reveal explanation

Close

As per salesforce support feedback, according to the R&D, this is working as designed. The NULL values are removed from List/Array when passed from Apex controller to Lightning JavaScript controller. To avoid this behaviour please use apex JSON.serialize to pass array/list as serialized string value. Here are the following idea on our Idea Exchange that relate to similar behavior for SObjects. https://ideas.salesforce.com/s/idea/a0B8W00000GdkyaUAB/null-properties-in-object-returned-by-auraenabled-action-in-lightning-component

Please upvote the above idea. The more votes Idea has, the quicker it would be added to the functionality.

5. Internal Salesforce.com Error when use the Messaging.renderStoredEmailTemplate

Be careful when using the Messaging.renderStoredEmailTemplate method in trigger context. Your email can fail without visible errors if you pass the updateEmailTemplateUsage param as TRUE. The internal salesforce.com error is thrown when salesforce tries to send an email after the changes are committed to the database. As result, your records were successfully created but no emails send.

trigger TestTrigger on TestObject__c (after insert) {
    final Id templateId = 'REPLACE_WITH_EMAIL_TEMPLATE_ID';
    final Id whoId = 'REPLACE_WITH_WHO_ID';
    final Id whatId = 'REPLACE_WITH_WHAT_ID';
    
    Messaging.SingleEmailMessage email = Messaging.renderStoredEmailTemplate(
        emailTemplateId,
        whoId,
        whatId,
        Messaging.AttachmentRetrievalOption.METADATA_WITH_BODY,
        true
    );
    email.toAddresses = new String[] { 'REPLACE_WITH_YOUR_EMAIL_ADDRESS' };
    
    Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
}
Please open Developer Console and execute there the next code:
insert new TestObject__c();
Review debug log, it should contain the FATAIL_ERROR row.
13:51:13.225 (2532861779)|CODE_UNIT_FINISHED|TestTrigger on TestObject trigger event AfterInsert|__sfdc_trigger/testTrigger
13:51:13.225 (2549213477)|DML_END|[1]
13:51:13.225 (2549446872)|FATAL_ERROR|Internal Salesforce.com Error
13:51:13.549 (2549478914)|CUMULATIVE_LIMIT_USAGE
13:51:13.549 (2549478914)|LIMIT_USAGE_FOR_NS|(default)|

Reveal explanation

Close

This issue has been identified as a bug at salesforce end and would be fixed soon in the upcoming patches/releases (Safe Harbor). Following is the Known issue link: https://trailblazer.salesforce.com/issues_view?id=a1p4V000002uw1SQAQ

Please click the 'This Issue Affects me' checkbox in article to receive the updates on this issue when it is fixed.



Please feel free to contact me if you have any questions.


Useful links:
Tagged under: Apex Developer

Comments powered by CComment