TrailForceTips

Apex Annotations in Salesforce

Annotations in Apex are used to add metadata to code, helping to define the behavior of classes, methods, and properties. They are widely used for customizations and for interacting with the Salesforce runtime.

This article will demonstrate use cases for the most commonly used annotations, with examples for clarity.

1. @AuraEnabled

This annotation exposes Apex methods or properties to Lightning components, allowing them to be invoked directly from Lightning components or Lightning Web Components (LWC).

Use Case: Fetching Account Records Based on Industry

Imagine a Lightning component that displays a list of accounts filtered by the industry selected by the user.

Apex Class:

public with sharing class AccountService {
@AuraEnabled
public static List<Account> getAccountsByIndustry(String industry) {
return [SELECT Id, Name, Industry FROM Account WHERE Industry = :industry];
}
}

Lightning Component (Aura):

<aura:component controller="AccountService" implements="force:appHostable">
<aura:attribute name="industry" type="String" />
<aura:attribute name="accounts" type="Account[]" />

<lightning:input label="Industry" value="{!v.industry}" onchange="{!c.fetchAccounts}" />

<aura:iteration items="{!v.accounts}" var="account">
<p>{!account.Name} - {!account.Industry}</p>
</aura:iteration>
</aura:component>

JavaScript Controller:

({
fetchAccounts: function (component, event, helper) {
const industry = component.get("v.industry");
const action = component.get("c.getAccountsByIndustry");
action.setParams({ industry: industry });

action.setCallback(this, function (response) {
if (response.getState() === "SUCCESS") {
component.set("v.accounts", response.getReturnValue());
} else {
console.error("Error fetching accounts: " + response.getError());
}
});

$A.enqueueAction(action);
}
});

2. @Future

Used to execute methods asynchronously in a separate thread, ideal for integrations or long-running operations.

Use Case: Integration with an External System

Consider a scenario where account data must be sent to an external system whenever a new account is created.

Integration Service Class:

public with sharing class IntegrationService {
@Future(callout=true)
public static void sendAccountData(String accountId) {
Account acc = [SELECT Id, Name, Industry FROM Account WHERE Id = :accountId LIMIT 1];

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.externalsystem.com/account');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"id":"' + acc.Id + '", "name":"' + acc.Name + '", "industry":"' + acc.Industry + '"}');

Http http = new Http();
HttpResponse res = http.send(req);

System.debug('Response: ' + res.getBody());
}
}

Trigger to Invoke the Integration:

trigger AccountTrigger on Account (after insert) {
for (Account acc : Trigger.new) {
IntegrationService.sendAccountData(acc.Id);
}
}

3. @InvocableMethod

Allows a method to be called from automation tools like Flows or Process Builder.

Use Case: Sending a Greeting Message

A method that sends personalized greeting messages based on names provided by a Flow.

Apex Class:

public with sharing class GreetingService {
@InvocableMethod(label='Send Greeting' description='Sends a greeting message.')
public static void sendGreeting(List<String> names) {
for (String name : names) {
System.debug('Hello, ' + name + '!');
}
}
}

Calling the Method in a Flow:

  1. Navigate to Setup > Flow Builder.
  2. Create a new Flow (e.g., Record-Triggered Flow).
  3. Add an Action element and select the Apex class containing the @InvocableMethod.
  4. Provide the input parameters required by the method (e.g., a list of names).
  5. Save and activate the Flow.

4. @IsTest

Indicates that a class or method is used exclusively for testing purposes.

Use Case: Testing a Trigger with Dependencies

Imagine a trigger that creates a task whenever a new account is created. The test needs to validate that the behavior works correctly.

Service Class:

public class TaskService {
public virtual void createTask(Id accountId) {
Task t = new Task(Subject = 'Follow-up', WhatId = accountId);
insert t;
}
}

public class AccountTriggerHandler {
public static void handleAfterInsert(List<Account> newAccounts) {
TaskService service = new TaskService();
for (Account acc : newAccounts) {
service.createTask(acc.Id);
}
}
}

Trigger:

trigger AccountTrigger on Account (after insert) {
AccountTriggerHandler.handleAfterInsert(Trigger.new);
}

Test Class:

@IsTest
public class AccountTriggerTest {
public class MockTaskService extends TaskService {
public override void createTask(Id accountId) {
System.debug('MockTaskService called with Account Id: ' + accountId);
}
}

@TestSetup
public static void setupData() {
Account acc = new Account(Name = 'Test Account');
insert acc;
}

@IsTest
public static void testAccountTrigger() {
Test.startTest();
Account acc = new Account(Name = 'Another Test Account');
insert acc;
Test.stopTest();

List<Task> tasks = [SELECT Id, Subject FROM Task WHERE WhatId = :acc.Id];
System.assertEquals(1, tasks.size(), 'A task should be created for the account.');
}
}

5. @WithSharing / @WithoutSharing

Defines the sharing context for a class, influencing how record access permissions are applied.

Use Case: Controlled Access Based on User Permissions

  • @WithSharing: Use for user-facing applications where record access must respect the user’s sharing rules.
  • @WithoutSharing: Use for administrative processes or integrations that require unrestricted access to records.

Example:

// Class respecting sharing rules
public with sharing class SharedClass {
public void displayAccounts() {
List<Account> accounts = [SELECT Id, Name FROM Account];
System.debug(accounts);
}
}

// Class ignoring sharing rules
public without sharing class NonSharedClass {
public void displayAllAccounts() {
List<Account> accounts = [SELECT Id, Name FROM Account];
System.debug(accounts);
}
}

6. @ReadOnly

Marks a method or property as read-only.

Use Case: A Method That Just Fetches Data

public with sharing class AccountService {
@ReadOnly
public static List<Account> getAllAccounts() {
return [SELECT Id, Name FROM Account];
}
}

7. @SuppressWarnings

Suppresses specific compiler warnings.

Use Case: Ignoring Unused Variable Warnings

public with sharing class SuppressWarningsExample {
@SuppressWarnings('unused')
public static void exampleMethod() {
Integer unusedVar = 0; // No warning generated for unused variable
}
}

8. @Deprecated

Marks a class or method as obsolete.

Use Case: Deprecating an Old Method

public with sharing class OldService {
@Deprecated
public static void oldMethod() {
// This method is no longer in use
}
}

9. @TestSetup

Used to define setup methods that run once before the tests are executed.

Use Case: Setting Up Test Data

public class AccountTestSetup {
@TestSetup
public static void setupTestData() {
Account acc = new Account(Name = 'Test Account');
insert acc;
}
}

10. @SeeAllData

Allows tests to access real data in the org.

Use Case: Accessing Real Data in Tests

@IsTest
@SeeAllData=true
public class RealDataTest {
@IsTest
public static void testUsingRealData() {
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 1];
System.assertNotEquals(0, accounts.size());
}
}

Final Considerations

Annotations are powerful tools in Apex and should be used appropriately to ensure code efficiency and maintainability. Always consider the implications of each annotation before implementation.