Advanced Business Logic - Development with the Force.com Platform: Building Business Applications in the Cloud, Third Edition (2014)

Development with the Force.com Platform: Building Business Applications in the Cloud, Third Edition (2014)

5. Advanced Business Logic

In the preceding chapter, you learned the basics of the Apex language for developing business logic. This chapter extends your knowledge of Apex to reach more features of the Force.com platform. The following topics are covered:

Image Aggregate SOQL queries—Aggregate queries operate on groups of records, summarizing data declaratively at the database level rather than in Apex.

Image Additional SOQL features—SOQL includes features for querying related objects and multi-select picklists.

Image Salesforce Object Search Language (SOSL)—SOSL is a full-text search language, a complement to SOQL, that allows a single query to search the textual content of many database objects and fields.

Image Transaction processing—Apex includes database methods to enable the partial success of transactions, saving and restoring of database state, and locking of records returned from a query.

Image Apex managed sharing—Managed sharing allows programmatic control over record-level sharing.

Image Sending and receiving email—Apex programs can send and receive email with support for text and binary attachments and templates for standardizing outbound messages.

Image Dynamic Apex—Execute database queries that aren’t hard-coded into your programs, query Force.com for your database’s metadata, and write generic code to manipulate database records regardless of their type.

Image Custom settings in Apex—Data from custom settings can be retrieved, created, updated, and deleted from Apex.

Image Sample application—The Services Manager sample application is enhanced to send email notifications to users when a business event occurs.


Note

The code listings in this chapter are available in a GitHub Gist at http://goo.gl/q65M4.


Aggregate SOQL Queries

SOQL statements that summarize or group records are called aggregate queries. Aggregate queries in SOQL run at the database level rather than in Apex. This results in much better performance and simpler code. This section covers three aspects of aggregate SOQL queries:

Image Aggregate functions—Rather than simply returning the discrete values of a database field in a SELECT statement, aggregate functions such as SUM apply a simple calculation on each record and return the accumulated result.

Image Grouping records—The GROUP BY syntax works with aggregate functions to return a set of summarized results based on common values.

Image Grouping records with subtotals—SOQL provides two special forms of the GROUP BY syntax to calculate subtotals and return them in the query results.

Aggregate Functions

Aggregate functions in SOQL work much like their SQL counterparts. They are applied to fields in the SELECT list. After you include an aggregate function in a query, nonaggregate fields in the same query are not allowed. The six aggregate functions available in SOQL are

Image AVG—Calculates an average of the values in a numeric field.

Image COUNT—Counts the values in a numeric, date, or string field, including duplicate values but not nulls. Unlike all other aggregate functions, the argument to COUNT is optional.

Image COUNT_DISTINCT—Counts the unique values in a numeric, date, or string field, excluding nulls.

Image MIN—Returns the minimum value in a numeric, date, or string field. The minimum of a string field is the first value when values are sorted alphabetically. If the string is a picklist type, the minimum is the first value in the picklist.

Image MAX—Returns the maximum value in a numeric, date, or string field. The maximum of a string field is the last value when values are sorted alphabetically. If the string is a picklist type, the maximum is the last value in the picklist.

Image SUM—Computes the sum of values in a numeric field.

All queries containing aggregate functions return a special Apex object called AggregateResult, except the no-argument form of COUNT, which returns an integer. The AggregateResult object contains the aggregate values calculated by running the query. They have default field names expr0 for the first field, expr1, and so forth. Alternatively, you can provide an alias immediately following the aggregate function column to provide a friendlier label for the value in your code. Aggregate result fields are accessed using the get method.

To get started with aggregate functions in Apex, open Force.com IDE’s Execute Anonymous view and type in and run the code given in Listing 5.1.

Listing 5.1 Returning the Record Count


Integer i = [ SELECT COUNT() FROM Timecard__c ];
System.debug(i);


This code prints the number of records contained in the Timecard__c object to the debug log. The SOQL query returns an integer because it uses the no-argument form of the COUNT aggregate function. In contrast, the example in Listing 5.2 uses the SUM aggregate function and returns anAggregateResult object, with an alias Total specified on the aggregate column. Note that if an alias were not specified, the aggregate column would be named expr0.

Listing 5.2 Calculating a Sum


AggregateResult r = [ SELECT SUM(Total_Hours__c) Total
FROM Timecard__c ];
System.debug(r.get('Total'));



Note

Normal SOQL governor limits apply to aggregate functions. The number of records used to compute an aggregate result are applied toward the limit on records returned. So although your COUNT query returns a single result record, if it counted more than 50,000 records, your query will fail with an exception. If such a failure is disruptive to your application, make sure you use a WHERE clause to reduce the number of records that are processed in the query. The LIMIT keyword is not allowed in queries with aggregate functions, except for the special form of the COUNT function that has no field argument.


Grouping Records

SOQL provides the GROUP BY syntax for grouping records by one or more fields. When a query contains a grouping, its results are collapsed into a single record for each unique value in the grouped field. Because you can no longer return individual field values, all fields not specified as grouped must be placed within aggregate functions.

Listing 5.3 shows a simple example of grouping records without aggregate functions. It examines all the records in the Contact object and returns only the unique values of the field Region__c.

Listing 5.3 Returning Unique Records by Grouping Them


for (AggregateResult r : [ SELECT Region__c FROM Contact
GROUP BY Region__c ]) {
System.debug(r.get('Region__c'));
}


Although aggregate functions can be used alone in a simple query, they are much more powerful when used in conjunction with record groupings. Listing 5.4 demonstrates aggregate functions with record groupings. It groups all Timecard records by the geographic region of the consultant (Contact) who performed the work, and sums their reported hours. This results in one record per geographic region with the region’s name and a sum of their timecard hours.

Listing 5.4 Using Aggregate Functions with Record Groupings


for (AggregateResult r : [ SELECT Contact__r.Region__c,
SUM(Total_Hours__c) FROM Timecard__c
GROUP BY Contact__r.Region__c ]) {
System.debug(r.get('Region__c') + ' ' + r.get('expr0'));
}


You’re already familiar with the WHERE keyword in SOQL for filtering query results using Boolean expressions. Filtering on the results of aggregate functions requires the HAVING keyword. It works just like WHERE, but the field being filtered must be wrapped with an aggregate function and included in the GROUP BY list.

The code in Listing 5.5 outputs the average hourly cost rates for consultants by education level, but excludes records at or below an average cost rate of $100. The filtering of the average cost rates is specified by the HAVING keyword.

Listing 5.5 Filtering Grouped Records by Aggregate Function Values


for (AggregateResult r : [ SELECT Highest_Education_Level__c ed,
AVG(Hourly_Cost_Rate__c) FROM Contact
GROUP BY Highest_Education_Level__c
HAVING AVG(Hourly_Cost_Rate__c) > 100 ]) {
System.debug(r.get('ed') + ' ' + r.get('expr0'));
}


Grouping Records with Subtotals

Two special forms of grouping in SOQL produce subtotals and grand totals for the record groupings specified in the query. They are GROUP BY ROLLUP and GROUP BY CUBE, and they replace GROUP BY syntax and support up to three grouped fields. These functions make it easier for developers to produce cross-tabular or pivot-style outputs common to reporting tools, where groups become the axes and aggregate values are the cells. The Force.com database calculates the totals and provides them in-line, in the results, eliminating the need to write Apex to postprocess the data.

Listing 5.6 demonstrates GROUP BY ROLLUP to add subtotals to combinations of two fields: Status__c and Region__c. Because Status__c appears first in the GROUP BY ROLLUP function, the subtotals are calculated for each of its unique values. The function GROUPING is used to identify subtotal records, and also to order the results so that the subtotals appear last.

Listing 5.6 Subtotals on Two Field Groupings


for (AggregateResult r : [ SELECT Project__r.Status__c, Contact__r.Region__c,
SUM(Total_Hours__c) hours, COUNT(Id) recs,
GROUPING(Project__r.Status__c) status, GROUPING(Contact__r.Region__c) region
FROM Timecard__c
GROUP BY ROLLUP(Project__r.Status__c, Contact__r.Region__c)
ORDER BY GROUPING(Project__r.Status__c), GROUPING(Contact__r.Region__c) ]) {
System.debug(LoggingLevel.INFO,
r.get('Status__c') + ' ' + r.get('Region__c') + ' ' +
r.get('region') + ' ' + r.get('status') + ' ' +
r.get('hours') + ' ' + r.get('recs'));
}


