Salesforce Development
Table of Contents
- 1. Salesforce Platform Basics
- 2. Salesforce Architecture
- 3. Navigate Setup
- 4. App Exchange
- 5. Create a Trailhead playground
- 6. Platform Development Basics
- 7. Data Modelling
- 8. Data Management
- 9. Formulas and Validations
- 10. Salesforce DX
- 11. Lightning Flow
- 12. Apex Basics and Database
- 12.1. Get Started with Apex
- 12.2. Use sObjects
- 12.3. sObject and Field Names
- 12.4. Creating sObjects and adding Fields
- 12.5. Working with the Generic sObject Type
- 12.6. Casting Generic sObjects to Specific sObject Types
- 12.7. Manipulate Records with DML
- 12.8. DML Statement Exceptions
- 12.9. Database Methods
- 12.10. Write SOQL Queries
- 12.10.1. Use the Query Editor
- 12.10.2. Basic SOQL Syntax
- 12.10.3. Filtering Query Results with Conditions
- 12.10.4. Ordering Query Results
- 12.10.5. Limit the Number of Records Returned
- 12.10.6. Combining all the above operators
- 12.10.7. Accessing Variables in SOQL Queries
- 12.10.8. Querying Relatede Records
- 12.10.9. Dot Notation
- 12.10.10. Querying Record in Batches by Using SOQL For Loops
- 12.11. When to use SOSL instead of SOQL?
- 12.12. Write SOSL Queries
- 13. Apex Triggers
- 13.1. Get Started with Apex Triggers
- 13.1.1. Trigger Syntax
- 13.1.2. Example of a Trigger
- 13.1.3. Types of Triggers
- 13.1.4. Using Context Variables
- 13.1.5. Trigger context Variables
- 13.1.6. Calling a Class Method from a Trigger
- 13.1.7. Adding Related Records
- 13.1.8. Use Trigger Exceptions
- 13.1.9. Triggers and Callouts
- 13.1.10. Unit Challenge
- 13.2. Bulk Apex Triggers
- 13.1. Get Started with Apex Triggers
- 14. Apex Testing
- 15. Visualforce Basics
- 15.1. Where you can use Visualforce
- 15.1.1. Open a Visualforce Page from the App Launcher
- 15.1.2. Add a Visualforce Page to the Navigation Bar
- 15.1.3. Display a Visualforce Page within a Standard Page Layout
- 15.1.4. Add a Visualforce Page as a Component in the Lightning App Builder
- 15.1.5. Lanunch A Visualforce Page as a Quick Action
- 15.1.6. Display a Visualforce Page by Overriding Standard Buttons or Links
- 15.1.7. Display a Visualforce Page using Custom Buttons or Links
- 15.2. Create and Edit Visualforce Pages
- 15.3. Use Simple Variables and Formulas
- 15.4. Use Standard Controllers
- 15.5. Display Records, Fields and Tables
- 15.6. Input Data Using Forms
- 15.6.1. Introduction to Visualforce forms
- 15.6.2. Create a Basic form
- 15.6.3. Add Field Labels and Platform Styling
- 15.6.4. Display Form Errors and Messages
- 15.6.5. Edit Related Records
- 15.6.6. Unit Challenge
- 15.6.7. Use Standard List Controllers
- 15.6.8. Introduction to the Standard List Controller
- 15.6.9. Display a List of Records
- 15.6.10. Add List View Filtering to the List
- 15.6.11. Add Pagination to the List
- 15.6.12. Unit Challenge
- 15.7. Use Static Resources
- 15.8. Create And Use Custom Controllers
- 15.1. Where you can use Visualforce
- 16. Developer Console
- 17. Optimizing Search
- 18. Apex
## #+HTMLHEADEXTRA: <style>*{font-family: sans-serif}</style>
Areas covered:
1 Salesforce Platform Basics
- define the sf platform
- describe the scenario provided (Dreamhouse)
- Create a playground
- Difference between declarative and programmatic development
Object is an important term in sf.
An app is a set of objects, fields and other functionality that supports a business process. Objects are tables in the sf database. There are standard obejects and custom objects. Records are rows in the object database. Fields are columns in the object database.
Org is short for organisation and refers to a specific instance of sf.
To create a new field:
- go to setup
- click on the object manager tab
- click the object that you want to create a field for
- under Fields and Relationships, click new
- choose what type of field you want
- give it a name (label)
- save.
2 Salesforce Architecture
Sf is a cloud company. Everything is integrated. It is a multi-tenated platform. It uses metadata. This is the structure of your data.
Then there are APIs. This helps abstract away a lot of the work somewhere else. You can even take it to Minecraft!
3 Navigate Setup
This is where most of the work will be done. As well as design and data structure, you can administer users here. The quick find box is your friend.
- Administration (users)
- Platform tools (customization)
- Settings (org information and security)
4 App Exchange
Installing apps on sf is more complicated than installing apps on your phone.
5 Create a Trailhead playground
This is similar to the Developer Edition, but specifically for Trailhead. It comes with data and pre-installed packages.
To create a playground:
- click the name of your org
- give your playground a name
- click create
5.1 Get your Trailhead Playground Username and Password
- go to setup
- Enter Users in Quick Find
- Locate your name
- Click Reset Password
- Click the link in the email sent
- Enter a new password, confirm it and click Change Password
5.2 Install Apps
You need the package id (which is a long string that starts with '04t').
To install a package:
- copy the package installation link
- paste it into a new browser tab (if you have an incognito tab, you'll have to log in)
- select 'Install for Admins Only'
- click install
You'll get a confirmation page once it has installed.
6 Platform Development Basics
6.1 Getting Started With Platform Development
You get to build your own functionality. You get to create custom data models and applications for desktop and mobile.
This uses Heroku. This is highly scalable for front-end and back-end. You get the database tools you need.
You get access to APIs. With these you can integrate and connect all enterprise data, networks, and identity information.
6.2 Develop without code
You're not just looking at a user interface sat on top of a relational database. You are looking at an abstraction of the database that's driven by the platform's metadata-aware architecture.
Objects are our database tables. Fields are the columns and records are rows. You can create custom objects for yourself.
Because the platform is metadata-aware, you can build things quicker, as Sf will auto-generate a significant part of the user experience.
You can see in the Schema Builder the relationships between the objects.
6.2.1 No-code and Low Code Development
You are encouraged to minimize your code. You get to use point and click declarative code.
There are other tasks that are low-code - this means that you can design something with a gui that has logic flow (e.g. Process Builder).
6.3 Code with SF languages
Lightning Component Framework is a UI development framework similar to AngularJS or React. Apex is a proprietary programming language similar to Java. Visualforce is a markup language that lets you create custom salesforce pages with code that looks a lot like HTML, and use Apex and Javascript.
6.4 Extend the SF Platform
Put simply, every object in your orhg has an API name that lets you access data for that object. You can use SOQL query to gain access to this data:
Property__c property = [SELECT Name, Price__c from Property__c WHERE Id=:propId];
The _c denotes that this is a custom object or field.
6.5 IoT
You can connect smart devices with SF.
6.6 Bots
Chatbots can be built right in to your org to help your employees navigate.
7 Data Modelling
7.1 Understand Custom & Standard Objects
Standard objects are objects that are included with SF, which are common to all businesses.
Custom Objects are objects that you create to store information that's specific to your company or industry.
Objects are containers for your data, but they also give functionality. You get stuff like page layout automatically.
7.1.1 Create a Custom Object
- click setup
- click object manager tab
- click Create | Custom object
- for Label enter the label required
- choose a plural label
- check the box for Launch New Custom Tab Wizard after saving this object
- change any values as required
- on the New Custom Object Tab page click the Tab Style field and choose a style you like. The style sets the icon to display in the UI for the object
- click next, next, save
7.1.2 Create a Custom Field
- from setup go to Object Manager
- choose the object that you wish to add a field for
- click Fields and Relationships in the right pane
- click New
- click the data type required
- click next
- provide a field label
- provide a description
- check the required box (to make this field mandatory)
- click next, next save
7.1.3 Create a Record
- choose the object that you'd like to create a new record for (e.g. Account)
- click new in the top corner
- enter the required data
- click save
7.2 Create Object Relationships
Object relationships are a special field type that connects two objects together.
A standard object like Account will probably have Contacts as a result of activity. It would be good to associat the Contacts with the Account.
When you look at Account object, you'll see there's a section for contacts on the Related tab. You can also add a contact to an account here.
This is an example of a standard relationship in sf. You can build custom relationships as well.
Different types of relationships in SF:
- Lookup relationships
- Master-Detail relationships
7.2.1 Lookup Relationships
A Lookup relationship is essentially a link to two objects so that you can 'look up' one object from the other. This can be one-to-one or one-to-many.
- Create a lookup relationship
- click the object manager
- click create | custom object
- enter the label
- enter the plural label
- check the box for Launch New Custom Tab Wizard after saving this custom object
- leave the other values as required
- press save
- click Tab Style field and select a style you like
- click next, next, save
- on the object manager page for the custom object that you created click Fields and Relationships
- click new
- choose the data type (autonumber is good for lookup relationships)
- give the field label
- choose the display format
- choose a starting number (if you chose auto-number)
- click next, next, save
Then create a custom relationship field:
- go to the object that you want create the relationship with
- click fields and relationships
- click new
- choose lookup relationship and click next
- choose the object that is Related to this object
- click next
- for field name, enter a name that relates to the related object (not this one)
- click next, next, save
7.2.2 Master-Detail Relationships
Lookups can be casual, but master-detail relationships are tighter. One object is the master and another is a detail. The master object controls certain behaviours of the detail object, like who can view the detail's data.
If you delete the master object, usually the detail object is also deleted.
- Create a Master-Detail Relationship
- on the object manager page, click fields and relationships for the object that is going to be the master
- click new
- select Master-Detail relationship
- choose the object that it is related to
- click next
- enter the field name (which should be related to the master object)
- click next, next, save
You can then view the related tab in an object to see which details have a relationship with that master.
7.2.3 More on relationships
Typically, choose lookup relationships if the relationship sometimes exists. This means that an object can exist in its own right (e.g. a Contact object doesn't have to have an Account object).
In a master-detail relationship, the detail won't work on its own. You should always create the relationship field on the detail object.
You can also have a hierarchicial relationship. Hierarchical relationships are only available on the User object. You can use them for creating management chains between users.
7.3 Work with Schema Builder
To get to the Schema Builder go to set up and then enter schema builder in the quick find box. This will bring up the objects, with their fields, with links showing the relationships.
You can drag these around the canvas. It can help visualize the data better.
7.3.1 Create an Object with the Schema Builder
- click the Elements tab in the left side bar
- click Object and drag it on to the canvas
- enter information about your object
- click save
7.3.2 Create Fields for the Object with the Schema Builder
- from the elements tab, choose a field type and drag it on to the object you just created
- fill out the details about your new field
- click save
8 Data Management
8.1 Import Data
There are two ways to import data:
- Data import wizard
- Data loader
8.1.1 Data loader
- needs to have less than 50,000 records
- objects need to be supported by the wizard
- you don't need the import process to be automated.
- Use data import
- start the wizard
- from setup, enter data import wizard in quick find
- click launch wizard!
- choose the data that you want to import
- to import accounts, contacts, leads, solutions, person accounts, or campaign members, click standard objects. To import custom objects, click custom objects
- specify whether you want to add new records to salesforce, update existing records, or add and update records simultaneously
- specify matching and other criteria as necessary
- specify the file that contains your data
- choose a character encoding method
- click next
- map your data fields to salesforce data fields
- scan the list of mapped data fields and locate any unmapped fields
- click map to the left of each unmapped field
- in the map your field dialogue box, choose salesforce fields you want to map and click map
- to change mappings that sf performed automatically, click change to the left of the appropriate field, then choose the salesforce fields you want to map and click map
- click next
- review and start your import
- review the import information on the review page
- click start import
- check import status
- the user who started the import will receive an email
- start the wizard
8.2 Export Data
Two methods exist:
- data export service
- data loader
8.2.1 Data Export Service
- from setup enter data export in quick find
- select data export
- select export now or schedule export
- select the desired encoding for the export file
- if you want images, docs, attachments, select the appropriate options
- if you schedule your export, select the frequency, start time and dates
- under exported data, select the types of data to include in the export
- click start or save
9 Formulas and Validations
9.1 Formula Editor
- from setup, open the object manager and click the required object
- click fields & relationships and new
- select formula, and next
- provide field label
- choose the data type
- click next
9.2 Use the Formula Editor
There are two flavours, simple and advanced. Nearly always use the advanced. It has more tools to create powerful formulas.
The Insert Field button opens a menu that allows you to select fields to use in your formula. Inserting from this menu automatically generates the correct syntax for accessing fields.
The Insert Operator button opens a drop-down list of the available mathematical and logical operators.
The Function menu is where you view and insert formula functions. Functions return a value according to the function used.
The text area is where you enter your formula. Formulas are case sensitive. Whitespace doesn't matter. The standard order of operations applios.
Use Check Syntax button to ensure that everything is in working order before saving.
9.3 Implement Roll-Up Summary Fields
Roll-up summary fields calculate values from a set of related records, such as those in a related list. You can create roll-up summary fields that automatically display a value on a master record based on the value of records in a detail record. These detail records must be directly related to the master through a master-detail relationship.
9.3.1 Creating a Summary Field
- from setup click object manager and click the object required
- click fields and relationships
- click new
- choose roll-up summary field types and click next
- enter field label
- click next
- the summarized object is the detail object that you want to summarize. Choose that detail object
- choose the summary type and choose the field to perform the operation on
- click next, next and save.
10 Salesforce DX
A scratch org is a dedicated, configurable, short term environment. A Developer Hub is the main salesforce org that you will use to create your scratch orgs.
10.1 Install the command line interface
Then login to your dev org with the sfdx cli:
sfdx force:auth:web:login -d -a DevHub
10.2 Setup the project on your local machine
mkdir my_sfdx_project cd my_sfdx_project git clone https://github.com/trailheadapps/dreamhouse-lwc.git cd dreamhouse-lwc
Create a branch:
git checkout -b my_branch
10.3 Create and Test the Scratch Org
Create the scratch org and give it an alias:
sfdx force:org:create -s -f config/project-scratch-def.json -a dreamhouse-org
Open the scratch you just created:
sfdx force:org:open
Because you set the scratch org as the default using the -s flag, the system remembers the authentication token and uses it to log in.
Now, push the dreamhouse repo to the scratch org:
sfdx force:source:push
10.4 Assign a permission set to the Dreamhouse App
sfdx force:user:permission:assign -n Dreamhouse
10.5 Import Test Data
sfdx force:data:tree:import --plan data/sample-data-plan.json
10.6 Test Your App in the Scratch Org
If the scratch org isn't open, open it with this command:
sfdx force:org:open
Then click the app launcher and choose dreamhouse. Click properties, and you should see 12 new properties.
11 Lightning Flow
11.1 Choose the right automation tool
- Process Builder
- Flow Builder
11.1.1 Process Builder
Use Process Builder to make processes. This is a web application that shows a GUI. It appears that Process Builder is more high level.
Use process builder when you need to start a behind the scenes business process automatically.
Processes can start when:
- a record is created
- a record is updated
- a platform event occurs
Every process consists of a trigger, at least one criteria node and at least one action. You can configure immediate actions, or scheduled actions.
- Trigger
Identify when the process should run. Is this when a record is created, or when it is created or updated?
- Criteria
This determines whether or not to execute actions. It will return true or false.
- Action
What should the process do? This is if the criteria is true. If it is immediate, it haapens as soon as the criteria is true.
If the action is scheduled, it can happen at a given time (e.g. 10 days before a record's close date), or 2 days from now.
- Actions available:
- create records
- update the record that started the process or any related record
- submit that record for approval
- send emails using a specified email template
- post to a chatter feed
If you need more than this, build and autolaunched flow or an Apex class to fill that gap. Then call the flow or Apex class from the process.
- Actions available:
- Process Types
Process builder can automate a few kinds of business processes. The main difference is the trigger: when the process starts.
- record change: a record is created or edited
- invocable: a different process can call this one
- platform event: a platform message is received
- Build a Process
- enter process builder in the quick find box
- name the process
- enter description
- configure the process to start
- click Add Object (this will start the trigger)
- select the object type
- Add a Trigger
- select what you want the trigger criteria to be
- click save
- Add criteria
- click Add Criteria
- name the criteria
- leave Conditions are met selected
- enter the fields and operators required
- click Advanced and select Yes - this means the process ignores record changes that aren't relevant to your defined criteria.
- click save
- Add a schedule
- click Set Schedule
- set the schedule to the required setting
- Add Actions
- under immediate actions click Add Action
- for the action type select the action required (this can be setting the value of a field to equal that of a different field)
- click save
- Scheduled Action
- click Add Action
- choose the Action Type
- provide a name
- select the record type
- click save
11.1.2 Flow Builder
Use Flow Builder to make flows. It appears that flow builder is more low level.
Use Flow Builder to:
- automate a guided visual experience
- add more functionality for a behind-the-scenes process than is available in Process Builder. Use Flow Builder to build the more complex functionality. Then call the resulting flow from the process.
- start a behind the scenes business process:
- when a user clicks something like a button
- when a record is created
- when a record is updated
- when a platform event occurs
- at a specified time or frequency
- Flow Building Blocks
Every flow is made up of three building blocks:
- elements
- connectors
- resources
- Elements
Elements appear on the canvas. To add an element, click it, or drag it there from the toolbox.
Flow elements fit in to four categories:
- screen (to display data to your users)
- logic (control the flow of the flow - create branches, update data, loop over sets of data, or wait for a particular time)
- actions (do something in salesforce when you have the information)
- integrations (connect your flow to an external database by using core actions or Apex actions)
- Connectors
Connectors define the path that the flow takes at runtime. They tell the flow element to execute next.
- Resources
Resources are containers that represent a given value, such as field values or formulas. You can reference resources throughout your flow. For example, look up an account's ID, store that ID in a variable, and later reference that ID to update the account.
- Build a Flow
- from the toolbox drag the elements that you need
- name the element
- connect the elements by dragging the bottom part of the element to the next element on the canvas
- click save
- Distribute A Flow
- Activate the Flow
- in the quick find box, type Flows
- click the flow that you want to change
- from the button bar in the flow builder, click Activate
- Add the Flow to a page
- from quick find box enter Builder and select Lightning App Builder
- click New
- select the page that you want to add the flow to and clickt Next
- give the page a name and click Next
- drag the flow component to the page and place it where you need it
- save your changes
- if this is a new page, you will need to Activate the page
- Check the Permissions for the Flow
You may need to change the permissions of the flow to ensure that users can use it.
- Activate the Flow
11.1.3 Combining Process Builder and Flow Builder
When process builder isn't enough, you can add a Flow Builder to it.
Configure the more complex functionality with the Flow Builder and then add the Flow to a Process built with Process Builder.
Build the Flow Builder as documented <IP_ADDRESS>, and then add it to the Process Builder.
To add a Flow Builder Process to a Process Builder Process, add it to the Action box.
11.2 Apex
When you need something more than can be provided by Process Builder or Flow Builder, you can code it with Apex.
11.3 Approval Process
This is another tool, which isn't included in Lightning Flow, but it offers a declarative way to automate a process.
11.3.1 Customize how records get Approved with Approvals
An approval automates how Salesforce records are approved in your org.
In an approval process you specify:
- the steps necessary for a record to be approved
- the actions to take based on what happened during the approval process (approved or rejected).
11.3.2 How to build an approval process
- in the quick find box, enter Approval Process
- for the Manage Approval Process For select the object that is required
- click Create New Approval Process and Use Jump Start Wizard
- configure the approval process
- give it a name
- give fields and values accordingly
- enter any criteria required
- select the approver
- save the approval process
- click View Approval Process Detail Page
- under Final Approval Actions, click Add New and Field Update
- provide the values required
- click save
- under Final Rejection Actions click Add New and Field Update
- provide the required settings
- click save
12 Apex Basics and Database
12.1 Get Started with Apex
Apex is saved, compiled and executed on the Lightning platform. It is object oriented, strongly typed, multitenant aware, integrates with the database, data focussed, easy to use, easy to test, and versioned.
12.1.1 Data Types
- primitive (Integer, Double, Long, Date, Datetime, String, ID, Boolean, etc.)
- sObject (such as Account or MyCustomObject)
- collection:
- a list (or array)
- a set of primitives
- a map of primitives
- a typed list of values
- user defined Apex classes
- system supplied Apex classes
- Apex Collections: List
You can declare a variable to be a list in two ways:
- List<String> colours = new List<String>;
- String[] colours = new List<String>;
It's easier to create a list rather than an array, as you can't be sure of how many items will be in a list.
You can add items to the list:
colours.add('orange'); colours.add('purple');
12.1.2 Apex Classes
Class methods can be called by triggers and other classes.
To create an Apex class, go to the developer console and type File > New > Class.
You can execute code by using the Debug > Open Execute Anonymous Window. Here you can instantiate your class:
EmailManager em = new EmailManager<EMAIL_ADDRESS>'Trailhead', '123 body1);
Click Execute.
Check the output using the debug logs.
12.2 Use sObjects
Every record in SF is natively represented as an sObject in Apex. The Account sObject is an abstraction of the account record and holds the account field information in memory as an object.
Each Salesforce record is represented as an sObject before it is inserted into the database. When a record is retrieved, it is stored as an sObject. If you have added custom objects to your org, they will be represented with an sObject and the _c suffix.
12.2.1 Creating sObject Variables
- declare a variable and assign it to an sObject instance
Accout acct = new Account(Name='Acme');
12.3 sObject and Field Names
The name of the sObjects corresponds to the API names of the corresponding standard or custom objects.
API names can differ from the label names.
For custom objects and custom fields the API name always ends with the suffix _c. For custom relationships, the API name ends with _r.
To find standard object look up the field API names here https://developer.salesforce.com/docs/atlas.en-us.224.0.object_reference.meta/object_reference/.
Your custom objects will be visible in your org.
12.4 Creating sObjects and adding Fields
You can create a new object with:
Account acct = new Account();
However, you can also pass in variables at instantiation:
Account acct = new Account(Name='Acme', Phone='01256 1234455', NumberofEmployees=100);
You can also use the dot notation:
Account acct = new Account(); acct.Name = 'Acme'; acct.Phone = '012451234455'; acct.NumberofEmployees=100;
12.5 Working with the Generic sObject Type
When you don't know the type of an sObject your method is calling, you can use the generic sObject data type.
Variables that are declared with the generic sObject data type can reference any Salesforce record, whether it is a standard or custom object record.
12.6 Casting Generic sObjects to Specific sObject Types
When you're dealing with generic objects, sometimes you need to cast them to a specific sObject type. One of the benefits of doing so is to be able to access fields using dot notation, which is not available on generic sObject.
This is how you cast a generic sObject to a specific object class:
Acct acct = (Account)myGenericObject; String name = acct.Name; String phone = acct.Phone;
The (Account) is the casting definition here.
12.7 Manipulate Records with DML
DML = Data Manipulation Language.
Once you have created an object, you can insert it into the database with:
insert Acct;
The following DML statements are:
- insert
- update
- upsert
- delete
- undelete
- merge
Each DML statement accepts a single sObject or a list of sObjects. Operating on a list of sObjects is a more efficient way for processing records.
The upsert DML operation creates new records and updates sObjects records within a single statement, using a specified field to determine the prescence of existing objects, or the ID field if no field is specified.
The merge field statement merges up to three records of the same sObject type into one of the records, deleting the others, and reparenting any relating records.
12.7.1 ID Field Auto-Assignedd to New Records
When inserting records, the system assigns an ID for each record. This snippet shows you how to find the ID of an sObject.
insert acct; ID acctID = acct.Id; System.debug('ID = ' + acctID);
12.7.2 Bulk DML
You can perform DML operations either on a single object or in a bulk on a list of sObjects. Performing bulk DML is recommended as you will not hit any limits. This example inserts contacts in bulk by inserting a list of contacts in one call:
// Create a list of contacts List<Contact> conList = new List<Contact> { new Contact(FirstName='Joe',LastName='Smith',Department='Finance'), new Contact(FirstName='Kathy',LastName='Smith',Department='Technology'), new Contact(FirstName='Caroline',LastName='Roth',Department='Finance'), new Contact(FirstName='Kim',LastName='Shain',Department='Education')}; // Bulk insert all contacts with one DML call insert conList; // List to hold the new contacts to update List<Contact> listToUpdate = new List<Contact>(); // Iterate through the list and add a title only // if the department is Finance for(Contact con : conList) { if (con.Department == 'Finance') { con.Title = 'Financial analyst'; // Add updated contact sObject to the list. listToUpdate.add(con); } } // Bulk update all contacts with one DML call update listToUpdate;
12.7.3 Upserting Records
If you have a list of new and existing records, you can process insertions and updates in the list by using the upsert statement.
If you don't specify a field when calling this argument, the upsert statement uses the sObject's ID to match the sObject with existing records. You can specify a field to use for matching.
The syntax follows this format:
upsert sObject | sObject[] upsert sObject | sObject[] field
The optional field is a field token. To specify the MyExternalID field, the statement would be:
upsert sObjectList Account.Fields.MyExternalID;
Upsert uses the sObject record's primary key (the ID) an idLookup field, or an external ID field to determine whether it should create a new record or update an existing one:
- if the key is not matched, a new object will be created
- if the key is matched, the existing record is updated
- if the key is matched multiple times, an error is generated and the object is neither inserted or updated
This example shows how to update an existing contact record and insert a new one in one call. It updates the existing Josh record and inserts Kathy.
// Insert the Josh contact Contact josh = new Contact(FirstName='Josh',LastName='Kaplan',Department='Finance'); insert josh; // Josh's record has been inserted // so the variable josh has now an ID // which will be used to match the records by upsert josh.Description = 'Josh\'s record has been updated by the upsert operation.'; // Create the Kathy contact, but don't persist it in the database Contact kathy = new Contact(FirstName='Kathy',LastName='Brown',Department='Technology'); // List to hold the new contacts to upsert List<Contact> contacts = new List<Contact> { josh, kathy }; // Call upsert upsert contacts; // Result: Josh is updated and Kathy is created.
If an insert statement had been used, a duplicate Josh record would have been created.
12.7.4 Deleting Records
Deleted records aren't permanently deleted from the Lightning Platform, but are put in a Recycling Bin for 15 days. They can be restored within the 15 days. This example shows you how to delete a record based on a SQL lookup:
Contact[] contactsDel = [SELECT Id FROM Contact WHERE LastName='Smith']; delete contactsDel;
Any record with the last name of 'Smith' will be deleted.
12.8 DML Statement Exceptions
If a DML operation fails, it returns an exception type of DmlException. You can catch exceptions in your code to handle error conditions.
This example produces a DmlException (it attempts to insert an account without the required Name field). The exception is then caught.
try { // This causes an exception because // the required Name field is not provided. Account acct = new Account(); // Insert the account insert acct; } catch (DmlException e) { System.debug('A DML exception has occurred: ' + e.getMessage()); }
12.9 Database Methods
Apex contains a Database class, which provides methods that perform DML operations and mirror the DML statement counterparts.
The methods are static and are called on the class name:
Database.insert()
Database.update()
Database.upsert()
Database.delete()
Database.undelete()
Database.merge()
Unlike DML statements, Database methods have an optional allOrNone
parameter that allows you to specify whether the operation should partially succeed. When this parameter is set to false if errors occur on a partial set of records, the successful records will be committed and the errors will be returned for the failed records. Also, no exceptions are thrown with the partial success option.
You can retrieve results by assigning them to a list variable:
Database.SaveResult[] results = Database.insert(recordList, false);
By default, the allOrNone parameter is set to true.
12.9.1 Example of Inserts with Partial Success
This example, uses the Database methods, and the bulk DML with the partial success method. One of the contacts in the list does not have fields (deliberately) and will cause an error because a contact cannot be saved without the required Lastname field. Three other contacts are saved. The last part iterates through the returned results and writes debug messages to the debug log
// Create a list of contacts List<Contact> conList = new List<Contact> { new Contact(FirstName='Joe',LastName='Smith',Department='Finance'), new Contact(FirstName='Kathy',LastName='Smith',Department='Technology'), new Contact(FirstName='Caroline',LastName='Roth',Department='Finance'), new Contact()}; // Bulk insert all contacts with one DML call Database.SaveResult[] srList = Database.insert(conList, false); // Iterate through each returned result for (Database.SaveResult sr : srList) { if (sr.isSuccess()) { // Operation was successful, so get the ID of the record that was processed System.debug('Successfully inserted contact. Contact ID: ' + sr.getId()); } else { // Operation failed, so get all errors for(Database.Error err : sr.getErrors()) { System.debug('The following error has occurred.'); System.debug(err.getStatusCode() + ': ' + err.getMessage()); System.debug('Contact fields that affected this error: ' + err.getFields()); } } }
12.9.2 DML Statements or Database Methods?
Use DML statements if you want any error that occurs during bulk DML processing to be thrown as an Apex exception that immediately interrupts control flow (by using try..catch blocks). This is similar to the way exceptions are handled in most database procedural languages.
Use Database class methods if you want to allow partial success of a bulk DML operation - if a record fails, the remainder of the DML operation can still succeed. Your application can then inspect the rejected records and possibly retry the operation.
12.9.3 Working with Related Records
Create and manage records that are related to each other through relationships.
- Inserting Related Records
This example shows how to add a contact to an account (the related record) by setting AccountId field on the contact. The Contact and Account are linked through a lookup relationship.
Account acct = new Account(Name='SFDC Account'); insert acct; // Once the account is inserted, the sObject will be // populated with an ID. // Get this ID. ID acctID = acct.ID; // Add a contact to this account. Contact mario = new Contact( FirstName='Mario', LastName='Ruiz', Phone='415.555.1212', AccountId=acctID); insert mario;
- Updating Related Records
Fields on related records require a separate DML call. You will need to lookup anything that is already existing before carrying out the insert.
The following example, shows the SELECT statement, then the setting of the fields to the required data, and then the related record's field:
// Query for the contact, which has been associated with an account. Contact queriedContact = [SELECT Account.Name FROM Contact WHERE FirstName = 'Mario' AND LastName='Ruiz' LIMIT 1]; // Update the contact's phone number queriedContact.Phone = '(415)555-1213'; // Update the related account industry queriedContact.Account.Industry = 'Technology'; // Make two separate calls // 1. This call is to update the contact's phone. update queriedContact; // 2. This call is to update the related account's Industry field. update queriedContact.Account;
- Deleting Related Records
The delete operation supports cascading deletions. If you delete a parent object, you delete its children automatically, as long as each chil record can be deleted.
This example will delete an Account and any related contacts.
Account[] queriedAccounts = [SELECT Id FROM Account WHERE Name='SFDC Account']; delete queriedAccounts;
12.10 Write SOQL Queries
To read a record from the database, you must write a query, in the form of Salesforce Object Query Language.
You can embed SOQL in your Apex code. When it is embedded, it is referred to as inline SOQL.
Wrap the SOQL statement within square brackets and assign the return to a variable:
Account [] accts = [SELECT Name, Phone FROM Account];
12.10.1 Use the Query Editor
The developer console has a specially designed tool for queries. It's a good way to test the SOQL you are writing.
12.10.2 Basic SOQL Syntax
SELECT fields FROM ObjectName [WHERE Condition]
The WHERE clause is optional.
Unlike other SQL languages, you cannot specify SELECT * FROM
for all fields. You must specify every field you want to return. However, you do not need to specify the Id
field as this is always returned. You may want to specify it if it is the only field that you require.
12.10.3 Filtering Query Results with Conditions
You can limit the amount of results you get with conditions (i.e. using the WHERE clause).
SELECT Name,Phone FROM Account WHERE Name='SFDC Computing'
or even further:
SELECT Name,Phone FROM Account WHERE (Name='SFDC Computing' AND NumberOfEmployees>25)
or further still:
SELECT Name,Phone FROM Account WHERE (Name='SFDC Computing' OR (NumberOfEmployees>25 AND BillingCity='Los Angeles'))
Instead of using the equal operator (=
) for comparison, you can perform fuzzy matches by using the LIKE
operator. For example, you can retrieve all accounts whose names start with SFDC by using this condition: WHERE Name LIKE 'SFDC%'
. The %
wildcard character matches any or no character. The _
character in contrast can be used to match just one character.
12.10.4 Ordering Query Results
This can be done with the ORDER BY
clause.
SELECT Name,Phone FROM Account ORDER BY Name
By default, the order returned is ordered using ASC
. You can reverse this:
SELECT Name,Phone FROM Account ORDER BY Name DESC
You cannot sort on rich text fields, or multi-select lists.
12.10.5 Limit the Number of Records Returned
You can limit the number of results returned using the LIMIT n
clause.
Account oneAccountOnly = [SELECT Name,Phone FROM Account LIMIT 1];
12.10.6 Combining all the above operators
SELECT Name,Phone FROM Account WHERE (Name = 'SFDC Computing' AND NumberOfEmployees>25) ORDER BY Name LIMIT 10
To get output to the debug console, assign it to a variable and output that variable to the debug console:
Account[] accts = [SELECT Name,Phone FROM Account WHERE (Name='SFDC Computing' AND NumberOfEmployees>25) ORDER BY Name LIMIT 10]; System.debug(accts.size() + ' account(s) returned.'); // Write all account array info System.debug(accts);
12.10.7 Accessing Variables in SOQL Queries
You may have a variable that you would like to search for. Use the :
notation to refer to that variable, thus:
String targetDepartment = 'Wingo'; Contact[] techContacts = [SELECT FirstName,LastName FROM Contact WHERE Department=:targetDepartment];
12.10.8 Querying Relatede Records
Records can be linked to each through relationships: lookup relationships or master-detail relationships. The Contact has a lookup relationship to Account. When you create a Contact, you cat associate it with an Account. These will be listed on the Related List on the Account's page. You can find these using SOQL.
To get child records, you add an inner query for the child records. The FROM
clause of the inner query runs against the relationship name, rather than a Salesforce object name.
This example contains an inner clause to get all contacts that are associated with each returned account. The FROM
clause specifies the Contacts relationship, which is a default relationship on Account that links accounts and contacts:
SELECT Name, (SELECT LastName FROM Contacts) FROM Account WHERE Name = 'SFDC Computing'
This example shows the SOQL query embedded in Apex and shows how to get the child records from the SOQL result by using the Contact relationship name on the Object:
Account[] acctsWithContacts = [SELECT Name, (SELECT FirstName,LastName FROM Contacts) FROM Account WHERE Name = 'SFDC Computing']; // Get child records Contact[] cts = acctsWithContacts[0].Contacts; System.debug('Name of first associated contact: ' + cts[0].FirstName + ', ' + cts[0].LastName);
12.10.9 Dot Notation
You can traverse a relationship from a child object (contact) to a field on its parent (Account.Name) by using dot notation. For example, the following Apex snippet queries contact records whose first name is Carol and is able to retrieve the name of Carol's associated account by traversing the relationship between accounts and contacts.
Contact[] cts = [SELECT Account.Name FROM Contact WHERE FirstName = 'Carol' AND LastName='Ruiz']; Contact carol = cts[0]; String acctName = carol.Account.Name; System.debug('Carol\'s account name is ' + acctName);
Here, the key part is carol.Account.name
.
12.10.10 Querying Record in Batches by Using SOQL For Loops
With a SOQL for loop, you can include a SOQL within a for
loop. The results of a SOQL query can be iterated over within the loop. SOQL for loops use a different method for retrieving records - records are retrieved using efficient chunking with calls to the query
and queryMore
methods of the SOAP API. By using SOQL for loops, you can avoid hitting the heap size limit.
SOQL for
loops iterate over all the sObject records returned by a SOQL query.
The syntax of a SOQL for
loop is either:
for (variable : [soql_query]){ code_block }
or
for (variable_list : [soql_query]){ code_block }
Both variable
and variable_list
must be the same type as the sObjects that are returned by the sql_query
.
It is preferable to use the sObject list format of the SOQL for loop as the loop executes once for each batch of 200 sObjects. Doing so enables you to work on batches of records and perform DML operations in batch, which helps avoid reaching governor limits.
insert new Account[]{new Account(Name = 'for loop 1'), new Account(Name = 'for loop 2'), new Account(Name = 'for loop 3')}; // The sObject list format executes the for loop once per returned batch // of records Integer i=0; Integer j=0; for (Account[] tmp : [SELECT Id FROM Account WHERE Name LIKE 'for loop _']) { j = tmp.size(); i++; } System.assertEquals(3, j); // The list should have contained the three accounts // named 'yyy' System.assertEquals(1, i); // Since a single batch can hold up to 200 records and, // only three records should have been returned, the // loop should have executed only once
More about SOQL can be read here https://developer.salesforce.com/docs/atlas.en-us.224.0.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm
- Activity for this Unit
Create an Apex class that returns contacts based on incoming parameters. For this challenge, you will need to create a class that has a method accepting two strings. The method searches for contacts that have a last name matching the first string and a mailing postal code matching the second. It gets the ID and Name of those contacts and returns them.
The Apex class must be called ContactSearch and be in the public scope The Apex class must have a public static method called searchForContacts The method must accept two incoming strings as parameters The method should then find any contact that has a last name matching the first string, and mailing postal code (API name: MailingPostalCode) matching the second string The method should finally return a list of Contact records of type List that includes the ID and Name fields
public class ContactSearch { public static List<Contact> searchForContacts(String lastNameSearch, String PostalCode){ List<Contact> Contacts = [select Id, Name from Contact where Lastname=:lastNameSearch AND MailingPostalCode=:PostalCode]; return Contacts; } }
12.11 When to use SOSL instead of SOQL?
Whether you use SOQL or SOSL depends on what you know about the objecs or the fields you want to search. SOSL is a programmatic way of performing a text-based search against the search index.
Use SOQL when you know which objects the data resides in and you want to:
- retrieve data from a single object or from multiple objects that are related to one another
- count the number of records that meet specific criteria
- sort results as part of a query
- retrieve data from number, date, or checkbox
Use SOSL when you don't know which object or fields the data resides in, and you want to:
- retrieve data for a specific term that you know exists within a field. Because SOSL can tokenize multiple terms within a field and build a search index from this, SOSL searches are faster and can return more relevant results
- retrieve multiple objects and fields efficiently where the objects might or might not be related to one another
- retrieve data for a particular division in an organization using the division feature
- retrieve data that has asian characters (e.g. Chinese characters)
12.12 Write SOSL Queries
Salesforce Object Search Language is a tool used to perform text searches in records.
You can embed SOSL queries to Apex - this is known as inline SOSL.
This example searches for accounts and contacts that have any fields with the word 'SFDC';
List<List<SObject>> searchList = [FIND 'SFDC' IN ALL FIELDS RETURNING Account(Name), Contact(FirstName,LastName)];
Unlike SOQL, SOSL can query all objects.
SOSL matches fields based on a word match while SOQL performs an exact match by default (when not using wildcards).
12.12.1 Using the Query Editor
Enter the following in the Query Editor and press the Execute button:
FIND {Wingo} IN ALL FIELDS RETURNING Account(Name), Contact(FirstName,LastName,Department)
All accounts and contact records in your org that satisfy the criteria will display in the query results.
12.12.2 Basing SOSL Syntax
SOSL allows you to specify the following search criteria:
- Text expression (single word or phrase) to search for
- Scope of the fields to search
- List of objects and fields to retrieve
- Conditions for selecting rows in the source objects
This is the syntax of a basic SOSL query:
FIND 'SearchQuery' [IN SearchGroup] [RETURNING ObjectsAndFields]
You can also include wildcard characters (such as *
and ?
). The *
matches zero or more characters. The ?
wildcard matches only one character at a time.
Text searches are case insensitive.
SearchGroup is optional. In the scope of the fields to search. If not specified, the default search scope is all fields. SearchGroup can take one or more of the following values:
- ALL FIELDS
- NAME FIELDS
- EMAIL FIELDS
- PHONE FIELDS
- SIDEBAR FIELDS
ObjectsAndFields is optional. It is the information to return in the search result - a list of one or more sObjects and, within each sObject, list of one or more fields, with optional values to filter against. If not specified, the search results contain the IDs of all objects found.
12.12.3 Single Words and Phrases
A SearchQuery contains two types of texts:
- Single word
- Phrase
- Single word
Words in a SearchQuery are delimited by spaces, punctuation and changes from letters to digits (and vice-versa). Words are always case-insensitive.
- Phrase
A collection of words and spaces surrounded by double quotes such as "john smith". Multiple words can be combined together with logic and grouping operators to from a more complex query.
12.12.4 Search Examples
This table lists various example search strings and the SOSL search results:
| Search in all fields for: | Search Description | Matched Records and Fields |
|---|---|---|
| The Query | returns records that has both words, in any location of text | Account: The SFDC Query Man (Name field matched) |
| Wingo OR Man | returns records with fields containing Wingo or Man | Contact: Carol Ruiz, Department: 'Wingo' or Account: The SFDC Query Man (Name field matched) |
| 1212 | returns records whose fields contains the word 1212 | Account: The SFDC Query Man, Phone: '(415)555-1212' |
| wing* | returns records with a field starting with wing | Contact: Maria Ruiz, Department: 'Wingo' and Account: The SFDC Query Man, Description: 'Expert in wing technologies.' |
12.12.5 SOSL Apex Example
This SOSL query combines two search terms by using OR logical operator - it searches for Wingo or SFDC in any field. The results are returned in a list of lists. Each list contains an array of the returned records. At index 0, the list contains the array of accounts. At index 1, the list contains the array of contacts.
E.g.:
list[0] // are accounts lists[1] // are contacts
Here is the code that returns this list of lists:
List<List<sObject>> searchList = [FIND 'Wingo OR SFDC' IN ALL FIELDS RETURNING Account(Name),Contact(FirstName,LastName,Department)]; Account[] searchAccounts = (Account[])searchList[0]; Contact[] searchContacts = (Contact[])searchList[1]; System.debug('Found the following accounts.'); for (Account a : searchAccounts) { System.debug(a.Name); } System.debug('Found the following contacts.'); for (Contact c : searchContacts) { System.debug(c.LastName + ', ' + c.FirstName); }
12.12.6 Filtering SOSL queries
You can filter queries using the RETURNING
clause by using a WHERE
clause.
E.g.:
RETURNING Account(Name, Industry WHERE Industry='Apparel')
12.12.7 Activity for this Unit
To pass this challenge, create an Apex class that returns both contacts and leads that have first or last name matching the incoming parameter.
The Apex class must be called ContactAndLeadSearch and be in the public scope The Apex class must have a public static method called searchContactsAndLeads The method must accept an incoming string as a parameter The method should then find any contact or lead that matches the string as part of either the first or last name The method should finally use a return type of List<List< SObject>> NOTE: Because SOSL indexes data for searching, you must create a Contact record and Lead record before checking this challenge. Both records must have the last name Smith. The challenge uses these records for the SOSL search
public class ContactAndLeadSearch { public static List<List<Sobject>> searchContactsAndLeads(String search){ List<List<Sobject>> sobjects = [Find :search in name fields returning contact(name), lead(name)]; return sobjects; } }
More about SOSL can be read here https://developer.salesforce.com/docs/atlas.en-us.224.0.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm
13 Apex Triggers
13.1 Get Started with Apex Triggers
Apex triggers enables you to perform custom actions after events to records in Salesforce, such as insertions, updates or deletions.
Typically, you use triggers to perform operations based on specific conditions, to modify related records or restrict certain operations from happening. You can use triggers to do anything you can do in Apex, including executing SOQL and DML or calling custom Apex methods.
Use triggers to perform tasks that can't be done by using the point-and-click tools in the Salesforce user interface.
Triggers can be defined for top-level standard objects such as Account or Contact, custom objects and some standard child objects.
13.1.1 Trigger Syntax
This is different from a class definition's syntax. A trigger definition starts with the trigger
keyword. It is then followed by the name of the trigger, the Salesforce object that the trigger is associated with, and the conditions under which it fires.
trigger TriggerName on ObjectName(trigger_events){ code_block }
To execute a trigger before or after an insert, update, delete and undelete operations, specify multiple trigger events in a comma-separated list. The events you can specify are:
- before insert
- before update
- before delete
- after insert
- after update
- after delete
- after undelete
13.1.2 Example of a Trigger
- in the developer console, click File > New > Apex Trigger
- enter the name of the trigger
- select the Object that this will affect
- click submit
- add code to the developer's canvas
For example:
trigger HelloWorld on Account (before insert){ System.debug('Hello world'); }
- to save press Ctrl-S
- to test this example trigger, create a new account
13.1.3 Types of Triggers
There are two types of triggers:
- Before triggers are used to validate or update record values before they are saved to the DB
- After triggers are used to access field values that are set by the system (such as record's
Id
orLastModifiedDate
field), and to affect changes in other records. The records that fire after trigger are read-only
13.1.4 Using Context Variables
To access records tha caused the trigger to fire use context variables. For examle, Trigger.New
contains all the records that were inserted in insert or update triggers.
Trigger.Old
provides the old version of the sObjects before they were updated in update triggers, or a list of deleted sObjects in delete triggers. Triggers can fire when one record is isnerted, or when many records are inserted in bulk via the API or Apex. Therefore, context variables, such as Trigger.New
, can contain only one record or multiple records. You can iterate over Trigger.New
to get each individual sObject.
This example is a modified version of the HelloWorldTrigger. It iterates over each account in a for loop and updates the Description fields for each:
trigger HelloWorldTrigger on Account (before insert){ for(Account a: Trigger.New){ a.Description = 'New Description'; } }
The system saves the records that fired the before trigger after the trigger finishes execution. You can modify records in the trigger without explicitly calling the DML insert or update operation. If you perform DML statements on those records, you get an error.
Some other context variables return a Boolean value to indicate whether the trigger was fired due to an update or some other event. These variables are useful when a trigger combines multiple events. For example:
trigger ContextExampleTrigger on Account (before insert, after insert, after delete) { if (Trigger.isInsert) { if (Trigger.isBefore) { // Process before insert } else if (Trigger.isAfter) { // Process after insert } } else if (Trigger.isDelete) { // Process after delete } }
13.1.5 Trigger context Variables
| Variable | Usage |
|---|---|
isExecuting |
Returns true if the current context for the Apex code is a trigger, not a Visualforce page, a Web service, or an executeanonymous() API call. |
isInsert |
Returns true if this trigger was fired due to an insert operation, from the Salesforce user interface, Apex, or the API. |
isUpdate |
Returns true if this trigger was fired due to an update operation, from the Salesforce user interface, Apex, or the API. |
isDelete |
Returns true if this trigger was fired due to an update operation, from the Salesforce user interface, Apex, or the API. |
isBefore |
Returns true if this trigger was fired before any record was saved. |
isAfter |
Returns true if this trigger was fired after all records were saved. |
isUndelete |
Returns true if this trigger was fired after a record is recovered from the Recycle Bin. This recovery can occur after an undelete operation from the Salesforce user interface, Apex, or the API. |
new |
Returns a list of the new versions of the sObject records. This sObject list is only available in insert, update, and undelete triggers, and the records can only be modified in before triggers. |
newMap |
A map of IDs to the new versions of the sObject records. This map is only available in before update, after insert, after update, and after undelete triggers. |
old |
Returns a list of the old versions of the sObject records. This sObject list is only available in update and delete triggers. |
oldMap |
A map of IDs to the old versions of the sObject records. This map is only available in update and delete triggers. |
operationType |
Returns an enum of type System.TriggerOperation corresponding to the current operation. Possible values of the System.TriggerOperation enum are: BEFORE_INSERT , BEFORE_UPDATE , BEFORE_DELETE , AFTER_INSERT , AFTER_UPDATE , AFTER_DELETE , and AFTER_UNDELETE . If you vary your programming logic based on different trigger types, consider using the switch statement with different permutations of unique trigger execution enum states. |
size |
The total number of records in a trigger invocation, both old and new.The total number of records in a trigger invocation, both old and new. |
13.1.6 Calling a Class Method from a Trigger
You can call public utility methods from a trigger. Calling methods of other classes enables code re-use, reduces the size of your triggers, and improves maintenance of your Apex code. It also allows for object-oriented code programming.
This example shows how to call a static method from a trigger. If the trigger was fired because of an insert event, the example calls the static sendMail()
method on the EmailManager
class. This utility method sends an email to the specified recipient and contains the number of contact records inserted.
trigger ExampleTrigger on Contact (after insert, after delete) { if (Trigger.isInsert) { Integer recordCount = Trigger.New.size(); // Call a utility method from another class EmailManager.sendMail('Your email address', 'Trailhead Trigger Tutorial', recordCount + ' contact(s) were inserted.'); } else if (Trigger.isDelete) { // Process after delete } }
When you next create a contact, you will receive an email with the specified information.
13.1.7 Adding Related Records
Triggers are often used to access and manage records related to the records in teh trigger contexc- the records that caused the trigger to fire.
This example trigger adds a related opportunity for each new or updated account if no opportunity is already associated with the account. The trigger:
- performs a SOQL query to get all child opportunities for the accounts that the trigger fired on
- iterates over the list of sObjects in
Trigger.New
to get each account sObject - checks if the account doesn't have any related opportunity sObjects, the for loop creates one
- if the trigger created any new opportunities, the final statement
insert
them
trigger AddRelatedRecord on Account(after insert, after update) { List<Opportunity> oppList = new List<Opportunity>(); // Get the related opportunities for the accounts in this trigger Map<Id,Account> acctsWithOpps = new Map<Id,Account>( [SELECT Id,(SELECT Id FROM Opportunities) FROM Account WHERE Id IN :Trigger.New]); // Add an opportunity for each account if it doesn't already have one. // Iterate through each account. for(Account a : Trigger.New) { System.debug('acctsWithOpps.get(a.Id).Opportunities.size()=' + acctsWithOpps.get(a.Id).Opportunities.size()); // Check if the account already has a related opportunity. if (acctsWithOpps.get(a.Id).Opportunities.size() == 0) { // If it doesn't, add a default opportunity oppList.add(new Opportunity(Name=a.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=a.Id)); } } if (oppList.size() > 0) { insert oppList; } }
This is a run-down of what the code does: Declare the trigger:
trigger AddRelatedRecord on Account(after insert, after update) {
Create a new list to hold opportunities:
List<Opportunity> oppList = new List<Opportunity>();
Get the related opportunities for the accounts in this trigger. The embedded SELECT
statement looks for the relationship. We create a Map to hold the Id and the account. Finally, we only search for Ids in this trigger:
Map<Id,Account> acctsWithOpps = new Map<Id,Account>( [SELECT Id,(SELECT Id FROM Opportunities) FROM Account WHERE Id IN :Trigger.New]);
Iterate through the each of the accounts in Trigger.New
. Print them to the screen. Then check if the account already has an opportunity, and if it doesn't add one:
for(Account a : Trigger.New) { System.debug('acctsWithOpps.get(a.Id).Opportunities.size()=' + acctsWithOpps.get(a.Id).Opportunities.size()); // Check if the account already has a related opportunity. if (acctsWithOpps.get(a.Id).Opportunities.size() == 0) { // If it doesn't, add a default opportunity oppList.add(new Opportunity(Name=a.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=a.Id)); } }
Then, insert any new records back to the database (if they exist):
if (oppList.size() > 0) { insert oppList; } }
Sometimes accessing all the records in Trigger.New
is not efficient. We can improve on this. In Bulk Design Patterns unit, we use SOQL query to get only the accounts with no opportunities. Then, iterate over only those records.
13.1.8 Use Trigger Exceptions
You sometimes need to add restrictions to certain database operations (e.g. preventing records from being saved when certain conditions are met. To prevent saving records in a trigger, call the addError()
method on the sObject. This method throws a fatal error inside a trigger. The error message is displayed in the user interface and is logged.
This example prevents the deletion of an account if it has related opportunities. By default, this delete would delete all the cascading related records. This stops that.
trigger AccountDeletion on Account (before delete) { // Prevent the deletion of accounts if they have related opportunities. for (Account a : [SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity) AND Id IN :Trigger.old]) { Trigger.oldMap.get(a.Id).addError( 'Cannot delete account with related opportunities.'); } }
Calling addError()
in a trigger causes the entire set of operations to rollback, except when a bulk DML is called with partial success.
If a DML statement in Apex spawned the trigger, any error rolls back the entire operation.
If a bulk DML call in the Lightning Platform API spawned the trigger, the runtime engine sets the bad records aside. It then attempts a partial save of the records that did not generate errors.
13.1.9 Triggers and Callouts
Callouts can go to external webservices and get information. They must be done asynchronously so the trigger doesn't block any process, while you are waiting for the response. It is made in the background.
To make a callout from a trigger, call a class method that executes asynchronously. Such a method is called a future method and is annotated with @future(callout=true)
. This example class contains the future method that makes the callout:
public class CalloutClass { @future(callout=true) public static void makeCallout() { HttpRequest request = new HttpRequest(); // Set the endpoint URL. String endpoint = 'http://yourHost/yourService'; request.setEndPoint(endpoint); // Set the HTTP verb to GET. request.setMethod('GET'); // Send the HTTP request and get the response. HttpResponse response = new HTTP().send(request); } }
Then, this trigger can make the call to the future method:
trigger CalloutTrigger on Account (before insert, before update) { CalloutClass.makeCallout(); }
More information about using Callouts can be found here: https://developer.salesforce.com/docs/atlas.en-us.224.0.apexcode.meta/apexcode/apex_callouts.htm
13.1.10 Unit Challenge
Create an Apex trigger for Account that matches Shipping Address Postal Code with Billing Address Postal Code based on a custom field. For this challenge, you need to create a trigger that, before insert or update, checks for a checkbox, and if the checkbox field is true, sets the Shipping Postal Code (whose API name is ShippingPostalCode) to be the same as the Billing Postal Code (BillingPostalCode).
The Apex trigger must be called 'AccountAddressTrigger'. The Account object will need a new custom checkbox that should have the Field Label 'Match Billing Address' and Field Name of 'MatchBillingAddress'. The resulting API Name should be 'MatchBillingAddress_c'. With 'AccountAddressTrigger' active, if an Account has a Billing Postal Code and 'MatchBillingAddress_c' is true, the record should have the Shipping Postal Code set to match on insert or update.
This is the solution:
trigger AccountAddressTrigger on Account (before insert, before update) { for (Account a : Trigger.New){ if (a.Match_Billing_Address__c == true){ a.ShippingPostalCode = a.BillingPostalCode; } } }
13.2 Bulk Apex Triggers
This section deals with operating on collections of sObjects and performing efficient SOQL and DML operations.
13.2.1 Bulk Trigger Design Patterns
Triggers are optimized to operate in bulk. When using bulk, you have better performance, consume less server resources and are less likely to exceed platform limits.
Bulkified code can process large numbers of records efficiently and run within governor limits.
The following sections demonstrate thte main ways of bulkifying your apex code in triggers: operating on all records in the trigger, and performing SOQL and DML on collections of objects instead of single sObjects at a time. The SOQL and DML bulk best practices apply to any Apex code, including SOQL and DML in classes. The examples given are based on triggers and use the Trigger.New
context variable.
13.2.2 Operating on Record Sets
Let's first look at the most basic bulk design concept in triggers. Typically, triggers operate on one record if the action that fired the trigger originates from the user interface. But, if the origin of the action was bulk DML or the API, the trigger operates on a record set. For example, when you import many records via the API, triggers operate on the full record set. Good programming practice is to assume that the trigger operates on a collection of records so that it works in all circumstances.
This example trigger assumes that only one record caused the trigger to fire:
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.New[0]; a.Description = 'New description'; }
This example is a modified version of the above. It uses a loop to iterate over all available objects. This will work if Trigger.New
contains one record, or many:
trigger MyTriggerBulk on Account(before insert) { for(Account a : Trigger.New) { a.Description = 'New description'; } }
13.2.3 Performing Bulk SOQL
SOQL queries can be powerful. You can retrieve related records and check a combination of multiple conditions in one query. By using SOQL queries, you write less code and make fewer queries to the database. This helps prevent you hitting any limits which are 100 SOQL query limits for synchronous Apex or 200 asynchronous Apex.
The following trigger shows a SOQL query pattern to avoid. It makes a SOQL query inside a for loop to get the related opportunities for each account, which runs for each Account sObject in Trigger.New
. This could result in too many SOQL queries.
trigger SoqlTriggerNotBulk on Account(after update) { for(Account a : Trigger.New) { // Get child records for each account // Inefficient SOQL query as it runs once for each account! Opportunity[] opps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId=:a.Id]; // Do some other processing } }
This modifies the above and shows best practices for running SOQL queries. The SOQL does the heavy lifting and is called once outside the main loop.
- the SOQL query uses an inner query
(SELECT Id FROM Opportunities)
to get related opportunities of accounts - the SOQL query is connected to the trigger context records by using the
IN
clause and binding theTrigger.New
variable in theWHERE
clause -WHERE Id IN :Trigger.New
. This where condition filters the accounts to only those records that fired this trigger
Combining the two parts in the query gives us all the records we need in one call: the accounts in this trigger with the related opportunities of each account.
Then the for loop iterates over them by using the collection variable - in this case acctsWithOpps
. The collection variable holds all the results of the SOQL query. The for loop only operates on the records we want. Because only related records are obtained, no further queries are needed within the loop to get those records.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the accounts and their related opportunities. List<Account> acctsWithOpps = [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities) FROM Account WHERE Id IN :Trigger.New]; // Iterate over the returned accounts for(Account a : acctsWithOpps) { Opportunity[] relatedOpps = a.Opportunities; // Do some other processing } }
If you don't need the account parent records, you can retrieve only the opportunities that are related to the accounts within this trigger context. This list is specified in the WHERE
clause by matching the AccountId field of the opportunity to the ID of accounts in Trigger.New
: WHERE AccountId IN :Trigger.New
. This means that you will not use a nested query. This example shows how to do this.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.New]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Do some other processing } }
- SOQL for loop
You can reduce the previous example in size by combining the SOQL query with the for loop in one statement: the SOQL for loop.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger, // and iterate over those records. for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.New]) { // Do some other processing } }
Triggers execute in batches of 200 at a time. So, if 400 records cause a trigger, the trigger fires twice. For this reason, you don't get the benefit of SOQL for loop record batching in triggers, because triggers batch up as well. The SOQL for loop is called twice in this example, but a standalone SOQL query would also be called twice. However, the SOQL for loop still looks more elegant than iterating over a collection variable.
13.2.4 Performing Bulk DML
You should always aim to perform DML calss on a collection of sObjects where possible. The Apex runtime allows up to 150 DML calls in one transaction.
This example is inefficient as an update
is performed inside the loop. It is called once per opportunity.
trigger DmlTriggerNotBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.New]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; // Update once for each opportunity -- not efficient! update opp; } } }
This example does the same, but the update
is carried out on a list of opportunities:
trigger DmlTriggerBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.New]; List<Opportunity> oppsToUpdate = new List<Opportunity>(); // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; oppsToUpdate.add(opp); } } // Perform DML on a collection update oppsToUpdate; }
Two lists are created - one for the first SOQL query and the second to collect those that need updating. The second list (oppsToUpdate
) collects those that need acting on, and then finally and update is performed on that list, outside of the loop.
13.2.5 Bulk Design Pattern in Action: Trigger Example for Getting Records
We are refactoring the AddRelatedRecord
trigger. In the previous example, it operated in bulk, but could still be more efficient, because it iterates over all Trigger.New
. This example modifies the SOQL query to get only records of interest and then iterate over those records.
The AddRelatedRecord
trigger fires after accounts are inserted or updated and adds a default opportunity for every account that doesn't already have an opportunity.
First problem is to figure out how to get the child opportunity records.
Because this is a after
trigger, we can query affected records from the database. They've already been committed to the database, so we can query them. Let's write a SOQL query that returns all accounts in this trigger that don't have related opportunities:
[SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity)]
Here, the embedded query does the lookup - we just need to find ones that are not in that lookup.
Now that we have these, let's iterate over these with a SOQL for loop:
for(Account a : [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity)]){ }
That is the basic of the trigger. All we need to do now is create the default opportunity, which we will do in bulk.
13.2.6 Challenge for this Unit
To complete this challenge, you need to add a trigger for Opportunity. The trigger will add a task to any opportunity inserted or updated with the stage of 'Closed Won'. The task's subject must be 'Follow Up Test Task'.
The Apex trigger must be called 'ClosedOpportunityTrigger' With 'ClosedOpportunityTrigger' active, if an opportunity is inserted or updated with a stage of 'Closed Won', it will have a task created with the subject 'Follow Up Test Task'. To associate the task with the opportunity, fill the 'WhatId' field with the opportunity ID. This challenge specifically tests 200 records in one operation.
Solution:
trigger ClosedOpportunityTrigger on Opportunity (after insert, after update) { List<Task> taskList = new List<Task>(); for (Opportunity opp : [SELECT Id, StageName FROM Opportunity WHERE StageName = 'Closed Won' AND Id in :Trigger.New ]){ taskList.add(new Task(Subject='Follow Up Test Task', WhatId = opp.Id)); } if (taskList.size() > 0){ insert taskList; } }
14 Apex Testing
14.1 Get Started with Apex Test Units
14.1.1 Apex Unit Tests
The Apex testing framework enables you to write and execute tetsts for your Apex classes and triggers.
Apex code can only be written in a sandbox or Dev org. It can be deployed to production from a dev or sandbox org. You can aslo deploy Apex code by uploading a package via AppExchange.
The benefits of testing are:
- ensure that any Apex class and triggers work as expected
- having a suite of regression tests that can be run everytime classes and triggers are updated to ensure that future updates you make do not break existing functionality
- meeting the code coverage requirements for deploying Apex to production or distributinng Apex to customers via packages
- high-quality apps delivered to production org
- high-quality apps delivered to package subscribers
Apex Hammer is run on your behalf on all tests before any major service upgrade. It does not run in all orgs.
14.1.2 Code Coverage Requirement for Deployment
Before you can deploy your code, at least 75% of it must be covered by tests, and all those tests must pass. Each trigger must have some coverage. Even though code coverage is a requirement for deployment, don't write tests only to meet this requirement. Make sure to test the common use cases in your app, including positive and negative test cases, and bulk and single-recording processing.
14.1.3 Test Method Syntax
Test methods take no arguments and have the following syntax:
@isTest static void testName(){ // code_block }
Alternatively, a test method can have this syntax:
static testMethod void testName(){ // code_block }
Using the isTest
annotation instead of testMethod
keyword is more flexible as you can specify parameters in the annotation.
The visibility of a test method doesn't matter, so declaring a test method as public or private doesn't make a difference as the testing framework is always able to access test methods. For this reason, the access modifiers are ommitted in the syntax.
Test methods must be defined in test classes, which are classes annotated with isTest
. This sample class shows a definition of a test class with one test method:
@isTest private class MyTestClass{ @isTest static void myTest(){ // code_block } }
Test classes can either be public or private. If the class is being used for unit testing only, keep it private. Public test classes are typically used for test data factory classes.
The following example test class is a test with 3 test methods. The class method that's being tested takes a temperature in Fahrenheit as an input and returns a Celsius conversion.
Add this as a new class:
public class TemperatureConverter { // Takes a Fahrenheit temperature and returns the Celsius equivalent. public static Decimal FahrenheitToCelsius(Decimal fh) { Decimal cs = (fh - 32) * 5/9; return cs.setScale(2); } }
Then add the test class:
@isTest private class TemperatureConverterTest { @isTest static void testWarmTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70); System.assertEquals(21.11,celsius); } @isTest static void testFreezingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32); System.assertEquals(0,celsius); } @isTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); System.assertEquals(100,celsius,'Boiling point temperature is not expected.'); } @isTest static void testNegativeTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10); System.assertEquals(-23.33,celsius); } }
The verifications are done with the System.assertEquals()
method, which takes two parameters. The first is the expected value, and the second is the actual value. There is a version of this method that accepts a third, String value, which is a description of the comparison. This is logged if the assertion fails.
14.1.4 To Run a test
In the developer console,
- click Test > New Run
- under Test Classes choose the class that is to be run
- to add all the test methods in the class, click Add Selected
- click Run
- in the Tests Tab, you see the status of your tests as they're running. Expand the test run, and expand again, until you see the list of individual tests that were run. Check for green ticks.
After you've run your tests, code coverage is automatically generated for the Apex classes and triggers in the org. You can check the code coverage percentage in the Tests tab of the Developer Console.
Whenever you modify the code, rerun your tests to refresh code coverage results.
A known issue with Developer Console preventsit from updating code coverage correctly when running a subset of tasks. To update the code coverage, use Test | Run All.
Let's test for a failure. Change testBoilingPoint()
to the following:
@isTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); // Simulate failure System.assertEquals(0,celsius,'Boiling point temperature is not expected.'); }
Check the results in the Tests tab. To get the error message, double click inside the Errors and the console will open up a debug output for that error.
14.1.5 Increase your Code Coverage
Don't just aim for 75%, but instead try to get as high a coverage as possible. The higher coverage, the more likely your code is robust. Even after writing a test method for all your class methods, code coverage is not at 100%. This could be because you are not covering all data values for conditional code execution. For example, some data values tend to be ignored when your class method has if
statements that cause different branches. Ensure that tests are met for these values and branches.
This example contains two if statements. This method returns a priority string value given a lead state. The method validates the state first and returns null
if the state is invalid. If the state is CA, the method returns 'High'; otherwise it returns 'Normal' for any other state.
public class TaskUtil { public static String getTaskPriority(String leadState) { // Validate input if (String.isBlank(leadState) || leadState.length() > 2) { return null; } String taskPriority; if (leadState == 'CA') { taskPriority = 'High'; } else { taskPriority = 'Normal'; } return taskPriority; } }
This is the test class for the above class:
@isTest private class TaskUtilTest { @isTest static void testTaskPriority() { String pri = TaskUtil.getTaskPriority('NY'); System.assertEquals('Normal', pri); } }
It only tests the class with one state. When you check the coverage, you only get 75% as an incorrect state wasn't tested, the state CA wasn't tested. These are added here:
@isTest private class TaskUtilTest { @isTest static void testTaskPriority() { String pri = TaskUtil.getTaskPriority('NY'); System.assertEquals('Normal', pri); } @isTest static void testTaskHighPriority() { String pri = TaskUtil.getTaskPriority('CA'); System.assertEquals('High', pri); } @isTest static void testTaskPriorityInvalid() { String pri = TaskUtil.getTaskPriority('Montana'); System.assertEquals(null, pri); } }
14.1.6 Create and Execute a Test Suite
A test suite is a collection of Apex test classes that you run together. For example, create a suite of tests that you run everytime you prepare for a deplopment or Salesforce releases a new version. Set up a test suite in the Developer Console to define a set of test classes that you execute together regularly.
To create a Test Suite:
- in the Developer Console select Test > New Suite
- enter a name for your new Test suite and click OK
- select the Classes that are required (ctrl-select is allowed here)
- click the right arrow icon to shift them to the right pane
- click save
- click Test > New Suite Run
- select the name of the Test Suite that you wish to run
- select the right arrow to select the Test Suite
- click Run Suites
- on the Tests Tab, monitor the status of your tests as they're running.
- expand the tests to see the outcome of each one
14.1.7 Creating Test Data
Salesforce records that are created in test methods aren't committed to the database. They're rolled back when the test finishes. This means you don't have to clean up each time.
Apex does not have access to pre-existing data in the org, except for access to setup and metadata objects, such as the User or Profile objects. Set up test data for your tests. Creating test data makes your tests more robust and prevents failures that are caused by missing or changed data in the org. You can create test data directly in your test method, or by using a utility test class.
There are times when a test method needs access to pre-existing data. To access org data, annotate the test method with @isTest(SeeAllData=true)
. The test method examples in this unit don't access org data and therefore don't use the SeeAllData
parameter.
- You can save up to 6MB of Apex code in each org. Test classes with @isTest don't count towards this limit.
- Even though data is rolled back, no separate database is used for testing. As a result, for some sObjects, inserting fields with unique constraints will result in an error if a duplicate is inserted
- Test methods don't send emails
- Test methods can't make callouts to external services. You can use mock callouts in tests
- SOSL searches performed in a test return empty results. To ensure predictable results, use
Test.setFixedSearchResults()
to define the records to be returned by the search
14.1.8 Unit Challenge
Install a simple Apex class, write unit tests that achieve 100% code coverage for the class, and run your Apex tests.
The Apex class to test is called 'VerifyDate', and the code is available here. Copy and paste this class into your Developer Edition via the Developer Console. 'VerifyDate' is a class which tests if a date is within a proper range, and if not will return a date that occurs at the end of the month within the range. The unit tests must be in a separate test class called 'TestVerifyDate'. The unit tests must cover scenarios for all lines of code included in the Apex class, resulting in 100% code coverage. Run your test class at least once (via 'Run All' tests the Developer Console) before attempting to verify this challenge.
Answer:
@isTest public class TestVerifyDate { @isTest static void testCheckDates(){ Date date1 = Date.newInstance(2020,11,09); Date date2 = Date.newInstance(2020,10,01); Date date_return = VerifyDate.CheckDates(date1, date2); Date answer = Date.newInstance(2020,11,30); System.assertEquals(answer, date_return); Date date3 = Date.newInstance(2020,11,01); Date date4 = Date.newInstance(2020,11,09); Date date_return1 = VerifyDate.CheckDates(date3, date4); Date answer2 = Date.newInstance(2020,11,09); System.assertEquals(answer2, date_return1); } }
14.2 Test Apex Triggers
Before deploying a trigger, write unit tests to perform the actions that fire the trigger and verify expected results.
This example needs the AccountDeletion class and it needs to be active. (You can do this by searching for Apex Triggers, in the quick find box, then clicking edit on the trigger, and then enabling the Active checkbox.
To create an Apex Trigger Test, in the Development Console, click File > New > Apex Class.
@isTest private class TestAccountDeletion { @isTest static void TestDeleteAccountWithOneOpportunity() { // Test data setup // Create an account with an opportunity, and then try to delete it Account acct = new Account(Name='Test Account'); insert acct; Opportunity opp = new Opportunity(Name=acct.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=acct.Id); insert opp; // Perform test Test.startTest(); Database.DeleteResult result = Database.delete(acct, false); Test.stopTest(); // Verify // In this case the deletion should have been stopped by the trigger, // so verify that we got back an error. System.assert(!result.isSuccess()); System.assert(result.getErrors().size() > 0); System.assertEquals('Cannot delete account with related opportunities.', result.getErrors()[0].getMessage()); } }
The test method first sets up a test account with an opportunity. Next, it deletes the test account, which should fire the AccountDeletion trigger. The test method verifies that the trigger prevented the deletion of the account by checking the value of ~Database.delete()* method. The return value is a Database.DeleteResult object that contains information about the delete operation. The test method verifies that the deletion was not successful and verifies the error message obtained.
Run the test:
- click Test > New Run
- under Test Classes, click TestAccountDeletion
- add all the methods by clicking Add Selected
The TestAccountDeletion test class contains only one test method, which tests for a single account record. Also, this test is a positive case. Always test for more scenarios to ensure the trigger works in all cases, including deleting an account without opportunities and bulk account deletions.
Test data is set up inside the test method, which can be time-consuming as you add more test methods. If you have many test methods, put the test-data creation in a test utility class and call the utility class from multiple test methods. The next unit shows you how to do this.
14.2.1 Unit Challenge
Install a simple Apex trigger, write unit tests that achieves 100% code coverage for the trigger, and run your Apex tests.
The Apex trigger to test is called 'RestrictContactByName', and the code is available here. Copy and paste this trigger into your Developer Edition via the Developer Console. 'RestrictContactByName' is a trigger which blocks inserts and updates to any contact with a last name of 'INVALIDNAME'. The unit tests must be in a separate Apex class called 'TestRestrictContactByName'. The unit tests must cover scenarios for all lines of code included in the Apex trigger, resulting in 100% code coverage. Run your test class at least once (via 'Run All' tests the Developer Console) before attempting to verify this challenge.
@isTest public class TestRestrictContactByName{ @isTest static void TestRestrictContactByName(){ // This contact will insert Contact valid = new Contact(Lastname='Cripps'); // The trigger will reject this insert Contact invalid = new Contact(Lastname='INVALIDNAME'); insert valid; Contact result1 = [SELECT Lastname FROM Contact WHERE Lastname='Cripps']; System.assertEquals('Cripps', result1.Lastname); // try catch will try to catch the error, which we can test try { Test.startTest(); insert invalid; Test.stopTest(); } catch (Exception e){ Boolean expectedExceptionThrown = e.getMessage().contains('Insert failed'); System.AssertEquals(expectedExceptionThrown, true); } } }
14.3 Creating Test Data for Apex Tests
This demonstrates how classes can be reused to set up test data.
14.3.1 Adding a Test Utility Class
This will create a TestDataFactory class which is a special type of class - it's a public class that is annotated with @isTest
and can be accessed from a runningtest. Test utility classes contain methods that can be called by test methods to perform useful tasks, such as setting up test data. Test utility classes are excluded from the org's code size limit.
To add the TestDataFactory
class:
- click File > New > Apex Class
- enter
TestDataFactory
for the class name - click OK
- add code that you need with methods to set up data as required
This is an example: #+BEGINSRC
@isTest public class TestDataFactory { public static List<Account> createAccountsWithOpps(Integer numAccts, Integer numOppsPerAcct) { List<Account> accts = new List<Account>();
for(Integer i=0;i<numAccts;i++) { Account a = new Account(Name='TestAccount' + i); accts.add(a); } insert accts;
List<Opportunity> opps = new List<Opportunity>(); for (Integer j=0;j<numAccts;j++) { Account acct = accts[j]; // For each account just inserted, add opportunities for (Integer k=0;k<numOppsPerAcct;k++) { opps.add(new Opportunity(Name=acct.Name + ' Opportunity ' + k, StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=acct.Id)); } } // Insert all opportunities for all accounts. insert opps;
return accts; } }#+ENDSRC This adds a given number of Accounts (as specified by the numAccts parameter) and a number of Opportunities per account (as specified by numOppsPerAcct).
The first loop creates all the accounts necessary, and then the second loop creates opportunities for each account.
You can call this with TestDataFactory.createAccountsWithOpps(x, y);
.
The all important part is the return
statement that returns the new accounts.
You can get the related opportunities in the test by using SOQL to look them up.
14.3.2 Calling Utility Methods for Test Data Creation
To store all the accounts use the snippet above, but assign it to a list of Accounts:
Account[] accts = TestDataFactory.createAccountsWithOpps(1,1);
The array contains one account with one opportunity.
The code can now be changed (I've left the old code in, but commented it out to show what was replaced):
@isTest private class TestAccountDeletion { @isTest static void TestDeleteAccountWithOneOpportunity() { // Test data setup // Create one account with one opportunity by calling a utility method Account[] accts = TestDataFactory.createAccountsWithOpps(1,1); // Perform test Test.startTest(); Database.DeleteResult result = Database.delete(accts[0], false); Test.stopTest(); // Verify that the deletion should have been stopped by the trigger, // so check that we got back an error. System.assert(!result.isSuccess()); System.assert(result.getErrors().size() > 0); System.assertEquals('Cannot delete account with related opportunities.', result.getErrors()[0].getMessage()); } }
The insert accts
happens automatically in the TestDataFactory class.
14.3.3 Testing for Different Conditions
One test method is not enough to test all the possible inputs for the trigger. We need to test some other conditions, such as when an account without opportunities is deleted. We also need to test the same scenarios with a bulk number of records instead of just a single record. Here is an update of the test class that contains the three additional test methods:
@isTest private class TestAccountDeletion { @isTest static void TestDeleteAccountWithOneOpportunity() { // Test data setup // Create one account with one opportunity by calling a utility method Account[] accts = TestDataFactory.createAccountsWithOpps(1,1); // Perform test Test.startTest(); Database.DeleteResult result = Database.delete(accts[0], false); Test.stopTest(); // Verify that the deletion should have been stopped by the trigger, // so check that we got back an error. System.assert(!result.isSuccess()); System.assert(result.getErrors().size() > 0); System.assertEquals('Cannot delete account with related opportunities.', result.getErrors()[0].getMessage()); } @isTest static void TestDeleteAccountWithNoOpportunities() { // Test data setup // Create one account with no opportunities by calling a utility method Account[] accts = TestDataFactory.createAccountsWithOpps(1,0); // Perform test Test.startTest(); Database.DeleteResult result = Database.delete(accts[0], false); Test.stopTest(); // Verify that the deletion was successful System.assert(result.isSuccess()); } @isTest static void TestDeleteBulkAccountsWithOneOpportunity() { // Test data setup // Create accounts with one opportunity each by calling a utility method Account[] accts = TestDataFactory.createAccountsWithOpps(200,1); // Perform test Test.startTest(); Database.DeleteResult[] results = Database.delete(accts, false); Test.stopTest(); // Verify for each record. // In this case the deletion should have been stopped by the trigger, // so check that we got back an error. for(Database.DeleteResult dr : results) { System.assert(!dr.isSuccess()); System.assert(dr.getErrors().size() > 0); System.assertEquals('Cannot delete account with related opportunities.', dr.getErrors()[0].getMessage()); } } @isTest static void TestDeleteBulkAccountsWithNoOpportunities() { // Test data setup // Create accounts with no opportunities by calling a utility method Account[] accts = TestDataFactory.createAccountsWithOpps(200,0); // Perform test Test.startTest(); Database.DeleteResult[] results = Database.delete(accts, false); Test.stopTest(); // For each record, verify that the deletion was successful for(Database.DeleteResult dr : results) { System.assert(dr.isSuccess()); } } }
14.3.4 Unit Challenge
Create a contact test factory. Create an Apex class that returns a list of contacts based on two incoming parameters: one for the number of contacts to generate, and the other for the last name. The list should NOT be inserted into the system, only returned. The first name should be dynamically generated and should be unique for each contact record in the list.
The Apex class must be called 'RandomContactFactory' and be in the public scope. The Apex class should NOT use the @isTest annotation. The Apex class must have a public static method called 'generateRandomContacts' (without the @testMethod annotation). The 'generateRandomContacts' method must accept an integer as the first parameter, and a string as the second. The first parameter controls the number of contacts being generated, the second is the last name of the contacts generated. The 'generateRandomContacts' method should have a return type of List<Contact>. The 'generateRandomContacts' method must be capable of consistently generating contacts with unique first names. For example, the 'generateRandomContacts' might return first names based on iterated number (i.e. 'Test 1','Test 2'). The 'generateRandomContacts' method should not insert the contact records into the database.
Solution:
public class RandomContactFactory { public static List<Contact> generateRandomContacts(Integer count, String lName){ List<Contact> contacts = new List<Contact>(); for(Integer i=0; i<count; i++){ String fName = 'Test '+ String.valueOf(i); Contact b = new Contact(Firstname = fName, Lastname = lName); contacts.add(b); } return contacts; } }
15 Visualforce Basics
Visualforce is a web development framework that allows developers to build custom interfaces for mobile and desktop apps that are hosted on the Lightning Platform.
Developers can extend Salesforce built-in features and replace them with new functionality, and build completely new apps. This is familiar to anyone who has built web apps. You compose Visualforce pages with HTML and original styling elements. Each page is accessed by a unique URL.
This is a short example of a Visualforce page:
#+BEGINSRC<apex:page standardController="Contact" > <apex:form > <apex:pageBlock title="Edit Contact"> <apex:pageBlockSection columns="1"> <apex:inputField value="{!Contact.FirstName}"/> <apex:inputField value="{!Contact.LastName}"/> <apex:inputField value="{!Contact.Email}"/> <apex:inputField value="{!Contact.Birthdate}"/> </apex:pageBlockSection> <apex:pageBlockButtons > <apex:commandButton action="{!save}" value="Save"/> </apex:pageBlockButtons> </apex:pageBlock> </apex:form> </apex:page> #+ENDSRC This displays a contact data entry form.
This markup does a lot:
- it connects to the visualforce standard controller, a part of the visualforce framework that provides automatic data access and modification, standard actions, and more
- when accessed without a record ID, the page displays a blank data entry form. When you click Save, a new record is created from form data
- when accessed with a record ID, the page looks up the data for that contact record and displays it in an editable form. When you click Save, your changes for the contact are saved back to the database
- each input field is smart about how it presents a value
- the email field knows what a valid email address looks like, and displays an error if an invalid email is entered
- the date field displays a date widget when you click into the field to make entering a date easier
- The Save button calls the save action method, one of the standard actions provided by the standard controller
15.1 Where you can use Visualforce
15.1.1 Open a Visualforce Page from the App Launcher
Your visualforce apps and custom tabs are all available from the App Launcher. To open the App Launcher, click the 9 dotted icon. To see all apps and items, select View All.
Click a custom app to activate it.
15.1.2 Add a Visualforce Page to the Navigation Bar
You can add visualforce tabs to an app and they display as items in the app's navigation bar.
15.1.3 Display a Visualforce Page within a Standard Page Layout
Extend your page layouts by embedding Visualforce pages on them to display completely custom content on a standard page. The behaviour here is identical to Salesforce Classic, except you need to view the record's Details to see the page layout.
15.1.4 Add a Visualforce Page as a Component in the Lightning App Builder
When you create a custom app page in the Lightning App Builder, you can add a Visualforce page to the page by using Visualforce component.
Note: you should enable "Available for Lightning Experience, Lightning Communities, and the mobile app" for a Visualforce page to make it available in the Lightning App Builder.
15.1.5 Lanunch A Visualforce Page as a Quick Action
You can add your page to the quick actions. You add them to the appropriate publisher area on the object's page layout.
15.1.6 Display a Visualforce Page by Overriding Standard Buttons or Links
You can override the actions available on an object with a Visualforce page. When the user clicks a button or link that has been overridden, your page displays instead of the standard page. This is the same setup as Salesforce Classic.
15.1.7 Display a Visualforce Page using Custom Buttons or Links
You can create new actions for your objects, in the form of buttons and links, by defining them on an object. Javascript buttons and links aren't supported in Lightning Experience, but Visualforce (and URL) items are. The process of defining Visualforce buttons and links is identical to that in Salesforce Classic so we won't bother to show it here.
15.2 Create and Edit Visualforce Pages
Each visualforce tag corresponds to a coarse or fine-grained user interface component, such as a selection of a page, a list view, or an individual field. Visualforce boasts nearly 150 built-in components, and provides a way for developers to create their own components. Visualforce markup can be freely mixed with HTML, CSS and javascript libraries, giving you considerable flexibility in how you implement your app's user interface.
You can view, create and edit Visualforce pages several different ways in Salesforce. Visualforce pages can also be created and modified using Salesforce APIs, enabling a variety of external tools.
15.2.1 Creating Visualforce Pages in Developer Console
Use the Developer Console to create and edit Visualforce pages, with access to other powerful Lightning Platform development tools.
The Development Console has automatic syntax highlighting, tag pair matching, auto-suggest and auto-complete, smart indenting, and many other features.
Follow these steps to create a Visualforce Page in the Developer Console:
- open the developer console
- click File > New > Visualforce Page
- provide the name for the page
- enter your markup
- Save
- to see your new page click Preview (top left)
- Preview opens a new page. This shows you your page without Salesforce styling. To see your page in the context of the Lightning Experience, return to your main Lightning Experienc browser window. Open your browser's Javascript console and enter the following code - don't forget to replace the
pageName
with your page's name:
$A.get("e.force:navigateToURL").setParams( {"url": "/apex/pageName"}).fire();
This javascript fires the Lightning Experience navigateToURL* event, and is the equivalent of entering the classic ~/apex/PageName
URL - you can even see that URL pattern in the code
- in the editor, add some additional text to your page and save
- the preview window automatically refreshes to reflect your changes when you save the page.
Click File > Open to see a list of existing Visualforce Pages.
15.2.2 Add Atttributes Using Auto-Suggest
Set attributes on Visualforce components to customize their behaviour. Use auto-suggest to quickly add attributes and values to Visualforce component tag.
Follow these steps to add and modify attributes on a Visualforce tag:
- in the
<apex:page>
tag before the closing bracket, type a space and thens
. Autocomplete will popup. You can add a sidebar.
If pop-ups don't have what you need, you might see it at the Standard Component Reference at https://developer.salesforce.com/docs/atlas.en-us.224.0.pages.meta/pages/pages_compref.htm
15.2.3 Add and Compose Components to Form a Page Structure
Add components to the Visualforce page to build the page's structure.
Follow these steps to add new Visualforce tags to your page, and use them to create a structure for the page:
- add a
<apex:pageBlock>
component - add a title attribute
<apex:pageBlock title="A Block Title">
- add a
<apex:pageBlockSection>
Now you have a page that displays some structure, with a top-level title, a collapsible section with a title, and some plain text.
<apex:page> <h1>Hello World</h1> <apex:pageBlock title="A Block Title"> <apex:pageBlockSection title="A Section Title"> I'm three components deep! </apex:pageBlockSection> </apex:pageBlock> </apex:page>
Add another pageBlockSection
. You cannot use a child <apex:pageBlockSection>
tag without putting it inside a parent <apex:pageBlock>
tag.
You can also edit pages through the Setup by entering Visualforce Pages in the Quick find box. This is a more limited editor.
The <apex:pageBlock>
and <apex:pageBlockSection>
components you added to your page render a user interface that matches that of Salesforce Classic interface elements. There are other components that also let you match the platform visual style, including <apex:pageBlockSectionItem>
, <apex:pageBlockButtons>
.
15.2.4 Unit Challenge
Create a Visualforce page without the standard Salesforce header and display an image using the Visualforce image component. Challenge Requirements The page must be named DisplayImage It must NOT display the standard Salesforce header It must use a Visualforce apex:image component to display this image -
Solution:
<apex:page showHeader="false"> <apex:image id="Logo" alt="Logo" url="https://developer.salesforce.com/files/salesforce-developer-network-logo.png" /> </apex:page>
15.3 Use Simple Variables and Formulas
15.3.1 Introduction to Global Variables and Visualforce Expressions
Visualforce can display data according to variables (e.g. from the database and/or according to who is logged in). Dynamic data is accessed in markup through the use of global variables, calculations, and properties made available by the page's controller. Together these are described as Visualforce expressions. Use expressions for dynamic output or passing values into components by assigning them to attributes.
A visualforce expression is any set of literal values, variables, sub-expressions or operators that can be resolved to a single value. Method calls aren't allowed in expressions.
The expression syntax in Visualforce is {! expression}
.
Anything inside the curly brackets delimeters is evaluated and dynamically replaced when the page is rendered, or when the value is used. Whitespace is ignored.
15.3.2 Global Variables
Use global variables to access and display system values and resources in your Visualforce markup. For example, Visualforce provides information about the logged-in user in a global variable called $User
. You can access fields of the $User
global variable (and others) using the expression {! $GlobalName.fieldName}
.
This will print information about the user:
{! $User.FirstName } {! $User.LastName } {! $User.Username }
Visualforce expressions are case-insensitive and spaces within the {! ... }
are ignored.
15.3.3 Formula Expressions
Visualforce lets you use more than just global variables in the expression language. It also supports formulas that let you manipulate values.
For example, the &
character is the formula language operator that concatenates strings.
You can change the User.FirstName, User.LastName example above with the below:
{! $User.FirstName & ' ' & $User.LastName }
This will calculate dates:
<p> Today's Date is {! TODAY() } </p> <p> Next week it will be {! TODAY() + 7 } </p>
This uses the TODAY() function. These are built in calculations:
<p>The year today is {! YEAR(TODAY()) }</p> <p>Tomorrow will be day number {! DAY(TODAY() + 1) }</p> <p>Let's find a maximum: {! MAX(1,2,3,4,5,6,5,4,3,2,1) } </p> <p>The square root of 49 is {! SQRT(49) }</p> <p>Is it true? {! CONTAINS('salesforce.com', 'force.com') }</p>
Some functions take parameters which are passed in the brackets. The CONTAINS()
function is interesting. It returns a boolean value: true or false. It compares two arguments of text and returns true if the first argument contains the second argument. If it doesn't it returns false.
15.3.4 Conditional Expressions
Use a conditional expression to display different information based on the value of an expression. For example, if an invoice has no line items, you might want to display the word "none" instead of an empty list. Or if an item has expired, you might want to display "Ended" instead of showing the date and time.
You can do this in Visualforce by using a conditional formula expression such as IF()
. The IF()
expression takes three arguments:
- the first is a boolean (something that is true or false)
- the second argument is the value returned if the parameter is true
- the third argument is the value returned if the parameter is false
This is an example of an IF()
function:
<p>{! IF( CONTAINS('salesforce.com','force.com'), 'Yep', 'Nope') }</p> <p>{! IF( DAY(TODAY()) < 15, 'Before the 15th', 'The 15th or after') }</p>
The IF() function makes the CONTAIN() function more useful (you can display more than just true or false).
This will print the username if the user is active, or "inactive" if the user is not logged in.
({! IF($User.isActive, $User.Username, 'inactive') })
More global variables are found on the global variables reference here: https://developer.salesforce.com/docs/atlas.en-us.224.0.pages.meta/pages/pages_variables_global.htm
15.3.5 Unit Challenge
Create a Visualforce page that displays the first name of the logged-in user. Challenge Requirements
- The page must be named DisplayUserInfo
- The displayed user information must be generated dynamically from the logged-in user
15.4 Use Standard Controllers
Visualforce uses the traditional model-view-controller (MVC) paradigm. These are called the standard controllers.
The MVC design pattern makes it easy to separate the view and its styling from the underlying database and logic. The view (Visualforce Page) interacts with a controller, and the controller provides functionality to the page. The controller can contain the logic to be executed when a button is clicked. A controller also typically interacts with the model (the database) - making available data that the view might want to display, or pushing changes back to the database.
Most standard and all custom objects have standard controllers that can be used to interact with the data associated with the object, so you don't need to write code for the controller yourself. You can extend the standard controllers to add new functionality, or create custom controllers from scratch.
15.4.1 Find a Record ID and Add It to the Request URL
Provide the record ID to the standard controller by adding it as a parameter in the request URL. To use the standard controller to reference a specific record, it needs to know the record identifier, or the ID, of the record to work with. It uses the ID to retrieve the data and to save it back to the database when the record data is changed.
While you are working on a Visualforce page in development, it won't have an ID to get a record for. Therefore, for testing you provide a GET
parameter in the request URL. To do this, get an ID from a live salesforce org and click on a record that relates to the request URL that you will be building (for example, an Account record).
- open up the account and copy the long string which is the unique ID
- switch back to the preview page and post
&id=
with the id at the end - press return to load that URL
This doesn't necessarily change the appearance of the page yet, but it will come in useful later on.
Remeber that you can also look at the page in the Lightning Experience by passing the following code in to a Javascript console:
$A.get("e.force:navigateToURL").setParams( {"url": "/apex/pageName"}).fire();
To preview a page with a specific ID, you can pass that in the javascript snippet:
$A.get("e.force:navigateToURL").setParams( {"url": "/apex/pageName?&id=00141000004jkU0AAI"}).fire();
15.4.2 Display Data from a Single Record
Add the standard controller for accounts to the page, and then reference account fields to display a record's data.
To add the standard controller, you will need to add the following attribute to the <apex:page>
tag:
standardController="Account"
Now you can add markup to view details of the record:
Name: {! Account.Name } <br/> Phone: {! Account.Phone } <br/> Industry: {! Account.Industry } <br/> Revenue: {! Account.AnnualRevenue } <br/>
There is a lot of functionality being looked after the standard controllers here.
- When a page is loaded, and the <apex:page> component is activated, it activates a standard controller for the account object
- The standard controller sees that there's an ID parameter field in the URL and searches for it and retrieves the matching account record
- The standard controller takes the record and puts it in a variable that it makes available to the page. The variable has the same name as the standard controller's sObject:Account. It's an object variable, and contains all of the fields available on the Account sObject.
- The four Visualforce expressions all reference the Account variable. They use dot notation to access individual fields within the Account variable. So
{! Account.Name }
gets the name of the account, and so on.
You can also format fields differently if they are displaying incorrectly.
15.4.3 Display Fields from Related Records
Use dot notation to display data from related records.
For example, while viewing an Account object, you might have noticed that the Account object has a field called Account Owner, and that its type is a Lookup(User). This field creates a relationship to a User record. By clicking the Account Owner field label link, you'll discover its Field Name is Owner.
The Owner relationship represents a User. And, if from Setup you go to Object Manager > Fields & Relationships, you'll find that the User has a Name field. We can display this with this snippet:
Account owner: {! Account.Owner.Name } <br/>
You traverse the path from the Account, via the Owner field to the User object.
Standard controller also provides methods to edit, create and delete records.
If you want to embed visualforce pages within an object's page layouts, use as object-specific custom actions, or use as mobile cards in the Salesforce app must use the standard controller for the relevant object.
If you've created custom objects and want to know how to reference a field, you have to follow a slightly different procedure. You'll need to check it's API name (ending in __c
) and reference it with that.
15.4.4 Unit Challenge
Using the Contact standard controller, create a Visualforce page which displays a Contact's First Name, Last Name and the Email address of the Contact's Owner. Challenge Requirements
The page must be named ContactView It must reference the Contact standard controller It should include three bind variables that use the standard controller to display the following Contact record information: First Name Last Name Owner Email
Solution:
<apex:page standardController="Contact"> <apex:pageBlock title="Contact"> <apex:pageBlockSection > {! Contact.FirstName } {! Contact.LastName } {! Contact.Owner.Email } </apex:pageBlockSection> </apex:pageBlock> </apex:page>
15.5 Display Records, Fields and Tables
15.5.1 Introduction to Output Components
Components are rendered to HTML, CSS and Javascript when a page is requested. Coarse-grained components provide a significant amount of functionality in a single component, andd might add a lot of information and user interface to the page it is used on.
Fine-grained components provide more focused functionality, and enable you to design the page to look and behave the way you want.
This will focus on output components - those components that output data from a record and enable you to design a view-only interface.
15.5.2 Create a Visualforce Page with a Standard Controller
Use output components with a standard controller to make it easy to access and display record details.
Our page will have a typical Account.Name in it:
<apex:page standardController="Account"> {! Account.Name } </apex:page>
15.5.3 Display Record Details
Use <apex:detail>
to quickly add record details to a page that uses a standard controller.
Some output components bring a lot to the party. These "coarse-grained" components offer a lot of functionality, displaying many fields, labels, and other user interface elements. They let you quickly build pages that are variations on the built-in Salesforce user interface.
Replace the {! Account.Name }
with the following code:
<apex:detail />
This includes nearly all the details for that record.
<apex:detail>
is a coarse-grained output component that adds many fields, sections, buttons and other user interface elements in just one line of markup.
You can change the styling with attributes of this markup tag. More can be read about that here https://trailhead.salesforce.com/content/learn/modules/lex_dev_visualforce/lex_dev_visualforce_design.
Now add this code below the apex:detail tag:
<apex:relatedList list="Contacts"/> <apex:relatedList list="Opportunities" pageSize="5"/>
The page should now display two related lists, for contacts and opportunities.
The <apex:relatedList>
component is another coarse-grained component, but it is lower level than <apex:detail>
. The former shows many related lists at once (or none). The latter lets you see one related list at a time.
15.5.4 Display Individual Fields
Use <apex:outputField>
to display individual fields from a record. When you need even more control over your page layout, you can add fields individually.
Add the following to your page as individual fields:
<apex:outputField value="{! Account.Name }"/> <apex:outputField value="{! Account.Phone }"/> <apex:outputField value="{! Account.Industry }"/> <apex:outputField value="{! Account.AnnualRevenue }"/>
The formatting isn't as expected. The fields are all displayed on one line, without labels and without any other formatting. When you wrap these in <apex:pageBlock>
and <apex:pageBlockSection>
components, it's behaviour changes quite a bit.
The pageBlock and pageBlockSection components are required to activate the platform look and feel.
Although outputField seems like a fine-grained component, because it only outputs one field, it's actually doing quite a lot. It knows which components it is being used inside and adapts to that.
15.5.5 Display a Table
Use <apex:pageBlockTable>
to add a table of data to a page.
What is a relatedList?
- it grabs a list of similar data elements
- it sets up a table with columns for each field, headers atop each column and so on
- for each item in the list - for each related contact - it adds a row to the table and fills in each column with the appropriate detail from that record
You can do the same thing in your own Visualforce markup using iteration components. For example {! Account.contacts }
is an expression that evaluates to a list of contacts for an account. You can use this expression with an iteration component to create a list or table with details of these related contacts.
This Visualforce markup will show the contacts for a given account, using a table (and iterating over the contacts:
<apex:pageBlock title="Contacts"> <apex:pageBlockTable value="{!Account.contacts}" var="contact"> <apex:column value="{!contact.Name}"/> <apex:column value="{!contact.Title}"/> <apex:column value="{!contact.Phone}"/> </apex:pageBlockTable> </apex:pageBlock>
<apex:pageBlockTable>
is an iteration component that generates a table of data, complete with platform styling. This is what is going on in the markup:
- the value attribute of pageBlockTable is set to the expression mentioned previously,
{! Account.contacts }
. This is the list of records that the pageBlockTable will work with - for each record in that list, one record at a time, pageBlockTable assigns that record to the variable named in the
<apex:pageBlockTable>
's var attribute. In this case, that variable is named contact - for each record,
<apex:pageBlockTable> constructs a new row in the table, using the row defined by the set of ~<apex:column>
components within the body of<apex:pageBlockTable>
. The<apex:column>
components, in turn, use the contact variable that represents the current record to pull out the field values for that record - outside of the loop,
<apex:pageBlockTable>
uses the fields in the<apex:column>
component to create column headers, by looking up the label for each field
15.5.6 Unit Challenge
Create a page which displays a subset of Opportunity fields using apex:outputField components. Bind the Name, Amount, Close Date and Account Name fields to the apex:outputField components. Challenge Requirements
The page must be named OppView It must reference the Opportunity standard controller It must have four apex:outputField components bound to the following Opportunity fields: Opportunity Name Amount Close Date Account Name of the Opportunity
Solution:
<apex:page standardController="Opportunity"> <apex:pageBlock title="Opportunity Details"> <apex:pageBlockSection> <apex:outputField value="{! Opportunity.Name }"/> <apex:outputField value="{! Opportunity.Amount }"/> <apex:outputField value="{! Opportunity.CloseDate }"/> <apex:outputField value="{! Opportunity.Account.Name }"/> </apex:pageBlockSection> </apex:pageBlock> </apex:page>
15.6 Input Data Using Forms
15.6.1 Introduction to Visualforce forms
Visualforce provides everything needed to easily create pages that create new records, edit its values and save the changes back to the database.
This will combine the <apex:form>
component and a few easy to understand form elements to create a sophisticated record editing page.
15.6.2 Create a Basic form
Use <apex:form>
and <apex:inputField>
to create a page to edit data. Combine <apex:commandButton>
with the save action built into the standard controller to create a new record, or save changes to an existing one.
A page that lets someone edit and update a record needs to do all the following:
- look up the record to edit and retrieve it from the database
- put the relevant record data on the page in an editing form
- receive a form submission with changed data
- validate the new values
- save valid changes back to the database
- provide appropriate messages to the person about the status of the save
To create a form:
- in the developer console, create a new visualforce page
- in the editor provide this markup:
<apex:page standardController="Account"> <h1>Edit Account</h1> <apex:form> <apex:inputField value="{! Account.Name }"/> <apex:commandButton action="{! save }" value="Save" /> </apex:form> </apex:page>
You will need the following code to preview:
$A.get("e.force:navigateToURL").setParams( {"url": "/apex/AccountEdit?&id=0014K000006szZlQAI"}).fire();
This has carried out quite a few processes:
- the page uses the ID to fetch the data
- the form creates the form with the data
- the inputField has the entry for the field
- the command button allows a save back to the database
15.6.3 Add Field Labels and Platform Styling
Place form elements within pageBlock and pageBlockSection tags to organize and group them.
- inside the form, component, wrap the two form elements in
<apex:pageBlock>
and<apex:pageBlockSection>
tags:
<apex:page standardController="Account"> <apex:form> <apex:pageBlock title="Edit Account"> <apex:pageBlockSection> <apex:inputField value="{! Account.Name }"/> </apex:pageBlockSection> <apex:pageBlockButtons> <apex:commandButton action="{! save }" value="Save" /> </apex:pageBlockButtons> </apex:pageBlock> </apex:form> </apex:page>
The styling now chages and the form is different.
When you use input components, they use the same styling provided by the surrounding tags.
Add three more fields:
<apex:pageBlockSection columns="1"> <apex:inputField value="{! Account.Name }"/> <apex:inputField value="{! Account.Phone }"/> <apex:inputField value="{! Account.Industry }"/> <apex:inputField value="{! Account.AnnualRevenue }"/> </apex:pageBlockSection>
15.6.4 Display Form Errors and Messages
Use <apex:pageMessages>
to display any form handling error or messages.
This helps provide useful feedback to the user when something goes wrong. The standard controller handles that for you. All you need to do is to tell it where to put the message on the page:
- put the following markup under the pageblock opening tag
<apex:pageMessages />
If there is a validation problem (e.g. a mandatory field missing, you will get an error box.
15.6.5 Edit Related Records
This makes it easier for your users, by providing links to related records. Up to now, we have only edited one record, as we have used the controller that allows that, so can only make changes on that object. But what about related objects?
Below the closing pageBlock tag, add the following:
<apex:pageBlock title="Contacts"> <apex:pageBlockTable value="{!Account.contacts}" var="contact"> <apex:column> <apex:outputLink value="{! URLFOR($Action.Contact.Edit, contact.Id) }"> Edit </apex:outputLink> <apex:outputLink value="{! URLFOR($Action.Contact.Delete, contact.Id) }"> Del </apex:outputLink> </apex:column> <apex:column value="{!contact.Name}"/> <apex:column value="{!contact.Title}"/> <apex:column value="{!contact.Phone}"/> </apex:pageBlockTable> </apex:pageBlock>
This adds a Table view of the related contacts and an edit button.
The above <apex:pageBlockTable>
component creates a table with a list of the account's contacts. What's new is the column with the <apex:outputLink>
components. These use an expression that references a global variable, $Action
, and combines it with the contact's ID inside the URLFOR()
function. The result is a link to the contact's edit or delete page depending on the action referenced.
For select lists and radio buttons look for components that start with <apex:select>
instead.
The actions here are standard actions. There's a core set that are available for all objects with standard controllers, but many of the built-in standard objects have additional actions that you can use.
To create a new contact, it gets more complicated as there is no referenced lookup. You will need to write a custom controller of your own.
15.6.6 Unit Challenge
Using the Visualforce apex:form component, create a page which will insert a Contact record based on First Name, Last Name and Email. After submitting the form, the user should be redirected to detail page of the new Contact record. Challenge Requirements
The page must be named CreateContact It must reference the Contact standard controller It must use a Visualforce apex:form component It must have three apex:inputField components bound to the following Contact fields: First Name Last Name Email It must have an apex:commandButton component that uses the save method from the standard controller
Solution:
<apex:page standardController="Contact"> <apex:form> <apex:pageBlock> <apex:pageBlockSection> <apex:inputField value="{! Contact.Firstname }" /> <apex:inputField value="{! Contact.Lastname }" /> <apex:inputField value="{! Contact.Email }" /> </apex:pageBlockSection> <apex:pageBlockButtons> <apex:commandButton action="{! save }" value="Save" /> </apex:pageBlockButtons> </apex:pageBlock> </apex:form> </apex:page>
15.6.7 Use Standard List Controllers