2

First of all, I am new to Apex. I am trying to create a trigger to do the following: when the user adds or updates an account, the trigger takes the billing address and makes a call to the Google Maps API to automatically get the geolocalization for that address. After getting the geolocalization, the latitude and longitude are added to the database into the custom fields Billing_Latitude__c and Billing_Longitude__c of the account.

To do that, the trigger invokes a public class method, passes to it the address details (street, postal code etc.) so that the Google API call is made, the response JSON is parsed and the latitude and longitude variables are created. Up to this point, I managed to make my code work. But I am having a hard time updating the latitude and longitude custom fields I created.

Here is my trigger:

trigger UpdateGeolocalization on Account (after insert, after update) {

for(Account acct : Trigger.New) { 

    Geolocalization.generateGeolocalization(acct.BillingStreet, acct.BillingCity, acct.BillingState, acct.BillingPostalCode, acct.BillingCountry, 'billing', acct.Id);

  }  
} 

Here is my class with public method:

    public class Geolocalization {

    @future (callout=true)
    public static void generateGeolocalization(String street, String city, String state, String postalCode, String country, String addressType, Id accountId) {

        // Create address string and replace spaces with plus sign
        String address = street+' '+city+' '+state+' '+postalCode+' '+country;
        String addressWithoutSpaces = address.replaceAll('\\s+',' ');
        String addressWithPlusSigns = addressWithoutSpaces.replaceAll(' ','+');

        // Call to Maps API
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://maps.googleapis.com/maps/api/geocode/json?address=' + addressWithPlusSigns + '&key=AIzaSyAuxku5rRASGQgKeAOyj38fjsu45847jf8');
        request.setMethod('GET');
        HttpResponse response = http.send(request);

        // Get latitude and Longitude from response JSON
        Double lat, lng;

        if (response.getStatusCode() == 200) {
            JSONParser parser = JSON.createParser(response.getBody());

            while (parser.nextToken() != null) {
                if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'geometry') {
                    while (parser.nextToken() != JSONToken.END_OBJECT) {
                        if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'location') {
                            while (parser.nextToken() != JSONToken.END_OBJECT) {

                                if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'lat') {
                                    parser.nextToken();
                                    lat = Double.valueOf(parser.getText());
                                }

                                if (parser.getCurrentToken() == JSONToken.FIELD_NAME && parser.getText() == 'lng') {
                                    parser.nextToken();
                                    lng = Double.valueOf(parser.getText());
                                }
                            }
                        }
                    }
                }
            }
        }

        // Convert lat and lng to String
        String latitudeString = String.valueOf(lat.format());
        String longitudeString = String.valueOf(lng.format());

    // Code works up till here. If I system.debug the string latitude variable, it is output correctly when I create a new account.


        // If the type of account is billing, update the custom geolocalization fields. addressType is a string passed as a method parameter.
        if(addressType == 'billing') {

            Account acct = [SELECT Name, Billing_Latitude__c FROM Account WHERE Id = :accountId];

            //Update latitude custom field
            acct.Billing_Latitude__c = latitudeString;

            //Update longitude custom field
            acct.Billing_Longitude__c = longitudeString;

        }

    }

}

Any help will be much appreciated!

  • You could probably save yourself some trouble by using JSON.deserialize() or JSON.deserializeUntyped() instead of trying to parse the JSON in some while loops. – Derek F Sep 24 '19 at 13:08
2

I'm not sure if you simply excluded it for clarity, but I'd add to your trigger conditions for each scenario (if.trigger.isAfter, if(trigger.isInsert) just for future maintainability.

It doesn't seem like you're actually updating the account itself. You're setting the field values, but you need to explicitly include an "update acct" statement.

More than that, I'd make sure you think about the code past "it's working". Potentially adding some exclusions before the callout so you don't always make a callout every update unless the address is not null or has changed (can use newMap vs. oldMap on trigger).

Difference between trigger.new and trigger.newmap

2

The primary issue here is that you aren't performing an update DML on the account you query.

We can only omit the DML if we're:

  1. Working on the record instance(s) contained in trigger.new or trigger.newMap
  2. Doing this in a before trigger (insert or update)
  3. In the same transaction

Using asynchronous processing (@future, Queueable, Batch) is required in your case because callouts aren't allowed to be made in a trigger directly. Unfortunately, that also means that we don't satisfy any of the conditions that allow us to avoid using DML.

So you need to add update acct; in your generateGeolocalization() method to get the changes to your fields to actually stick.

The next problem you'll run into is that this update will cause your Account trigger to run again, and try to make another @future call so you can make another callout. Salesforce doesn't allow you to call a future method as a result of work that's done in an @future context. Adding a check in your trigger to see if the address has changed, and only making your callout if it has is a good thing (and you should do it), but that's not enough to guarantee that you won't run into issues.

Ultimately, I don't think @future is going to work for you. We can only make 50 @future calls per transaction, @future can't be chained, and Google's geocoding API only works on one address per callout. Queueable or Batch apex are probably better suited for this task (since both of them can be chained).

This can also allow you to make more than a single callout in one go, which should help processing speed (and the number of records you can process). A quick example/pseudocode:

// You also need to implement Database.AllowsCallouts to be, well... allowed to make a callout
public class Geolocalization implements Queueable, Database.AllowsCallouts{
    public List<Account> records;

    public Geolocalication(List<Account> input){
        records = input;
    }

    public void execute(QueueableContext ctx){
        List<Account> processed = new List<Account>();

        while processed has less than 100 elements...{
            use remove(0) to get one record from the records list
            (this will also remove it from the records list, which will prevent
              us from operating on the same record twice)

            make your callout

            deserialize results

            set account lat & long

            add account to the processed list
        }

        if there are records remaining in the records list{
            make a new instance of Geolocalization, and pass in the current records list

            enqueue the instance of Geolocalization
        }

        update the processed accounts

    }

}

Summary

  • You need to add update acct;
  • And add some conditions to your trigger so you don't work yourself into an infinite loop
  • Look at using Queueable or Batch apex to handle this

Bonus tip

If you have the Id of an SObject, you don't need to query for it to make an update. The following would work just fine:

// We can specify the record Id in the SObject constructor
// Since this is a constructor, everything here is basically a parameter, and
//   parameters to methods are separated by commas
Account acctToUpdate = new Account(
    Id = acctId,
    Billing_Latitude__c = latitude,
    Billing_Longitude__c = longitude
);

update acctToUpdate;
  • Enjoy the summary and bonus tip heading. +1 – Kris Goncalves Sep 24 '19 at 14:27
  • Thank you for your answer! I am fairly new to Apex and don't fully understand your observations, but I will do some research. At least now I know that the original code would not work. Many thanks! – edbgl Sep 24 '19 at 14:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.