Listing 5.7 shows the result of running the code in Listing 5.6 on a database containing 13 Timecard records spread across West and Central regions’ projects in Yellow and Green status. Note the third and fourth columns contain the value of the GROUPING function. Here, a 1 indicates that the record is a subtotal, and 0 indicates a normal record. For example, the fifth record from the top is a subtotal on status because the 1 appears in the status column. The other values in that record indicate the sum of all Timecard hours for projects in Yellow status is 109, and that this constitutes three records’ worth of data. The final record contains the grand totals, which you can verify by adding the record count of the Green subtotal (10) to the Yellow subtotal (3).

Listing 5.7 Excerpt of Debug Log after Running Code in Listing 5.6


16:04:43.207|USER_DEBUG|[7]|INFO|Green West 0 0 230.0 6
16:04:43.207|USER_DEBUG|[7]|INFO|Green Central 0 0 152.0 4
16:04:43.207|USER_DEBUG|[7]|INFO|Yellow Central 0 0 109.0 3
16:04:43.207|USER_DEBUG|[7]|INFO|Green null 1 0 382.0 10
16:04:43.207|USER_DEBUG|[7]|INFO|Yellow null 1 0 109.0 3
16:04:43.207|USER_DEBUG|[7]|INFO|null null 1 1 491.0 13


To experiment with GROUP BY CUBE, replace the word ROLLUP with CUBE in Listing 5.6 and run the code. The GROUP BY CUBE syntax causes all possible combinations of grouped fields to receive subtotals. The results are shown in Listing 5.8. Note the addition of two records, subtotals on the Region__c field indicated by a 1 in the region column.

Listing 5.8 Excerpt of Debug Log after Changing Listing 5.6 to Group By Cube


16:06:56.003|USER_DEBUG|[7]|INFO|Green Central 0 0 152.0 4
16:06:56.003|USER_DEBUG|[7]|INFO|Green West 0 0 230.0 6
16:06:56.004|USER_DEBUG|[7]|INFO|Yellow Central 0 0 109.0 3
16:06:56.004|USER_DEBUG|[7]|INFO|Green null 1 0 382.0 10
16:06:56.004|USER_DEBUG|[7]|INFO|Yellow null 1 0 109.0 3
16:06:56.004|USER_DEBUG|[7]|INFO|null West 0 1 230.0 6
16:06:56.004|USER_DEBUG|[7]|INFO|null Central 0 1 261.0 7
16:06:56.005|USER_DEBUG|[7]|INFO|null null 1 1 491.0 13


Additional SOQL Features

Although SOQL doesn’t allow arbitrary joins, it provides some control over how related objects are navigated. This section discusses inner and outer joins, as well as semi-joins and anti-joins:

Image Inner join and outer join—SOQL statements that include related objects normally do so by outer join, but can perform an inner join instead using a WHERE clause.

Image Semi-join and anti-join—Semi-join and anti-join are types of relationship queries that use the results of a subquery to filter the records returned from the parent object.

Image Multi-select picklists—A multi-select picklist is a form of picklist field that allows multiple values to be stored for a single record. The standard conditional filters of the SOQL WHERE clause do not suffice for handling multiple values within a single record and column, so SOQL provides special syntax to handle this case.

Inner Join and Outer Join

A SOQL statement consists of a single base object, specified using the FROM keyword. All fields in the base object can be retrieved in the query, as well as fields from parent and child objects depending on their distance away from the base object. Force.com takes care of joining related objects together to retrieve the requested fields.

These implicit joins are always outer joins. An outer join returns all records from the base object, including records that do not refer to a related object. To get a feel for this behavior, create a new Project record in the native user interface and leave all of its fields blank, but enter Test Projectfor the Name. Open Force.com IDE’s Schema Explorer and enter and run the query given in Listing 5.9.

Listing 5.9 SOQL Outer Join


SELECT Name, Account__r.Name
FROM Project__c


This query returns the name and account name of the Projects. Account is the parent object of Project through a Lookup relationship. Because it is a Lookup relationship and not Master-Detail, it can contain a null value in Account__c, the Account foreign key field. With no foreign key to Account, Account__r, the foreign object reference, is also null.

You should see the five records imported from Listing 2.11 in Chapter 2, “Database Essentials,” plus the newly added record, named Test Project. Figure 5.1 shows the result of running the query. The Test Project record contains no value for Account__r yet was included in the results anyway. This is due to the outer join behavior.

Image

Figure 5.1 Outer join results in Schema Explorer

In a relational database, this same query translated to SQL would result in five rows. The Test Project row would not be returned because it does not match a row in the Account table. Joins in SQL are inner by default, returning only rows that match both tables of the join.

To duplicate this inner join behavior in SOQL, simply add a filter condition to eliminate records without a matching record in the related object. For example, Listing 5.10 adds a filter condition to Listing 5.9 to exclude Project records without a corresponding Account.

Listing 5.10 SOQL Inner Join


SELECT Name, Account__r.Name
FROM Project__c
WHERE Account__c != null


The results of this query are shown in Figure 5.2. It has returned five records, each one with a corresponding parent Account record. The newly added Project record without the Account is correctly omitted.

Image

Figure 5.2 Inner join results in Schema Explorer

Semi-Join and Anti-Join

In Chapter 4, “Business Logic,” you learned the two ways related objects can be included in SOQL: parent-to-child and child-to-parent queries. Semi-join and anti-join queries enhance the functionality of both queries, and add the ability to make child-to-child queries. In general, they allow records from one object to be filtered by a subquery against another object.

For example, suppose you need a list of all Account records that have at least one Project record in a yellow status. To make sure you have a valid test case, edit one of the Project records in the native user interface to set it to a yellow status. Try to write a query to return its Account, with Account as the base object.

You can’t do this without using a semi-join. Listing 5.11 shows one attempt. But it returns the unique identifiers and names of all Accounts and the unique identifiers of any Projects in yellow status. You would still have to write Apex code to filter through the Account records to ignore those without Project child records.

Listing 5.11 Parent-to-Child Query, Filter on Child


SELECT Id, Name,
(SELECT Id FROM Projects__r WHERE Status__c = 'Yellow')
FROM Account


Figure 5.3 shows the result of executing this query. Grand Hotels & Resorts Ltd is the Project in yellow status, and you can see that its Project record has been returned in the relationship field Projects__r.

Image

Figure 5.3 Parent-to-child query, filter on child

Listing 5.12 rewrites this query using a semi-join. Read it from the bottom up. A subquery identifies Projects in yellow status, returning their Account unique identifiers. This set of Account unique identifiers is used to filter the Account records returned by the query. The result is a single Account, as shown in Figure 5.4.

Image

Figure 5.4 SOQL with parent-to-child semi-join

Listing 5.12 SOQL with Semi-Join


SELECT Id, Name
FROM Account
WHERE Id IN
(SELECT Account__c FROM Project__c WHERE Status__c = 'Yellow')


An anti-join is the negative version of a semi-join. It uses the NOT IN keyword to allow the subquery to exclude records from the parent object. For example, Listing 5.13 returns all Accounts except those containing Projects in a green status. Note that the results include the Project in yellow status, as well as all Account records not associated with a Project.

Listing 5.13 SOQL with Anti-Join


SELECT Id, Name
FROM Account
WHERE Id NOT IN
(SELECT Account__c FROM Project__c WHERE Status__c = 'Green')


Returning to semi-joins, Listing 5.14 provides an example of another type, called child-to-child. It joins two child objects that aren’t directly related by relationship fields. The records in the Timecard object are filtered by contacts that have at least one assignment as a consultant. This means Timecards logged by contacts who are not assigned to a project as a consultant are excluded from the results. Child-to-child refers to the Timecard and Assignment objects, which are related to each other only in so much as they are children to other objects.

Listing 5.14 SOQL with Child-to-Child Semi-Join


