There are two main relationship types in Salesforce Lookup Relationship and Master-Detail relationships. There are some differences betweent these relationships:
| Lookup Relationship | Master-Detail Relationship |
|---|---|
| up to 25 per one object | up to 2 per one object |
| parent is not a required field | parent field on a child is required |
| deleting a parent does not delete a child | deleting a parent automatically deletes a child |
| can be multiple layers deep | a child of one master detail relationship cannot be the parent of another one |
| no impact on a security and access | access to a parent determines access to a children |
| - | the standard object cannot be on the detail side of a relationship with a custom object |
Account (Parent), Contact (Child) - Lookup field is on the Contact standard object | One Account can have 0 or many Contacts | One Contact can have 0 or one Account.
Child-to-parent queries:
SELECT Name, Phone, Account.Id, Account.Name FROM ContactSELECT Name, Phone, Account.Id, Account.Name FROM Contact WHERE Account.Id = '0010900000iMM6GAAW'Parent-to-child queries:
SELECT Id, Name, (SELECT Name, Phone FROM Contacts) FROM AccountSELECT Id, Name, (SELECT Name, Phone FROM Contacts) FROM Account WHERE Id = '0010900000iMM6DAAW'Vehicle (Parent), Driver (Child) - Lookup field is on the Driver custom object | One Vehicle can have 0 or many Drivers | One Driver can have 0 or one Vehicle
Child-to-parent queries:
SELECT Name, Nationality__c, Vehicle__r.Id, Vehicle__r.Manufacturer__c FROM Driver__cSELECT Name, Nationality__c, Vehicle__r.Id, Vehicle__r.Manufacturer__c FROM Driver__c WHERE Vehicle__r.Id = 'a000900000FThKjAAL'
Parent-to-child queries:
SELECT Id, Name, Manufacturer__c, (SELECT Name, Nationality__c FROM Drivers__r) FROM Vehicle__cSELECT Id, Name, Manufacturer__c, (SELECT Name, Nationality__c FROM Drivers__r) FROM Vehicle__c WHERE Id = 'a000900000FThKjAAL'Notice that it is pretty important to use __c suffix when you are using/querying custom object and __r suffix when you want to query fields of realated object. Also remember to use plural form in the second SELECT statement when you use parent-to-child queries.
The Master-Detail queries look exactly the same as for the Lookup.
Account (Parent/Master), Entitlement (Child/Detail) - Master-detail field is on the Entitlement standard object | One Account can have 0 or many Entitlements | One Entitlement must have exactly one Account.
Child-to-parent queries:
SELECT Name, Status, Account.Id, Account.Name FROM EntitlementSELECT Name, Status, Account.Id, Account.Name FROM Entitlement WHERE Account.Id = '0010900000g6FkaAAE'Parent-to-child queries:
SELECT Id, Name, (SELECT Name, Status FROM Entitlements) FROM AccountSELECT Id, Name, (SELECT Name, Status FROM Entitlements) FROM Account WHERE Id = '0010900000iMM6FAAW'The idea of Apex batch jobs is to work on the huge number (thousands / millions) of records. Apex Batch class has to implement Database.Batchable interface and its 3 methods:
- start (pre-processing operations) - here the records are being collected
- execute - here some operations are performed on the provided records
- finish (post-processing operations) - this method is called once all operations are done
Here is a pretty simple implementation of this interface. This batch just deletes every Opportunity with the stage name 'Closed Won' or 'Closed Lost'.
public class OpportunitiesCleanerBatch implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext context) {
System.debug('Job started!');
return Database.getQueryLocator([SELECT StageName FROM Opportunity]);
}
public void execute(Database.BatchableContext context, List<Opportunity> opportunities) {
List<Opportunity> opportunitiesToRemove = new List<Opportunity>();
for (Opportunity o : opportunities) {
if (o.StageName == 'Closed Won' || o.StageName == 'Closed Lost') {
opportunitiesToRemove.add(o);
}
}
delete opportunitiesToRemove;
System.debug(
String.format(
'{0} opportunities with the \"{1}\" or \"{2}\" stage name have been deleted.',
new List<String> { String.valueOf(opportunitiesToRemove.size()), 'Closed Won', 'Closed Lost' }
)
);
}
public void finish(Database.BatchableContext context) {
System.debug('Job finished!');
}
}Here is the code that allows to run the batch above from Developer Console or within another class:
OpportunitiesCleanerBatch oppCleanerBatch = new OpportunitiesCleanerBatch();
Database.executeBatch(oppCleanerBatch);The interface, that works really good with batches is Schedulable interface. It allows to schedule an instance of the Apex class to run at a specific time.
Here is implementation of Schedulable interface that runs OpportunitiesCleanerBatch.
public class OpportunitiesCleanerBatchSchedule implements Schedulable {
public void execute(SchedulableContext context) {
OpportunitiesCleanerBatch oppCleanerBatch = new OpportunitiesCleanerBatch();
Database.executeBatch(oppCleanerBatch);
}
}There are two possibilities to schedule a job. Obviously you can run the code from the Developer Console or within another class.
OpportunitiesCleanerBatchSchedule scheduledBatch = new OpportunitiesCleanerBatchSchedule();
// String sch = 'SECONDS MINUTES HOURS DAY_OF_MONTH MONTH DAY_OF_WEEK OPTIONAL_YEAR';
String sch = '00 45 6-22 ? * * *';
System.schedule('Opportunities Cleaner Batch', sch, scheduledBatch);You can also use UI to do this: Setup -> Apex Jobs -> Apex Classes -> Schedule Apex. There you have to add job name, select proper class and choose specific time and how often you want the job to run.
As the Salesforce documentation says:
Each execution of a batch Apex job is considered a discrete transaction.
Implementing Database.Stateful interface in the case of the batches allows to maintain state across the transactions.
Example:
Lets say that we have 1028 Case records in our Salesforce database. We have written a simple batch (this batch does nothing - I just want this example to be as simple as possible). This is how does it looks like:
public class CasesBatch implements Database.Batchable<sObject> {
private Integer count = 0;
public Database.QueryLocator start(Database.BatchableContext context){
System.debug('count (start): ' + count);
String exampleQuery = 'SELECT Id FROM Case';
return Database.getQueryLocator(exampleQuery);
}
public void execute(Database.BatchableContext context, List<sObject> scope){
this.count++;
System.debug('scope.size(): ' + scope.size());
System.debug('count (execute): ' + count);
}
public void finish(Database.BatchableContext BC){
System.debug('count (finish): ' + count);
}
}Lest run our batch using the code below:
Database.executeBatch(new CasesBatch());We have got couple of logs:
#1
19:29:03:020 USER_DEBUG [5]|DEBUG|count (start): 0
#2
19:29:03:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:29:03:001 USER_DEBUG [13]|DEBUG|count (execute): 1
#3
19:29:03:002 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:29:03:002 USER_DEBUG [13]|DEBUG|count (execute): 1
#4
19:29:04:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:29:04:001 USER_DEBUG [13]|DEBUG|count (execute): 1
#5
19:29:04:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:29:04:001 USER_DEBUG [13]|DEBUG|count (execute): 1
#6
19:29:04:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:29:04:001 USER_DEBUG [13]|DEBUG|count (execute): 1
#7
19:29:04:001 USER_DEBUG [12]|DEBUG|scope.size(): 28
19:29:04:001 USER_DEBUG [13]|DEBUG|count (execute): 1
#8
19:29:04:025 USER_DEBUG [17]|DEBUG|count (finish): 0Lets implement Database.Stateful interface now:
public class CasesBatch implements Database.Batchable<sObject>, Database.Stateful {
private Integer count = 0;
public Database.QueryLocator start(Database.BatchableContext context){
System.debug('count (start): ' + count);
String exampleQuery = 'SELECT Id FROM Case';
return Database.getQueryLocator(exampleQuery);
}
public void execute(Database.BatchableContext context, List<sObject> scope){
this.count++;
System.debug('scope.size(): ' + scope.size());
System.debug('count (execute): ' + count);
}
public void finish(Database.BatchableContext BC){
System.debug('count (finish): ' + count);
}
}Lets run the batch again and check the logs:
#1
19:35:15:019 USER_DEBUG [5]|DEBUG|count (start): 0
#2
19:35:16:002 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:35:16:002 USER_DEBUG [13]|DEBUG|count (execute): 1
#3
19:35:16:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:35:16:001 USER_DEBUG [13]|DEBUG|count (execute): 2
#4
19:35:16:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:35:16:001 USER_DEBUG [13]|DEBUG|count (execute): 3
#5
19:35:16:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:35:16:001 USER_DEBUG [13]|DEBUG|count (execute): 4
#6
19:35:16:001 USER_DEBUG [12]|DEBUG|scope.size(): 200
19:35:16:001 USER_DEBUG [13]|DEBUG|count (execute): 5
#7
19:35:16:001 USER_DEBUG [12]|DEBUG|scope.size(): 28
19:35:16:001 USER_DEBUG [13]|DEBUG|count (execute): 6
#8
19:35:16:020 USER_DEBUG [17]|DEBUG|count (finish): 6
- Send an email + debug.
String subject = 'Email Subject.';
String body = 'Email Body.';
String[] addresses = new String[] { 'test@mail.com' };
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(addresses);
mail.setSubject(subject);
mail.setPlainTextBody(body);
Messaging.SendEmailResult[] results = Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
for (Messaging.SendEmailResult r : results) {
System.debug(r);
}- Get picklist values for picklist field (StageName field values of Opportunity standard object in this case).
DescribeFieldResult stageNameField = Opportunity.StageName.getDescribe();
for (Integer i = 0; i < stageNameField.getPicklistValues().size(); i++) {
System.debug(String.format(' StageName {0}: {1}', new List<String> { String.valueOf(i), stageNameField.getPicklistValues()[i].value }));
}- Run batch manually using Anonymous Apex.
ExampleBatch batch = new ExampleBatch();
Database.executeBatch(batch);- Schedule a job using Anonymous Apex.
ExampleBatchSchedule scheduledBatch = new ExampleBatchSchedule();
// String sch = 'SECONDS MINUTES HOURS DAY_OF_MONTH MONTH DAY_OF_WEEK OPTIONAL_YEAR';
String sch = '00 45 6-22 ? * * *';
System.schedule('Example Batch', sch, scheduledBatch);- Easy way to get set of object Ids.
List<SObject> opportunities = [SELECT Id, Name FROM Opportunity LIMIT 10];
Set<Id> opportunitiesIds = (new Map<Id, SObject>(opportunities)).keySet(); Below you can find an Aura Component which is using Apex Controller to read and display data. It doesn't look so good, but it is one of the simplest examples. In this case data loads after clicking the Get Opportunities button.
<aura:component implements="flexipage:availableForAllPageTypes" controller="OpportunitiesController">
<aura:attribute name="opportunities" type="Opportunity[]"/>
<!-- We are calling component controller method here (NOT Apex method!) -->
<button onclick="{!c.getOpportunities}">Get Opportunities</button>
<p>Opportunities list:</p>
<ul>
<aura:iteration var="opportunity" items="{!v.opportunities}">
<li>Id: {!opportunity.Id}, Name: {!opportunity.Name}, StageName: {!opportunity.StageName}</li>
</aura:iteration>
</ul>
</aura:component>({
getOpportunities: function(component, event, helper){
// We are calling Apex controller method here.
var action = component.get("c.getOpportunitiesList");
action.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
component.set("v.opportunities", response.getReturnValue());
}
});
$A.enqueueAction(action);
}
})public with sharing class OpportunitiesController {
@AuraEnabled
public static List<Opportunity> getOpportunitiesList() {
return [SELECT Id, Name, StageName, CreatedDate
FROM Opportunity
ORDER BY CreatedDate DESC
LIMIT 10];
}
}Of course we can use datatable to achieve much better appearance. In this case there is no need to click the button to load data.
<aura:component implements="flexipage:availableForAllPageTypes,force:hasRecordId" controller="OpportunitiesController" access="global">
<aura:attribute name="columns" type="list"/>
<aura:attribute name="opportunities" type="Opportunity[]"/>
<!-- We are calling component controller method here (NOT Apex method!) -->
<aura:handler name='init' action="{!c.getOpportunities}" value="{!this}"/>
<!-- <button onclick="{!c.getOpportunities}">Get Opportunities</button> -->
<lightning:card title="Opportunities List">
<lightning:datatable keyField="id"
data="{!v.opportunities}"
columns="{!v.columns}"
hideCheckboxColumn="true"
maxColumnWidth="1000"
minColumnWidth="150"/>
</lightning:card>
</aura:component>({
getOpportunities: function (component, event, helper) {
component.set('v.columns', [
{label: 'Id', fieldName: 'Id', type: 'text'},
{label: 'Name', fieldName: 'Name', type: 'text'},
{label: 'Stage Name', fieldName: 'StageName', type: 'text'},
{label: 'Creation Date', fieldName: 'CreatedDate', type: 'date'},
]);
// We are calling Apex controller method here.
var action = component.get("c.getOpportunitiesList");
action.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
component.set("v.opportunities", response.getReturnValue());
}
});
$A.enqueueAction(action);
}
})What is Apex?
As the Salesforce documentation says:
Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on Salesforce servers in conjunction with calls to the API
It is just an OOP language that is pretty similar to Java or C#.
Below you can find some informations about classes and interfaces:
| Virtual | Abstract | Interface | |
|---|---|---|---|
| implementation | full class implementation | partial class implementation | it's just a "contract" |
| keyword | extends | extends | implements |
| can have variables and properties | yes | yes | no |
| can have defined methods | yes | yes | no |
| can be instantiated (directly) | yes | no | no |
| can have abstract methods | no | yes | yes |
Here is really simple example, how this all works together in Apex:
public interface Tuningable {
// Interface methods cannot have a body.
void tuning();
}// This class is abstract so it cannot be instantiated. It can be extended.
public abstract class Vehicle {
// Automatic property - like in C#.
public String Name { get; set; }
public String Color { get; set; }
public Vehicle(String name, String color) {
this.Name = name;
this.Color = color;
}
// Abstract method that has to be implemented by the subclass. Abstract methods have no body.
public abstract Integer getMaxSpeed();
// Virtual method can (but not has to) be overridden by the subclass.
public virtual String getInfo() {
return String.format('I am a {0} {1}.', new List<String> { this.Color, this.Name });
}
}// This is a virtual class so it can be extended by other classes. It also can be instantiated.
public class BaseCar extends Vehicle {
public Integer MaxSpeed { get; set; }
// We can call super class constructor by using 'super' keyword - like in Java.
public BaseCar(String name, String color, Integer maxSpeed) {
super(name, color);
this.MaxSpeed = maxSpeed;
}
public override Integer getMaxSpeed() {
return this.MaxSpeed;
}
// We can call super class method by using 'super' keyword - like in Java.
// We have to use 'override' keyword if we want to override superclass method.
public override String getInfo() {
return String.format('{0} My max speed is {1} km/h.',
new List<String> { super.getInfo(), String.valueOf(this.MaxSpeed) });
}
}// We cannot extend this class. We can extend class only if it is abstract or virtual.
public class SportsCar extends BaseCar implements Tuningable {
public Boolean HasTurbo { get; set; }
public SportsCar(String name, String color, Integer maxSpeed) {
super(name, color, maxSpeed);
}
// We can call another constructor by using 'this' keyword - like in Java.
public SportsCar(String name, String color, Integer maxSpeed, Boolean hasTurbo) {
this(name, color, maxSpeed);
this.hasTurbo = hasTurbo;
}
// We do not have to (and even cannot) use 'override' keyword if we are implementing interface method.
public void tuning() {
maxSpeed += 10;
}
}SportsCar car = new SportsCar('Nissan', 'Blue', 100, true);
car.tuning();
System.debug(car.getInfo()); // 20:48:36:017 USER_DEBUG [70]|DEBUG|I am a Blue Nissan. My max speed is 110 km/h.What are the Apex Governor Limits?
As the Salesforce documentation says:
Because Apex runs in a multitenant environment, the Apex runtime engine strictly enforces limits so that runaway Apex code or processes don’t monopolize shared resources.
Below you can find one of the most common limits in Salesforce (the table is not complete):
| Description | Synchronous Limit | Asynchronous Limit |
|---|---|---|
| number of SOQL queries | 100 | 200 |
| number of records retrieved by SOQL queries | 50,000 | 50,000 |
| number of records retrieved by Database.getQueryLocator | 10,000 | 10,000 |
| number of SOSL queries | 20 | 20 |
| number of records retrieved by a single SOSL query | 2,000 | 2,000 |
| number of DML statements issued | 150 | 150 |
| number of records processed as a result of DML statements | 10,000 | 10,000 |
| heap size | 6 MB | 12 MB |
| maximum CPU time on the Salesforce servers | 10,000 ms | 60,000 ms |
Exceeding the limit will cause an exceptions to occur. Some of examples:
Too many SOQL queries (synchronous limit):
// 'System.LimitException: Too many SOQL queries: 101' will be thrown because max number of (synchronous) SOQL queries is equal to 100.
static void queryCases() {
for (Integer i = 0; i < 101; i++) {
Case c = [SELECT Id FROM Case];
}
}Too many SOQL queries (asynchronous limit):
// 'System.LimitException: Too many SOQL queries: 201' will be thrown because max number of (asynchronous) SOQL queries is equal to 200.
@future
static void queryCases() {
for (Integer i = 0; i < 201; i++) {
Case c = [SELECT Id FROM Case];
}
}Too many DML statements:
// 'System.LimitException: Too many DML statements: 151' will be thrown because max number of DML statements is equal to 150.
static void insertCases() {
for (Integer i = 0; i < 151; i++) {
insert new Case();
}
}Too many SOSL queries:
// 'System.LimitException: Too many SOSL queries: 21' will be thrown because max number of SOSL queries is equal to 20.
static void queryCases() {
for (Integer i = 0; i < 21; i++) {
List<List<sObject>> sObjects = [FIND 'New' IN ALL FIELDS RETURNING Case(Status)];
}
}Too many DML rows:
// 'System.LimitException: Too many DML rows: 10001' will be thrown because max number of records processed as a result of DML statements is equal to 10 000.
static void insertCases() {
List<Case> cases = new List<Case>();
for (Integer i = 0; i < 10001; i++) {
cases.add(new Case());
}
insert cases;
}Apex Trigger is a code that executes before or after any operations are performed on the specific record.
Below you can find an example of pretty simple trigger that updates Vehicle name by appending current user name to it.
trigger VehicleTrigger on Vehicle__c (before insert) {
for (Vehicle__c vehicle : Trigger.New) {
vehicle.Name += ' of ' + System.UserInfo.getUserName();
}
}Possibly types of operations are:
- insert
- update
- delete
- merge
- upsert
- undelete
There are some Trigger context variables that allow you to check which operation fired the trigger or if it was fired before or after the records were saved:
| Variable | Returns |
|---|---|
| isInsert | true if the trigger was fired due to an insert operation |
| isUpdate | true if the trigger was fired due to an update operation |
| isDelete | true if the trigger was fired due to a delete operation |
| isUndelete | true if the trigger was fired after a record is recovered from the Recycle Bin |
| isBefore | true if the trigger was fired before any record was saved |
| isAfter | true if this trigger was fired after all records were saved |
There are also some variables, that allow you to access the records:
| Variable | Returns | Records available in |
|---|---|---|
| new | a list of the new versions of the sObject records | insert, update, undelete (and can be modified only before) |
| newMap | a map of IDs to the new versions of the sObject records | before update, after insert, after update, after undelete |
| old | a list of the old versions of the sObject records | update, delete |
| oldMap | a map of IDs to the old versions of the sObject records | update and delete |
Since Queues can be the owners of the records, we can take advantage of this fact and only search for records that are owned by the Queue to which the user is assigned.
String currentUserId = UserInfo.getUserId();
// Get GroupMember records related to the current User (where the Group Type is Queue).
List<GroupMember> groupMembers = [SELECT GroupId
FROM GroupMember
WHERE Group.Type = 'Queue'];
// Create set of the Queues Ids.
Set<Id> currentUserQueuesIds = new Set<Id>();
for (GroupMember member : groupMembers) {
currentUserQueuesIds.add(member.GroupId);
}
// Select records where the Queue related to the current User is the Owner.
List<Case> cases = [SELECT Id, Owner.Name
FROM Case
WHERE OwnerId IN :currentUserQueuesIds];As the Salesforce documentation says Schema class:
Contains methods for obtaining schema describe information.
Useful examples:
Get metadata information about custom apps.
Schema.DescribeTabSetResult[] results = Schema.describeTabs();
for (Schema.DescribeTabSetResult result : results) {
System.debug('Label: ' + result.getLabel());
}03:36:27:072 USER_DEBUG [4]|DEBUG|Label: Sales
03:36:27:072 USER_DEBUG [4]|DEBUG|Label: Service
03:36:27:072 USER_DEBUG [4]|DEBUG|Label: MarketingGet sObjects names.
Map<String, Schema.SObjectType> sObjectsMap = Schema.getGlobalDescribe();
for (String key : sObjectsMap.keySet()) {
Schema.DescribeSObjectResult result = sObjectsMap.get(key).getDescribe();
System.debug('SObject name: ' + result.getName());
}04:05:09:071 USER_DEBUG [5]|DEBUG|SObject name: OpportunityStage
04:05:09:071 USER_DEBUG [5]|DEBUG|SObject name: LeadStatus
04:05:09:072 USER_DEBUG [5]|DEBUG|SObject name: CaseStatusGet picklist entries.
Schema.DescribeFieldResult describeFieldResult = Account.Industry.getDescribe();
List<Schema.PicklistEntry> picklistEntries = describeFieldResult.getPicklistValues();
for (Schema.PicklistEntry picklistEntry : picklistEntries) {
System.debug('Picklist entry: ' + picklistEntry);
}10:38:41:007 USER_DEBUG [5]|DEBUG|Picklist entry: Schema.PicklistEntry[getLabel=Consulting;getValue=Consulting;isActive=true;isDefaultValue=false;]
10:38:41:007 USER_DEBUG [5]|DEBUG|Picklist entry: Schema.PicklistEntry[getLabel=Education;getValue=Education;isActive=true;isDefaultValue=false;]
10:38:41:007 USER_DEBUG [5]|DEBUG|Picklist entry: Schema.PicklistEntry[getLabel=Electronics;getValue=Electronics;isActive=true;isDefaultValue=false;]Get type of sObject.
Schema.DescribeSObjectResult describeResult = Account.sObjectType.getDescribe();
System.debug('SObject type: ' + describeResult.getSObjectType());11:08:36:015 USER_DEBUG [2]|DEBUG|SObject type: AccountPrimitive data types in Apex.
| Data Type | Description | Usage example |
|---|---|---|
| Blob | - | String myString = 'StringToBlob'; Blob myBlob = Blob.valueof(myString); System.assertEquals('StringToBlob', myBlob.toString()); |
| Boolean | - | Boolean isValid = true; |
| Date | - | Date myDate = Date.newInstance(2022, 2, 18); // 2022-02-18 00:00:00 |
| DateTime | - | DateTime myDateTime = DateTime.newInstance(1999, 2, 11, 8, 6, 16); // 2022-02-18 13:29:15 |
| Decimal | - | Decimal phi = 1.618033; |
| Double | - | Double pi = 3.14159; |
| Id | - | 0017Q000008Yo6JQAS |
| Integer | - | Integer count = 15; |
| Long | - | Long amount = 1337; |
| Object | - | Object color = 'Red'; |
| String | - | String name = 'Adrian'; |
| Time | - | Time myTime = Time.newInstance(13, 47, 35, 570); // 13:47:35.570Z |