SELECT Project__r.Name, Week_Ending__c, Total_Hours__c
FROM Timecard__c
WHERE Contact__c IN
(SELECT Contact__c FROM Assignment__c WHERE Role__c = 'Consultant')


Listing 5.15 demonstrates a third type of semi-join, the child-to-parent. Timecards are filtered again, this time to include consultants with an hourly cost rate of more than $100. Child-to-parent refers to the relationship between the Timecard and Contact objects. Contact is the parent object, and it is being used to restrict the output of the query on Timecard, the child object.

Listing 5.15 SOQL with Child-to-Parent Semi-Join


SELECT Project__r.Name, Week_Ending__c, Total_Hours__c
FROM Timecard__c
WHERE Contact__c IN
(SELECT Id FROM Contact WHERE Hourly_Cost_Rate__c > 100)


Several restrictions are placed on semi-join and anti-join queries:

Image The selected column in the subquery must be a primary or foreign key and cannot traverse relationships. It must be a direct field on the child object. For example, it would be invalid to rewrite the subquery in Listing 5.12 to return Account__r.Id in place of Account__c.

Image A single query can include at most two semi-joins or anti-joins.

Image Semi-joins and anti-joins cannot be nested within other semi-join and anti-join statements, and are not allowed in subqueries.

Image The parent object cannot be the same type as the child. This type of query can always be rewritten as a single query without a semi-join or an anti-join. For example, the invalid query SELECT Name FROM Project__c WHERE Id IN (SELECT Id FROM Project__c WHERE Status__c = 'Green') can be expressed without a subquery: SELECT Name FROM Project__c WHERE Status__c = 'Green'.

Image Subqueries cannot be nested and cannot contain the OR, count(), ORDER BY, or LIMIT keywords.

Multi-Select Picklists

Multi-select picklists are interchangeable with ordinary picklists in queries, except for being prohibited in the ORDER BY clause. SOQL includes two additional features for filtering multi-select picklists, described in the following list:

Image Semicolon AND operator—The semicolon is used to express multiple string literals. For example, 'Java;Apex' means that the multi-select picklist has both Java and Apex items selected in any order. The semicolon notation can be used with the = and != SOQL operators to make assertions about the selected items of multi-select picklists.

Image INCLUDES and EXCLUDES keywords—The INCLUDES and EXCLUDES keywords are followed by comma-separated lists of literal values. The INCLUDES keyword returns records in which the selected values of a multi-select picklist are included in the list of values. TheEXCLUDES keyword returns records that match none of the values.

The semicolon notation can be combined with the INCLUDES and EXCLUDES keywords to express any combination of multi-select picklist values.

To try this out, create a multi-select picklist named Requested Skills on the Project object. Run the SOQL statement given in Listing 5.16 using the Force.com IDE’s Schema Explorer. It returns Project records with the multiple selection of Apex, Java, and C# in the Requested Skills field and also records with only Python selected. Populate Project records with matching values to see them returned by the query.

Listing 5.16 SOQL with Multi-Select Picklist


SELECT Id, Name
FROM Project__c
WHERE Requested_Skills__c INCLUDES ('Apex;Java;C#', 'Python')


Salesforce Object Search Language (SOSL)

Data stored in the Force.com database is automatically indexed to support both structured and unstructured queries. SOQL is the language for structured queries, allowing records from a single object and its related objects to be retrieved with precise, per-field filter conditions. SOSL is a full-text search language for unstructured queries. It begins by looking across multiple fields and multiple objects for one or more search keywords, and then applies an optional SOQL-like filter on each object to refine the results.

To decide which query language to use, consider the scope of the query. If the query spans multiple unrelated objects, SOSL is the only practical choice. If the query searches for words within many string fields, it can probably be expressed more concisely in SOSL than SOQL. Use SOQL for queries on a single object with filters on various data types.

SOSL Basics

At the highest level, a SOSL query specifies search terms and scope. The search terms are a list of string literals and can include wildcards. The search scope is fields containing string data from one or more objects. This excludes Number, Date, and Checkbox fields from being searched with SOSL.

SOSL query syntax consists of four parts:

Image Query—The query is one or more words or phrases to search on. The query can include the wildcards * (matches any number of characters) and ? (matches any single character) at the middle or end of search terms. Enclose a search term in quotation marks to perform an exact match on multiple words. Use the logical operators AND, OR, and AND NOT to combine search terms and parentheses to control the order in which they’re evaluated. Note that searches are not case sensitive.

Image Search group—The search group is an optional part of the SOSL query indicating the types of fields to search in each object. Valid values are ALL FIELDS (all string fields), NAME FIELDS (the standard Name field only), EMAIL FIELDS (all fields of type Email), and PHONE FIELDS (all fields of type Phone). The default value is ALL FIELDS.

Image Field specification—The field specification is a comma-separated list of objects to include in the result. By default, the Id field of each object is included. Optionally, you can specify additional fields to return by enclosing them in parentheses. You can also specify conditional filters using the same syntax as the SOQL WHERE clause, set the sort order with the ORDER BY keyword, and use the LIMIT keyword to limit the number of records returned per object.

Image Record limit—This optional value specifies the maximum number of records returned by the entire query, from all the objects queried. If a record limit is not provided, it defaults to the maximum of 200.

These four parts are combined in the following syntax: FIND 'query' IN search group RETURNING field specification LIMIT record limit. The single quotation marks around query are required.

SOSL in Apex

SOSL in Apex works much like SOQL in Apex. Queries are enclosed in square brackets and compiled directly into the code, ensuring that the query syntax is correct and references valid fields and objects in the database.

As with SOQL, bind variables can be used to inject variable values from the running program into select parts of the query. This injection of values is performed in a secure manner because Apex automatically escapes special characters. Bind variables are allowed in the search string (following FIND), filter literals (in the WHERE block), and the LIMIT keyword.

SOSL is not allowed in triggers. It will compile, but will fail at runtime. It is allowed in unit tests and custom user interfaces, as covered in Chapter 6, “User Interfaces.” In this chapter, you can experiment with SOSL using the Execute Anonymous view.


Note

You are limited to 20 SOSL queries returning a maximum of 2,000 rows per query.


Listing 5.17 is a sample SOSL query in Apex. It returns the names of records in the Project and Contact objects that contain the word Chicago in any of their fields.

Listing 5.17 SOSL in Apex


List<List<SObject>> result = [
FIND 'Chicago'
RETURNING Project__c(Name), Contact(Name)
];
List<Project__c> projects = (List<Project__c>)result[0];
for (Project__c project : projects) {
System.debug('Project: ' + project.Name);
}
List<Contact> resources = (List<Contact>)result[1];
for (Contact resource : resources) {
System.debug('Contact: ' + resource.Name);
}


Figure 5.5 shows the results of running this code in the Execute Anonymous view. If your debug log is cluttered with too many other entries to see the output of the query, set Apex code to the Debug level and all other Log categories to None.

Image

Figure 5.5 Results of SOSL in Apex

Transaction Processing

This section covers three features of Apex that control how transactions are processed by the database:

Image Data Manipulation Language (DML) database methods—DML database methods are much like DML statements from Chapter 4, but add support for partial success. This allows some records from a batch to succeed while others fail.

Image Savepoints—Savepoints designate a point in time that your code can return to. Returning to a savepoint rolls back all DML statements executed since the establishment of the savepoint.

Image Record locking—Apex provides a SOQL keyword to protect records from interference by other users or programs for the duration of a transaction.

Data Manipulation Language (DML) Database Methods

All database operations in Apex are transactional. For example, an implicit transaction is created when a trigger fires. If the code in a trigger completes without error, DML operations performed within it are automatically committed. If the trigger terminates prematurely with an uncaught exception, all DML operations are automatically rolled back. If multiple triggers fire for a single database operation, all trigger code succeeds or fails as a group.

In Chapter 4, you were exposed to DML statements. These statements accept a single record or batch of records. When operating on a batch, they succeed or fail on the entire group of records. For example, if 200 records are inserted and the last record fails with a validation error, none of the 200 records are inserted.

Apex offers a second way of making DML statements called DML database methods. DML database methods allow batch DML operations to fail on individual records without impacting the entire batch. To do this, they do not throw exceptions to indicate error. Instead they return an array of result objects, one per input record. These result objects contain a flag indicating success or failure, and error details in the event of failure.

A DML database method exists for each of the DML statements. Each method takes an optional Boolean parameter called opt_allOrNone to specify batch behavior. The default value is true, indicating that the behavior is “all or none.” This makes the method identical to a DMLstatement, with one failed record causing the failure of all records and a DmlException. But if the opt_allOrNone parameter is false, partial success is allowed.


Note

DML database methods are subject to the same governor limits and general restrictions as DML statements. Refer to Chapter 4 for more information.


Listing 5.18 inserts a batch of two Skill records using the insert database method. It passes false as an argument to allow partial success of the DML operation. The insert method returns an array of SaveResult objects. They correspond one-to-one with the array passed as an argument to the insert method. Each SaveResult object is examined to check for failure, and the results are displayed in the debug log.

Listing 5.18 DML Database Method Usage


Contact tim = [ SELECT Id
FROM Contact
WHERE Name = 'Tim Barr' LIMIT 1 ];
Skill__c skill1 = new Skill__c(Contact__c = tim.Id,
Type__c = 'Java', Rating__c = '3 - Average');
Skill__c skill2 = new Skill__c(Contact__c = tim.Id,
Rating__c = '4 - Above Average');
Skill__c[] skills = new Skill__c[] { skill1, skill2 };
Database.SaveResult[] saveResults =
Database.insert(skills, false);
for (Integer i=0; i<saveResults.size(); i++) {
Database.SaveResult saveResult = saveResults[i];
if (!saveResult.isSuccess()) {
Database.Error err = saveResult.getErrors()[0];
System.debug('Skill ' + i + ' insert failed: '
+ err.getMessage());
} else {
System.debug('Skill ' + i + ' insert succeeded: new Id = '
+ saveResult.getId());
}
}


The result of executing this code is shown in Figure 5.6. The debug log indicates the first record is inserted, but the second failed because it doesn’t contain a value for the Type__c field. This is enforced by a validation rule created in Chapter 2. If you edit this code and remove the second argument to Database.insert, which enables partial success, the failure of the second record raises an exception and rolls back the successful insertion of the first record.

Image

Figure 5.6 Results of insert DML database method

Savepoints

Savepoints are markers indicating the state of the database at a specific point in the execution of your Apex program. They allow the database to be restored to a known state in case of error or any scenario requiring a reversal of all DML operations performed since the savepoint.

Set a new savepoint using the Database.setSavepoint method, which returns a Savepoint object. To restore the database to a savepoint, call the Database.rollback method, which takes a Savepoint object as its only argument.

Several limitations exist on the use of savepoints. The number of savepoints and rollbacks contributes toward the overall limit on DML statements, which is 150. If you create multiple savepoints and roll back, all savepoints created after the savepoint you roll back to are invalidated. Finally, you cannot share a savepoint across triggers using a static variable.

Listing 5.19 is an example of using the setSavepoint and rollback methods. First, a savepoint is set. Then, all the Project records in your database are deleted, assuming your database doesn’t contain more than the governor limit of 10,000 records for DML. Finally, the database is rolled back to the savepoint. The number of records in the Project object is counted before each operation in the program to illustrate its behavior.

Listing 5.19 Savepoint and Rollback Usage


void printRecordCount() {
System.debug([ SELECT COUNT() FROM Project__c ] + ' records');
}
printRecordCount();
Savepoint sp = Database.setSavepoint();

delete [ SELECT Id FROM Project__c ];
printRecordCount();

Database.rollback(sp);
printRecordCount();


The results of running the code snippet in the Execute Anonymous view are shown in Figure 5.7. The debug log indicates that the Project object initially contains five records. They are all deleted, leaving zero records. Then the database is rolled back to the savepoint established before the deletion, resulting in a count of five records again.

Image

Figure 5.7 Results of savepoint and rollback sample code

Record Locking

Apex code has many entry points. Code can be invoked from outside of Force.com via a Web service call, by modifying a record with a trigger on it in the native user interface, inside Force.com IDE in an Execute Anonymous view, or in a unit test. Additionally, multiple users or programs can be running the same code simultaneously or code that uses the same database resources.

DML operations using values returned by SOQL or SOSL queries are at risk for dirty writes. This means values updated by one program have been modified by a second program running at the same time. The changes of the second program are lost because the first program is operating with stale data.

For example, if your code retrieves a record and then modifies its value later in the program, it requires a write lock on the record. A write lock prevents the record from being concurrently updated by another program. Write locks are provided in Apex via the SOQL FOR UPDATE keyword. This keyword indicates to Apex that you intend to modify the records returned by the SOQL query. This locks the records, preventing them from being updated by another program until your transaction is complete. No explicit commit is necessary. The records are unlocked, and changes are automatically committed when the program exits successfully or is rolled back otherwise.


Note

You cannot use the ORDER BY keyword with FOR UPDATE. Query results are automatically ordered by Id field.


Listing 5.20 is an example of record locking in Apex. Tim Barr is given a raise of $20. His Resource record is retrieved and locked, the hourly cost is incremented, and the database is updated. The use of FOR UPDATE ensures that this code running simultaneously in two contexts still results in the correct outcome: a $40 increase in hourly cost rate, $20 from each of the two independent execution contexts, serialized with FOR UPDATE. Without the locking, a dirty write could cause one of the updates to be lost. For this example to execute without errors, make sure you have a Contact record named Tim Barr with a non-null value for the Hourly_Cost_Rate__c field.

Listing 5.20 Record Locking Example


Contact tim = [ SELECT Id, Hourly_Cost_Rate__c
FROM Contact
WHERE Name = 'Tim Barr' LIMIT 1
FOR UPDATE ];
tim.Hourly_Cost_Rate__c += 20;
update tim;


Apex Managed Sharing

Apex managed sharing allows Apex code to add, edit, and delete record sharing rules. This is the third and most advanced type of record sharing provided by Force.com. It provides the Apex developer with full control of record sharing. Apex managed sharing uses the same infrastructure as the other two types of record sharing, discussed in Chapter 3, “Database Security,” and briefly reviewed here:

Image Force.com managed sharing—These are record sharing rules maintained by Force.com. A native user interface enables administrators to add, edit, and delete these rules. Rules are based on user, group, or role membership and defined individually on each object. They are configured in the Administration Setup area, Security Controls, Sharing Settings.

Image User managed sharing—Users who own records can grant permission to additional users from the native user interface. This is a manual process. The owner visits a record to share and clicks the Sharing button to add, edit, or remove its sharing rules.

This section is divided into two parts, described next:

Image Sharing objects—Sharing objects are where Force.com stores record sharing rules. The fields of sharing objects are described, as well as restrictions on their use.

Image Creating sharing rules in Apex—This walks you through the infrastructure behind sharing rules, finishing with a code sample to add a sharing rule in the Services Manager sample application schema.

Sharing Objects

Every custom object, except Detail objects in a Master-Detail relationship, has a corresponding sharing object to store its record-level sharing rules. The sharing object is created automatically by Force.com and is invisible to the native user interface. It can be seen in the Force.com IDE’s Schema Explorer. Its name is the name of your object with __Share appended. For example, the sharing object for the Project__c object is Project__Share.

The sharing object contains explicit sharing rules. These are created by Force.com managed sharing, user managed sharing, and Apex managed sharing. It does not contain implicit shares such as organization-wide defaults.

Four fields of the sharing object control how records are shared between users and groups, as follows:

Image ParentID—ParentId is the unique identifier of the record being shared.

Image UserOrGroupId—This is the unique identifier of the user or group that the sharing rule is granting access to. Groups are public groups or roles.

Image AccessLevel—This field stores the level of access granted to the user or group for this record. The three valid values are Read (Read Only), Edit (Read and Edit), and All (Full Control). Apex managed sharing cannot set a record to All. The value of AccessLevel must be more permissive than the organization-wide default or a runtime exception is thrown.

Image RowCause—The purpose of the RowCause field is to track the origin of the sharing rule. Valid values are Manual (the default) or a custom sharing reason, defined on the object in the Apex Sharing Reasons related list. Manual sharing rules can be edited and removed by the record owner and are reset when record ownership changes. Sharing records with a custom reason are not reset when ownership changes and cannot be edited or removed without the administrative permission Modify All Data.

Restrictions

Two important restrictions exist on Apex managed sharing:

Image Objects with an organization-wide default sharing level of Public Read/Write, the most permissive setting, cannot use Apex managed sharing. Set the level to Private or Public Read Only instead.

Image After a sharing record is created, the only field that can be updated is the access level. If you need to change other fields, delete the sharing record entirely and re-create it.


Caution

When the organization-wide sharing default is changed for an object, all sharing rules are recalculated. This causes your Apex managed sharing rules to be deleted. To re-create them, you must implement an Apex class to participate in the recalculation event. This code uses the Apex batch processing feature to allow processing of millions of records in smaller groups of records, to stay within governor limits. The Apex batch processing functionality is covered in Chapter 9, “Batch Processing.”


Creating Sharing Rules in Apex

Figure 5.8 shows the Force.com managed sharing settings for the Project object, configured in Chapter 3. The sharing rules specify that projects owned by members of one role are shared by all users in that role. This is defined three times because three separate roles exist, one for each region in the sample company.

Image

Figure 5.8 Sharing rules for Project object

Navigate to an individual Project record and click the Sharing button. Figure 5.9 is an example of the resulting screen. It lists the sharing rules in effect for this record. The first sharing rule is the default one, specifying that the owner has full control over the record. The second is the sharing rule maintained by Force.com managed sharing, configured using the screen shown in Figure 5.8, which allows users in the same role as the owner (West) to edit the record.

Image

Figure 5.9 Sharing detail for Project record

You’ve visited screens in the native user interface where record sharing is taking place. Next, look a level deeper at the data driving the sharing behavior. Open the Force.com IDE’s Schema Explorer and run the query shown in Listing 5.21. It illustrates how Force.com stores the information for the sharing rules in Figure 5.9 and what you will be manipulating with Apex managed sharing.

Listing 5.21 SOQL Query on Project Share Object


SELECT ParentId, UserOrGroupId, AccessLevel
FROM Project__Share
WHERE Parent.Name = 'GenePoint'


Figure 5.10 is the result of running the query. Note that the identifiers in your Force.com organization will be different from those in the figure.

Image

Figure 5.10 Results of SOQL query on Project Share object

Try to decode the meaning of each record. The ParentId field contains the unique identifier of the record being shared. The query has filtered by the name GenePoint, which is a Project record. The UserOrGroupId field contains the unique identifier of a User or Group record. TheAccessLevel field is one of the four access levels (All, None, Edit, View), although only Edit and View can be set using Apex managed sharing.

The first record has All access, so it’s the default sharing rule granting the owner of the record full access. The second record might be a mystery at first. The UserOrGroupId does not match up with the unique identifier of the West region’s role record. Run the query shown in Listing 5.22to track down the meaning of this value.

Listing 5.22 SOQL Query on Group Object


SELECT Id, Type, RelatedId
FROM Group


The Group object stores information about roles and other groups in Force.com. Figure 5.11 displays the results of the query. The RelatedId field contains the same value as the UserOrGroupId value of the second sharing record. This is where Force.com managed sharing has stored the fact that the Project record named GenePoint is shared with other members of the West role.

Image

Figure 5.11 Results of SOQL query on Group object

Apex managed sharing allows you to insert new rows into the Project__Share object, and other sharing objects, and specify custom sharing reasons that are meaningful to your application. Custom sharing reasons are maintained for each object individually. To try adding one, go to the App Setup area and click Create, Objects and select the Project object. Scroll to the bottom of the page. In the Apex Sharing Reasons list, add a new reason with a label of My Sharing Reason. Force.com automatically suggests a Name, converting spaces to underscores. Refer to the custom sharing reason in your Apex code by adding __c to the end of the name.

Listing 5.23 contains sample code you can run in the Execute Anonymous view. It shares the GenePoint record with an additional user, specifying the custom sharing reason, with Read-only access.

Listing 5.23 Inserting Sharing Rule on Project Object


User carrie = [ SELECT Id FROM User
WHERE Name = 'Carrie Oakey' LIMIT 1 ];
Project__c genePoint = [ SELECT Id FROM Project__c
WHERE Name = 'GenePoint' LIMIT 1 ];
Project__Share share = new Project__Share(
ParentId = genePoint.Id,
UserOrGroupId = carrie.Id,
rowCause = Project__Share.rowCause.My_Sharing_Reason__c,
AccessLevel = 'Read');
insert share;


After executing this code, refresh the Sharing Details for GenePoint and you should see the screen shown in Figure 5.12. It shows that the new custom sharing rule has been added. Because the sharing rule was created by Apex code and uses a custom sharing reason, it’s preserved across changes of record ownership and cannot be edited or deleted by users unless they have the Modify All Data administrative permission in their profile.

Image

Figure 5.12 Sharing detail for Project record with Apex managed sharing rule

Sending and Receiving Email

Force.com allows emails to be sent and received in Apex code. This functionality can be helpful in many scenarios. For example, you could send an email from within a trigger to notify users of events occurring in the application, such as work that requires their attention. You could write code to automate the classification of incoming emails to customer support, searching for keywords and routing them to the proper support employees. This section describes how to use the objects and methods built in to Apex to process inbound and outbound email and introduces the administration screens of the native user interface that support them.

Sending Email

The three ways to send email in Apex are the following:

Image SingleEmailMessage—Sends an email to up to ten receivers. The email addresses of receivers are provided as strings. A string containing HTML or plain text is used as the message body.

Image SingleEmailMessage with template—Sends to up to ten receivers, but the unique identifiers of Contact, Lead, or User objects must be used instead of strings to provide the receivers’ email addresses. The message body is constructed from a template. Templates are globally available to an organization as defined by an administrator or private to an individual user. Templates can include merge fields to dynamically substitute field values from the receiver’s record and, optionally, field values from an additional, related object.

Image MassEmailMessage—Behaves like a SingleEmailMessage with template but can send email to up to 250 receivers in a single call.

Each of these three ways of sending email contributes toward the maximum of 10 email calls within a single context, an instance of running Apex code. To translate that to the number of email messages, if you use the SingleEmailMessage object with 10 recipients, you can reach a maximum of 100 recipients (10 recipients times the 10 invocation maximum) within a single execution of your program. You can reach 2,500 recipients using the MassEmailMessage. Force.com imposes a daily limit on mass emails, which varies based on the edition of Force.com being used. If this limit is exceeded, an exception is thrown with the exception code MASS_MAIL_LIMIT_EXCEEDED.

Using SingleEmailMessage

You can run the code in Listing 5.24 directly in the Execute Anonymous view. It looks up the User record for the current user and sends a test message to its email address.

Listing 5.24 Sending Email


User you = [ SELECT Email
FROM User
WHERE Id = :UserInfo.getUserId()
LIMIT 1 ];
Messaging.SingleEmailMessage mail =
new Messaging.SingleEmailMessage();
mail.setToAddresses(new String[] { you.Email });
mail.setSubject('Test message');
mail.setPlainTextBody('This is a test');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });


Check the email account associated with your Force.com user for the new message. If you do not see the message, it might be in your junk mail folder. If it’s not in your inbox or junk mail folder, your email server might have refused its delivery. In this case, Force.com will send you the returned message with any delivery error information, given that you are both the sender and the receiver.


Note

Force.com provides online tools to help you authorize its mail servers to ensure that its messages are delivered. Go to the Administration Setup area and click Email Administration, Deliverability and Test Deliverability for more information.


Notice that the sender and receiver of the email are identical. You have sent a message to yourself via Force.com. By default, Apex email methods run using the identity of the current user. The current user’s email address becomes the “from” address in outbound emails. Alternatively, you can define an organization-wide email address and use it to set the “from” address. This enables all of your outbound emails to be sent from a single set of authorized, public email addresses. To define an organization-wide email address, go to the Administration Setup area and click Email Administration, Organization-Wide Addresses.

Using SingleEmailMessage with Template

Templates standardize the appearance and content of emails. They also make including dynamic content in messages without cumbersome, hard-to-maintain code full of string concatenations simple. To add a new email template, go to the Personal Setup area and click Email, My Templates.

When a template is used to send a message, you must provide a targetObjectId value. This is the unique identifier of a Lead, Contact, or User record. The email address associated with this record becomes the recipient of the email.

Optionally, a whatId can be provided. This is the unique record identifier of an Account, Asset, Campaign, Case, Contract, Opportunity, Order, Product, Solution, or any custom object. The fields from this record can be referenced in your template using merge fields. When the message is sent, the record is retrieved and its data substituted into the message body in the locations specified by the merge fields.

Listing 5.25 sends an email using a template. Before trying it, create a template with the unique name of Test_Template. Set its text or HTML content to Hello {!User.FirstName}! or the equivalent to demonstrate the use of merge fields. Mark the template as available for use. InListing 5.25, a SOQL query is used to retrieve the template’s unique identifier so that it isn’t hard-coded into the program.

Listing 5.25 Sending Email Using a Template


User you = [ SELECT Email
FROM User
WHERE Id = :UserInfo.getUserId()
LIMIT 1 ];
EmailTemplate template = [ SELECT Id
FROM EmailTemplate
WHERE DeveloperName = 'Test_Template'
LIMIT 1 ];
Messaging.SingleEmailMessage mail =
new Messaging.SingleEmailMessage();
mail.templateId = template.Id;
mail.targetObjectId = you.Id;
mail.setSaveAsActivity(false);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });



Note

The setSaveAsActivity method was called in Listing 5.25 to disable the HTML email tracking feature, which is not compatible with the User object (targetObjectId). The setSaveAsActivity method is described in the upcoming subsection, “Additional Email Methods.”


Using MassEmailMessage

Mass emails can be sent to 250 recipients in a single method call. The code for sending a mass email is similar to that for sending a single email with a template. The difference is that a MassEmailMessage object is created instead of a SingleEmailMessage. At minimum, you must provide a value for targetObjectIds (an array of Lead, Contact, or User record unique identifiers) and a templateId.

Optionally, you can provide whatIds, an array of record unique identifiers corresponding to the array of targetObjectIds. Field values from these records add dynamic content to the message body. The records are limited to Contract, Case, Opportunity, and Product types. Note that none of these object types are available in a Force.com platform-only license.

Listing 5.26 demonstrates the use of the MassEmailMessage. It selects one Contact in the system and sends an email using the same template created for Listing 5.25.

Listing 5.26 Sending a Mass Email


User you = [ SELECT Email
FROM User
WHERE Id = :UserInfo.getUserId()
LIMIT 1 ];
EmailTemplate template = [ SELECT Id
FROM EmailTemplate
WHERE DeveloperName = 'Test_Template'
LIMIT 1 ];
Messaging.MassEmailMessage mail = new Messaging.MassEmailMessage();
mail.templateId = template.Id;
mail.targetObjectIds = new Id[] { you.Id };
mail.setSaveAsActivity(false);
Messaging.sendEmail(new Messaging.MassEmailMessage[] { mail });


Transactional Email

The transactional behavior of the sendEmail method is consistent with that of Force.com database DML methods. When an invocation of Apex code is completed without error, email is sent. If an uncaught error causes the program to be terminated prematurely, email is not sent. If multiple emails are sent, by default they all fail if one fails. Setting the optional opt_allOrNone parameter of the sendEmail method to false enables partial success of a group of outbound messages. In this case, the sendEmail method returns an array of SendEmailResult objects. These objects can be used to determine the success or failure of each message and include error details in case of failure.

Additional Email Methods

The following list describes useful methods that apply to both SingleEmailMessage and MassEmailMessage objects:

Image setCcAddresses—This method accepts a string array of email addresses to carbon copy on the email.

Image setSenderDisplayName—The sender display name is shown in email reading programs as a label for the sender email address.

Image setReplyTo—The reply-to address is the email address designated to receive replies to this message. If not specified, it’s always the sender’s email address.

Image setBccSender—If this is set to true, Force.com blind-carbon-copies the sender’s email address. In a mass email, the sender is copied only on the first message. Force.com prevents use of this feature if an administrator has enabled Compliance BCC Email. You can do this in the Administration Setup area by clicking Email Administration, Compliance BCC Email.

Image setUseSignature—By default, Force.com appends the sending user’s signature to the end of outbound emails. You can edit this signature in the Personal Setup area by clicking Email, My Email Settings. To turn off this feature, pass false to this method.

Image setFileAttachments—The argument to this method is an array of EmailFileAttachment objects. These objects contain the names and data of attachments to be sent with the message. They provide a method to set the attachment body (setBody) and filename (setFileName). The total size of the attachments for a single message cannot exceed 10MB.

Image setDocumentAttachments—Force.com has a native object type for storing content called Document. You can find it in the native user interface by clicking the Documents tab. Here you can create, edit, and delete Documents and group them into folders. Each Document record has a unique identifier, and this method accepts an array of them. Each Document specified is sent as an attachment to the message. All attachments in a single message, including file attachments, cannot exceed 10MB.

Image setOrgWideEmailAddressId—Use this method to specify the unique identifier of an organization-wide email address. This email address is used as the “from” address rather than the address of the current user. To define organization-wide email addresses and obtain their unique identifiers, go to the Administration Setup area and click Email Administration, Organization-Wide Addresses.

Image setSaveAsActivity—Force.com’s outbound email can be configured to track the behavior of email recipients who are Leads or Contacts in the system. This is accomplished with an invisible image embedded in messages sent using templates. When receivers who haven’t blocked multimedia content in their email readers open the message, the Force.com service is contacted and tracks this information. By visiting the receiver’s Lead or Contact record, you can see the date the email was first opened, the number of times it was opened, and the date it was most recently opened. By default, this setting is enabled. To disable or enable it for the organization, go to the App Setup area and click Customize, Activities, Activity Settings and select Enable Email Tracking. To disable it for a specific message, pass false to this method.

Receiving Email

The two steps for configuring Force.com to process inbound emails are as follows:

1. Write an Apex class that implements a specific interface (Messaging.InboundEmailHandler) and method (handleInboundEmail). This provides your code access to the envelope (Messaging.InboundEnvelope) and content (Messaging.InboundEmail) of inbound emails, including mail headers and attachments. It is otherwise standard Apex code with no special restrictions. The return value of this method is a Messaging.InboundEmailResult. To indicate processing failure, set the success field of this object to false. Any explanatory message set in the message field is returned to the sender as an email response.

2. Create an Email Service using the native user interface. An Email Service is associated with one or more Force.com-issued email addresses that serve as the gateways to your Apex class. When email arrives at the email address, your Apex class is invoked to process it.

If your Apex code fails with an uncaught exception while processing an incoming email, Force.com treats the email as undeliverable. This is much like a mail gateway behaves when presented with an unknown recipient email address. An email is returned to the sender with diagnostic information about the problem, including the error message from your Apex code.

To personalize email processing based on the identity of the sender, use one of these strategies:

Image Have all users share a single inbound email address. Your Apex code reads the sender’s “from” address and customizes behavior based on that, perhaps by querying Contact or Lead for more information about them.

Image Issue each user or group of users a unique email address. Your Apex code can adjust its behavior based on the “to” address of the incoming message.


Caution

There are governor limits on inbound email. The maximum size of each inbound message, attachments included, is 10MB. The maximum size of each message body, text and HTML combined, is 100KB. The maximum size of each binary attachment is 5MB and 100KB for text attachments. The maximum heap size for Apex email handlers is 18MB. If any of these limits are reached, your Apex code will not be invoked, and the offending message will be returned to its sender.


Getting Started with Inbound Email Processing

Follow these next steps to create a new Apex class to process inbound email in the Force.com IDE. This is a simple example that sends a reply to the inbound message with the original message quoted in the body.

1. Make sure your Force.com project is selected and click New, Apex Class in the File menu.

2. Enter MyEmailService for the name and select the Inbound Email Service template.

3. Click the Finish button. Enter the code given in Listing 5.27, skipping the class, method, and result declarations because they are provided by the template. Save your changes.

Listing 5.27 Receiving Email


global class MyEmailService implements
Messaging.InboundEmailHandler {
global Messaging.InboundEmailResult
handleInboundEmail(Messaging.InboundEmail email,
Messaging.InboundEnvelope envelope) {
Messaging.InboundEmailResult result = new
Messaging.InboundEmailresult();
Messaging.SingleEmailMessage outbound = new
Messaging.SingleEmailMessage();
outbound.toAddresses = new String[] { email.replyTo };
outbound.setSubject('Re: ' + email.subject);
outbound.setHtmlBody('<p>This reply was generated by Apex.'
+ 'You wrote:</p><i>' + email.plainTextBody + '</i>');
Messaging.sendEmail(new Messaging.SingleEmailMessage[]
{ outbound });
return result;
}
}


4. In the native user interface, go to the App Setup area and click Develop, Email Services.

5. Click the New Email Service button.

6. Enter a service name. Enter MyEmailService as the Apex class. Leave the other options set to their defaults and click the Save button.

7. Click the Activate button. Then click the New Email Address button to create a Force.com-generated email address.

8. This screen allows you to whitelist email addresses and domains that are allowed to use this email service. By default, it’s configured to allow emails only from the current user’s email address. Accept this setting by clicking the Save button.

9. You should now see an email address listed at the bottom of the page, as shown in Figure 5.13. Copy the address to your Clipboard, open your favorite email application, and send a test message to this address. Within a minute, you should receive an email in response, generated by your Apex class.

Image

Figure 5.13 Email service configuration

Dynamic Apex

Dynamic Apex describes features of Apex that bypass its typically strongly typed nature. For example, database queries, objects, and fields are part of the language, and references to them are strongly typed, validated at compile time. Dynamic Apex allows you to work with these objects as ordinary strings rather than compiled parts of your program. This has its advantages in that your program can be more dynamic and generic. It also has disadvantages, the primary one being that your code can suffer a greater variety of errors at runtime.

This section describes three dynamic Apex features. Dynamic database queries are SOQL and SOSL queries executed at runtime from strings rather than from compiled code. Schema metadata allows Apex code to introspect the structure of the Force.com database, including its objects, fields, and relationships. Type methods allow introspection of an object’s type, including creation of a new instance.

Dynamic Database Queries

In Chapter 4, you learned about bind variables. They are variables whose values are injected into SOQL and SOSL statements in predefined locations, notated with colons. But bind variables are not powerful enough to support an entirely dynamic WHERE clause, one that includes conditional filters added and subtracted based on the behavior of the program. You could write every combination of WHERE clause and use long strings of conditional statements to pick the right one. An alternative is a completely dynamic query, executed using the Database.query method.

Listing 5.28 provides an example of two dynamic queries. The first is on the Contact object. The results of the query are returned in a list of Contact records. Other than the dynamic query itself, this code should be familiar. The second query selects Project records but treats them as a list of SObject objects.

Listing 5.28 Dynamic SOQL Queries


List<Contact> resources = Database.query(
'SELECT Id, Name FROM Contact');
for (Contact resource : resources) {
System.debug(resource.Id + ' ' + resource.Name);
}
List<SObject> projects = Database.query('SELECT Id, Name FROM Project__c');
for (SObject project : projects) {
System.debug(project.get('Id') + ' ' + project.get('Name'));
}


The SObject is a typeless database object. It allows you to interact with database records without declaring them as a specific type. The get method of the SObject allows the retrieval of a field value by name. The getSObject method returns the value of a related object. These values also have setter methods: set and setSObject. Used in conjunction with DML statements or database DML methods, you can write generic code that operates on a series of database objects. This is particularly useful when you have several objects with the same field names because it can reduce the amount of code.


Tip

Use the escapeSingleQuotes of the String object to prevent SOQL injection attacks. This method adds escape characters (\) to all single quotation marks in a string.


SOSL queries can also be constructed and executed dynamically. The Search.query method returns a list of lists containing SObjects. Listing 5.29 provides an example of its use.

Listing 5.29 Dynamic SOSL Query


List<List<SObject>> result = Search.query(
'FIND \'Chicago\' '
+ 'RETURNING Contact(Name), Project__c(Name)');
for (List<SObject> records : result) {
for (SObject record : records) {
System.debug(record.get('Name'));
}
}


The SOSL query returns the names of Project and Contact records containing the word Chicago. The outer loop is executed for each type of object specified in the RETURNING clause. The inner loop runs over the matching records of that object type. For example, the first iteration of the loop assigns records to a list of Contact records that matched the search term. The second iteration assigns it to the matching Project records.


Note

Dynamic queries have all the same governor limits as their static counterparts.


Schema Metadata

Schema metadata is information about the Force.com database, available to your Apex code dynamically, at runtime. It has many potential uses, such as customizing the behavior of Apex code installed in multiple organizations, driving the construction of dynamic queries, or verifying that the database is configured in a certain way. This section describes the five types of schema metadata (object, field, child relationship, picklist, and record type) and includes code that can be run in the Execute Anonymous view to demonstrate accessing them.


Note

You are limited to a maximum of 100 calls to schema metadata methods. All five types of schema metadata methods contribute equally to the limit.


Object Metadata

Object metadata is information about the database objects in the Force.com organization. It includes custom as well as standard objects. Listing 5.30 provides an example of retrieving object metadata. The metadata of all objects in the database is retrieved, and their names and labels are printed to the debug log.

Listing 5.30 Retrieving Object Metadata


Map<String, Schema.SObjectType> objects = Schema.getGlobalDescribe();
Schema.DescribeSObjectResult objInfo = null;
for (Schema.SObjectType obj : objects.values()) {
objInfo = obj.getDescribe();
System.debug(objInfo.getName() + ' [' + objInfo.getLabel() + ']');
}


Field Metadata

Field metadata provides access to all the attributes of fields you configure on a database object. Listing 5.31 demonstrates how to access field metadata. The fields of the Project__c object are retrieved, including standard and custom fields. The getDescribe method is invoked on each to return its metadata, a Schema.DescribeFieldResult object. The name, label, data type, precision, and scale of each field are displayed in the debug log.

Listing 5.31 Retrieving Field Metadata


Map<String, Schema.SObjectField> fields =
Schema.SObjectType.Project__c.fields.getMap();
Schema.DescribeFieldResult fieldInfo = null;
for (Schema.SObjectField field : fields.values()) {
fieldInfo = field.getDescribe();
System.debug(fieldInfo.getName()
+ ' [' + fieldInfo.getLabel() + '] '
+ fieldInfo.getType().name()
+ '(' + fieldInfo.getPrecision()
+ ', ' + fieldInfo.getScale() + ')');
}



Tip

If you do not know the type of an object, you can still retrieve its metadata using getSObjectType. For example, if a01i0000000rMq1 is the unique identifier of a Project record, the result of Id.valueOf('a01i0000000rMq1').getSObjectType() can replace Schema.SObjectType.Project__c in the second line of Listing 5.31.


Child Relationship Metadata

Child relationship metadata contains the child’s object type, the relationship name, and an object identifying the field in the child object that relates it to the parent. Listing 5.32 demonstrates the retrieval of child relationship metadata from the Contact object. Compare the results with what you see in the Force.com IDE’s Schema Explorer for the Contact object.

Listing 5.32 Retrieving Child Relationship Metadata


Schema.DescribeSObjectResult res = Contact.SObjectType.getDescribe();
List<Schema.ChildRelationship> relationships = res.getChildRelationships();
for (Schema.ChildRelationship relationship : relationships) {
System.debug(relationship.getField() + ', ' + relationship.getChildSObject());
}


Picklist Metadata

Picklist metadata provides access to the master list of available picklist values for a picklist or multi-select picklist field. It does not include the assignments of picklist values to record types, nor does it provide any information about the relationship between picklist values in dependent picklists. Listing 5.33 is an example of its use, printing the picklist values of the Skill object’s Type field to the debug log.

Listing 5.33 Retrieving Picklist Metadata


Schema.DescribeFieldResult fieldInfo =
Schema.SObjectType.Skill__c.fields.Type__c;
List<Schema.PicklistEntry> picklistValues = fieldInfo.getPicklistValues();
for (Schema.PicklistEntry picklistValue : picklistValues) {
System.debug(picklistValue.getLabel());
}


Record Type Metadata

Record type metadata contains the names and unique identifiers of record types defined on an object. It also indicates the availability of the record type to the current user (isAvailable) and whether the record type is the default record type for the object (isDefaultRecordTypeMapping).

Listing 5.34 provides an example of using record type metadata. It retrieves the record types in the Contact object and prints their names to the debug log.

Listing 5.34 Retrieving Record Type Metadata


Schema.DescribeSObjectResult sobj = Contact.SObjectType.getDescribe();
List<Schema.RecordTypeInfo> recordTypes = sobj.getRecordTypeInfos();
for (Schema.RecordTypeInfo recordType : recordTypes) {
System.debug(recordType.getName());
}


Dynamic Instance Creation

Sometimes it can be useful to create an object instance without hard-coding its type in a program. For example, your program might include an extensibility mechanism for other developers to add or customize its behavior. One way to do this is to expose an Apex interface, document it, and allow users to provide the name of a custom Apex class that implements the interface. Listing 5.35 is a simplified version of this scenario that can run in the Execute Anonymous window.

Listing 5.35 Creating Instance from Type Name


interface MyType { void doIt(); }
class MyTypeImpl implements MyType {
public void doIt() { System.debug('hi'); }
}
Type t = MyTypeImpl.class;
if (t != null) {
MyType mt = (MyType)t.newInstance();
mt.doIt();
}


Notice that MyTypeImpl is defined as the type to be created in the program on line 5, so it isn’t dynamic. The dynamic form is Type.forName('MyTypeImpl'), which is invalid in the Execute Anonymous window because MyTypeImpl is transient, defined in the scope of the Execute Anonymous code block only. To try the dynamic type lookup, create the interface and class using the Force.com IDE.

Custom Settings in Apex

You are not limited to using the native user interface for managing data in custom settings, as demonstrated in Chapter 2. Custom settings can also be created, updated, and deleted using standard DML methods. This means you can build your own user interfaces for managing them, or use them to store frequently accessed, simple configuration values needed by your programs. Force.com provides increased performance for custom settings access versus ordinary database access, and custom settings are exempt from the governor limits placed on database access. For example, you might use a custom setting named Debug as a global switch to enable verbose logging within your Apex code.

To get started with custom settings in Apex, run the code in Listing 5.36. It inserts a custom setting record, setting its name and its field value. It assumes you already have defined a List type custom setting object named ConfigSetting containing a single Checkbox field named Debug.

Listing 5.36 Creating a Custom Setting Record


insert new ConfigSetting__c(Name = 'Default', Debug__c = false);


Now that your custom setting has a value, try retrieving it. Run the code in Listing 5.37 in the Force.com IDE’s Execute Anonymous view.

Listing 5.37 Retrieving a Custom Setting Value


ConfigSetting__c cfg = ConfigSetting__c.getValues('Default');
System.debug(cfg.Debug__c);


The first line retrieves the named record, Default, which you created in Listing 5.36. The second line prints the value of the custom field to the debug log. You can also retrieve a Map of all fields and values using the getAll method.

To update a custom setting value, retrieve it by name, and then update it as you would a database record. Listing 5.38 provides an example.

Listing 5.38 Updating a Custom Setting Record


ConfigSetting__c cfg = ConfigSetting__c.getValues('Default');
cfg.Debug__c = true;
update cfg;


You can also delete custom setting records using the delete DML method, as shown in Listing 5.39.

Listing 5.39 Deleting a Custom Setting Record


ConfigSetting__c cfg = ConfigSetting__c.getValues('Default');
delete cfg;


Hierarchy type custom settings allow a user or profile to be related to them. If no user or profile is specified, they become organization-wide defaults. The code in Listing 5.40 assumes you have created a Hierarchy type custom setting named HierarchySetting with a single text field named Field. It creates a new record and relates it to the current user by setting the system field SetupOwnerId to the current user’s unique identifier. This same field also accepts a profile unique identifier to make the custom setting apply to a profile instead of a user. And ifSetupOwnerId is set to null, it becomes an organization-wide default.

Listing 5.40 Creating a Hierarchy Type Custom Setting Record


insert new HierarchySetting__c(
SetupOwnerId = UserInfo.getUserId(),
Field__c = 'My user preference value');


To retrieve a Hierarchy type custom setting value, use the getInstance method of the custom setting object. By default, it returns the “lowest” level of setting value, meaning the value most specific to the current user. If a user-level setting is available, it is returned. Otherwise, the return value is the setting associated with the user’s profile. If no user or profile-level settings are present, the organization-wide default is returned. This behavior can be overridden by passing a user or profile unique identifier as an argument to the getInstance method.

Sample Application: Adding Email Notifications

This section applies your knowledge of Apex’s outbound email features to enhance the Services Manager sample application. Many scenarios in Services Manager could benefit from email notifications. For example, consultants have requested that they get an email when a timecard is approved or rejected by their project managers.

To implement this change, add a trigger on the after update event of the Timecard object. If the new value of the Timecard’s Status field is Approved or Rejected, query the Contact record that created the Timecard. Send an email notification of the change to the Contact.

Listing 5.41 is a sample implementation. It begins by checking to make sure that the updated Timecard contains a new value for the Status field and that the new status is either Approved or Rejected. If so, it makes three queries to retrieve data to send the notification email: the email address of the Contact logging the Timecard, the name of the Project, and the name of the user modifying the Timecard record. It constructs the email message and sends it.

Listing 5.41 Email Notification Trigger on Timecard


trigger handleTimecardNotifications
on Timecard__c (after update) {
for (Timecard__c timecard : trigger.new) {
if (timecard.Status__c !=
trigger.oldMap.get(timecard.Id).Status__c &&
(timecard.Status__c == 'Approved' ||
timecard.Status__c == 'Rejected')) {
Contact resource =
[ SELECT Email FROM Contact
WHERE Id = :timecard.Contact__c LIMIT 1 ];
Project__c project =
[ SELECT Name FROM Project__c
WHERE Id = :timecard.Project__c LIMIT 1 ];
User user = [ SELECT Name FROM User
WHERE Id = :timecard.LastModifiedById LIMIT 1 ];
Messaging.SingleEmailMessage mail = new
Messaging.SingleEmailMessage();
mail.toAddresses = new String[]
{ resource.Email };
mail.setSubject('Timecard for '
+ timecard.Week_Ending__c + ' on '
+ project.Name);
mail.setHtmlBody('Your timecard was changed to '
+ timecard.Status__c + ' status by '
+ user.Name);
Messaging.sendEmail(new Messaging.SingleEmailMessage[]
{ mail });
}
}
}


This implementation is not batch-safe. It makes four SOQL queries per Timecard. Even if this were addressed, the code could easily reach the limit of ten email invocations.

To fix this problem, you could change the code to use the MassEmailMessage, building a list of recipient Contact objects from the batch. Unfortunately, the MassEmailMessage’s whatIds field cannot be used with custom objects, so you’ll have to forgo the customized message detailing the changes to the Timecard.

An alternative is to anticipate the governor limit. If a batch of Timecards requires more than ten email notifications, send the ten and suppress subsequent notifications.

Summary

This chapter has introduced some of the advanced features of Apex, features that you might not need in every application but that contribute to your knowledge of what is possible with Apex. Before moving on to the next chapter, consider these final points:

Image Aggregate queries provide a standard, declarative way to perform calculations on groups of records in the database.

Image Rules governing record sharing can be controlled in Apex code using Apex managed sharing.

Image You can send and receive emails in Apex code. This provides your applications an additional way to interact with users.

Image Although Apex features strongly typed database objects and queries, you can also write code that uses database resources dynamically. This carries with it the risk of runtime errors but opens up new possibilities of dynamic behavior to your applications. It is particularly powerful when writing custom user interfaces.

Image You can read and write custom settings from Apex like any database object, but without the governor limits.