Appirio's Tech Blog

Thursday, August 20, 2009

Rich Internet Applications Using Flex, Salesforce.com and Google App Engine

Jeff Douglas

It’s fairly common these days to see Flex applications running inside Salesforce.com. But what if you'd like to run your Flex applications on another SaaS provider like Google App Engine or Amazon EC2. We are going to set up a simple Flex application that fetches Accounts and Opportunities from Salesforce.com using an open source remoting library that runs on Google App Engine. Here a quick peak at the final application:

flex-graniteDS-screenshot

You can run this demo at: http://jeffdouglas-sfdc-graniteds.appspot.com/main.html

Flex can communicate with a Java backend using HTTP, SOAP-based web services or Adobe’s proprietary AMF format. There are a number of open source AMF implementations including Aodobe BlazeDS, WebORB and GraniteDS. These implementations allow you to communicate via JMS or Flex remoting and are more efficient and exponentially faster than using XML across the wire. For this application we are going to be utilizing GraniteDS. The GraniteDS remoting service is a high performance data transfer service that allows your Flex applications to directly invoke Java object methods on your application and consume the return values natively. The objects returned from the server-side methods are automatically deserialized into either dynamic or typed ActionScript objects.

Setting Up Your Environment

To get started you'll need to download the Adobe Flex 3.0 Builder 3 or Flex 3 Builder Plug-In. There’s a 60 day trial if you don’t already have a license. Installation is pretty straight-forward if you are familiar with the Eclipse installation process. The plug-in is a little easier and quicker to install.

If you don't have a Google App Engine account, you can create one here. You'll also need to download and install the Google App Engine SDK and Eclipse plug-in. You can find details on this process here.

Creating Your Project

Now that our environment is setup, create a new Web Application Project and uncheck “Use Google Web Toolkit”. Since we are going to be using Flex as the front end for our application you will want to add the Flex Project Nature to your project. Right click on the project name in the left panel and select Flex Project Nature -> Add Flex Project Nature. Choose “Other” as the application server and click Next and then Finish. This will automatically create your Flex main.mxml file for you in the src directory.

After the file has been created you should see the following error message in the Eclipse Problems tab, "Cannot create HTML wrapper. Right-click here to recreate folder html-template." To fix this simply right click on the error message and select “Recreate HTML Templates”.

Adding Required Libraries

There are a number of libraries that we are going to need for Salesforce.com and GraniteDS. Download the latest version of GraniteDS from here, unzip the files, find the granite.jar in the graniteds/build/ directory and place it into your project’s /WEB-INF/lib/ directory.

You’ll also need to get the latest version of Xalan-J from here. Unzip the files and copy serializer.jar and xalan.jar into your project’s /WEB-INF/lib directory.

There are two jar files you will need for the Salesforce.com integration. Download the Force.com Web Service Connector files from here and copy them to your project's /WEB-INF/lib directory:

  • partner-library.jar - the objects and methods from the Force.com partner WSDL

  • wsc-gae-16_0.jar - the Web Service parsing and transport for GAE-Java


You'll need to add these new jar files to your Java build path in Eclipse by right clicking on the project name and selecting Properties. Select Java Build Path -> Libraries and add your jars from the lib directory.

Server Configuration

So now that we have all of requirements in place we can start configuring our application. Place the following code into your /WEB-INF/web-xml file between the tags to tell App Engine which classes GraniteDS utilizes.

<!-- GraniteDS -->
<listener>
<listener-class>org.granite.config.GraniteConfigListener</listener-class>
</listener>

<!-- handle AMF requests serialization and deserialization -->
<filter>
<filter-name>AMFMessageFilter</filter-name>
<filter-class>org.granite.messaging.webapp.AMFMessageFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AMFMessageFilter</filter-name>
<url-pattern>/graniteamf/*</url-pattern>
</filter-mapping>

<!-- processes AMF requests -->
<servlet>
<servlet-name>AMFMessageServlet</servlet-name>
<servlet-class>org.granite.messaging.webapp.AMFMessageServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>AMFMessageServlet</servlet-name>
<url-pattern>/graniteamf/*</url-pattern>
</servlet-mapping>


You'll also want to change the welcome-file to specify main.html instead of the generated index.html file.

GraniteDS communicates with the servlet container via a remoting destination. A Remoting destination exposes a Java class that your Flex application can invoke remotely. The destination id is the logical name that your Flex application uses to refer to the remote class. This eliminates the need to hardcode a reference to the fully qualified Java class name. This logical name is mapped to the Java class name as part of the destination configuration in services-config.xml. Create a new folder under /WEB-INF/ called “flex” and create the following services-config.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service
id="granite-service"
class="flex.messaging.services.RemotingService"
messageTypes="flex.messaging.messages.RemotingMessage">
<destination id="Gateway">
<channels>
<channel ref="my-graniteamf"/>
</channels>
<properties>
<scope>application</scope>
<source>com.appirio.Gateway</source>
</properties>
</destination>
</service>
</services>

<channels>
<channel-definition id="my-graniteamf" class="mx.messaging.channels.AMFChannel">
<endpoint
uri="/graniteamf/amf"
class="flex.messaging.endpoints.AMFEndpoint"/>
</channel-definition>
</channels>
</services-config>


Our remoting destination points to a class called Gateway that we will create shortly. Now we need to tell the Flex compiler where to find the services file that defines our remoting destination. Right click on the project name in the left panel and select Properties -> Flex Compiler. Replace your compiler arguments with the following:
-locale en_US -services ../war/WEB-INF/flex/services-config.xml

Client-Side Code

Now that our server is configured we can start working on the Flex client. Our client is fairly simple and allows a user select an Account and view/create Opportunities for it. The Flex portion of the application is represented by a single MXML file. For larger applications you would typically break your application into an MVC model and possibly use some sort of Flex framework like Cairngorm, Mate or PureMVC. Since our application is so small there is really no need to implement any sort of framework.

One of the most important parts of this file is the RemoteObject tag at the top. The id of the tag (gateway) is used by the application to reference the object while the destination (Gateway) is same destination we set up in our services-config.xml file specifying our remoting destination of com.appirio.Gateway.

The individual methods specified by the RemoteObject tag map directly to methods in the Gateway class that we are about to define.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
creationComplete="gateway.getAccounts()">

<mx:RemoteObject id="gateway" destination="Gateway" fault="Alert.show(event.fault.toString());">
<mx:method name="getAccounts" result="storeAccounts(event)" fault="Alert.show(event.fault.faultString);" />
<mx:method name="getOpportunities" result="storeOpportunities(event)" fault="Alert.show(event.fault.faultString);" />
<mx:method name="createOpportunity" result="fetchOpportunities(event)" fault="Alert.show(event.fault.faultString);" />
</mx:RemoteObject>

<mx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.collections.ArrayCollection;
import mx.rpc.events.ResultEvent;

[Bindable] private var accounts:ArrayCollection;
[Bindable] private var opportunities:ArrayCollection;
[Bindable] private var selectedAccount:Account;

// store the accounts returned from Salesforce.com, select the first account as
// the currently selected one and then fetch its opportunities from Salesforce.com
private function storeAccounts(event:ResultEvent):void {
accounts = event.result as ArrayCollection;
selectedAccount = accounts.getItemAt(0) as Account;
gateway.getOpportunities(selectedAccount.id);
}

// store the list of opportunities returned from Salesforce.com
private function storeOpportunities(event:ResultEvent):void {
opportunities = event.result as ArrayCollection;
}

// fetch the opportunities from Salesforce.com after a new opportunities has been created
// and returned from the createOpportunity() remote object method
private function fetchOpportunities(event:ResultEvent):void {
gateway.getOpportunities(selectedAccount.id);
}

private function changeAccount():void {
opportunities = null;
selectedAccount = cbxAccount.selectedItem as Account;
gateway.getOpportunities(selectedAccount.id);
}

private function saveOpportunity():void {

var opp:Opportunity = new Opportunity();
opp.accountId = selectedAccount.id;
opp.probability = frmProbability.text;
opp.stageName = frmStage.text;
opp.amount = frmAmount.text;
opp.closeDate = dateFormatter.format(frmCloseDate.selectedDate);
opp.name = frmName.text;
opp.orderNumber = frmOrder.text;

// create the opportunity
gateway.createOpportunity(opp);

// reset the form
frmProbability.selectedIndex = 0;
frmStage.selectedIndex = 0;
frmAmount.text = null;
frmCloseDate.selectedDate = null;
frmName.text = null;
frmOrder.text = null;

}

]]>
</mx:Script>

<mx:DateFormatter id="dateFormatter" formatString="MM/DD/YYYY"/>

<mx:Label x="10" y="14" text="Telesales Demo with Salesforce.com, GraniteDS and Google App Engine" fontSize="18" color="#FFFFFF"/>

<mx:VBox top="50" bottom="10" left="10" right="10">
<mx:HBox width="100%" height="50%">
<mx:Panel width="50%" height="100%" layout="absolute" title="Account Display">
<mx:Form width="100%" height="100%">
<mx:FormItem label="Account">
<mx:ComboBox
id="cbxAccount"
dataProvider="{accounts}"
labelField="name"
change="changeAccount()"/>
</mx:FormItem>
<mx:FormItem label="City">
<mx:Text text="{selectedAccount.city}"/>
</mx:FormItem>
<mx:FormItem label="State">
<mx:Text text="{selectedAccount.state}"/>
</mx:FormItem>
<mx:FormItem label="Phone">
<mx:Text text="{selectedAccount.phone}"/>
</mx:FormItem>
<mx:FormItem label="Website">
<mx:Text text="{selectedAccount.website}"/>
</mx:FormItem>
</mx:Form>
</mx:Panel>
<mx:Panel width="50%" height="100%" layout="absolute" title="New Opportunity for {selectedAccount.name}">
<mx:Form width="100%" height="100%">
<mx:FormItem label="Name">
<mx:TextInput id="frmName"/>
</mx:FormItem>
<mx:FormItem label="Amount">
<mx:TextInput id="frmAmount"/>
</mx:FormItem>
<mx:FormItem label="Stage">
<mx:ComboBox id="frmStage">
<mx:dataProvider>
<mx:String>Prospecting</mx:String>
<mx:String>Qualifications</mx:String>
<mx:String>Value Proposition</mx:String>
</mx:dataProvider>
</mx:ComboBox>
</mx:FormItem>
<mx:FormItem label="Probability">
<mx:ComboBox id="frmProbability">
<mx:dataProvider>
<mx:String>10</mx:String>
<mx:String>25</mx:String>
<mx:String>50</mx:String>
<mx:String>75</mx:String>
</mx:dataProvider>
</mx:ComboBox>
</mx:FormItem>
<mx:FormItem label="Close Date">
<mx:DateField id="frmCloseDate"/>
</mx:FormItem>
<mx:FormItem label="Order">
<mx:TextInput id="frmOrder"/>
</mx:FormItem>
<mx:FormItem>
<mx:Button label="Save Opportunity" click="saveOpportunity()"/>
</mx:FormItem>
</mx:Form>
</mx:Panel>
</mx:HBox>
<mx:Panel width="100%" height="50%" layout="absolute" title="Opportunities for {selectedAccount.name}">
<mx:DataGrid width="100%" height="100%" id="dgOpps" dataProvider="{opportunities}">
<mx:columns>
<mx:DataGridColumn headerText="Name" dataField="name"/>
<mx:DataGridColumn headerText="Amount" dataField="amount"/>
<mx:DataGridColumn headerText="Stage" dataField="stageName"/>
<mx:DataGridColumn headerText="Probability" dataField="probability"/>
<mx:DataGridColumn headerText="Close Date" dataField="closeDate"/>
<mx:DataGridColumn headerText="Order" dataField="orderNumber"/>
</mx:columns>
</mx:DataGrid>
</mx:Panel>
</mx:VBox>

</mx:Application>


One last thing we need for the front end are Account and Opportunity value objects. Right-click the src folder and select New -> ActionScript Class. Enter the class name as and click Finish. Add the following code to these classes. Notice that the code uses the [RemoteClass(alias=" com.appirio.Account")] annotation to map the ActionScript version of the Account class (Account.as) to the Java version (Account.java). As a result, Account objects returned by methods of the Java object in the service layer that are deserialized into instances of the ActionScript Account class automatically for you.

package
{
[Bindable]
[RemoteClass(alias="com.appirio.Account")]
public class Account
{

public var id:String;
public var name:String;
public var city:String;
public var state:String;
public var phone:String;
public var website:String;

}
}


package
{
[Bindable]
[RemoteClass(alias="com.appirio.Opportunity")]
public class Opportunity
{

public var id:String;
public var name:String;
public var amount:String;
public var stageName:String;
public var probability:String;
public var closeDate:String;
public var orderNumber:String;
public var accountId:String;

}
}


Server-Side Code

So now back to the server-side to finish up our application. We need to add the POJOs to model our Account and Opportunity object returned from Salesforce.com. These classes will consist of the same members as the ActionScript classes so that GraniteDS can translate them back and forth for us.

package com.appirio;

public class Account {

private String id;
private String name;
private String city;
private String state;
private String phone;
private String website;

public Account(String id, String name, String city, String state, String phone, String website) {
this.id = id;
this.name = name;
this.city = city;
this.state = state;
this.phone = phone;
this.website = website;
}

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
}


package com.appirio;

public class Opportunity {

private String id;
private String name;
private String amount;
private String stageName;
private String probability;
private String closeDate;
private String orderNumber;
private String accountId;

public Opportunity(String id, String name, String amount, String stageName, String probability, String closeDate, String orderNumber) {
this.id = id;
this.name = name;
this.amount = amount;
this.stageName = stageName;
this.probability = probability;
this.closeDate = closeDate;
this.orderNumber = orderNumber;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAmount() {
return amount;
}

public void setAmount(String amount) {
this.amount = amount;
}

public String getStageName() {
return stageName;
}

public void setStageName(String stageName) {
this.stageName = stageName;
}

public String getProbability() {
return probability;
}

public void setProbability(String probability) {
this.probability = probability;
}

public String getCloseDate() {
return closeDate;
}

public void setCloseDate(String closeDate) {
this.closeDate = closeDate;
}

public String getOrderNumber() {
return orderNumber;
}

public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}

public String getAccountId() {
return accountId;
}

public void setAccountId(String accountId) {
this.accountId = accountId;
}

}


The last part of our application is the Gateway class that GraniteDS uses as the remoting endpoint. This class contains the methods that our Flex front-end calls via the RemoveObject tag.

package com.appirio;

import java.text.DateFormat;
import java.util.logging.Logger;
import java.util.Date;
import java.util.Vector;

import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.soap.partner.QueryResult;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
import com.sforce.soap.partner.sobject.SObject;

public class Gateway {

private static final Logger log = Logger.getLogger(Gateway.class.getName());
private String username = "YOUR-SALESFORCE-USERNAME";
private String password = "YOUR-SALESFORCE-PASSWORD-AND-TOKEN";
private PartnerConnection connection;

// query for 10 accounts in Salesforce.com
public Vector<Account> getAccounts() {

// get a new connection to Salesforce.com ising the Force.com Web Service Connector (WSC) toolkit
getConnection();

QueryResult result = null;
Vector<Account> accounts = new Vector<Account>();

try {
result = connection.query("Select Id, Name, Phone, BillingCity, BillingState, website from Account LIMIT 10");

if (result.getSize() > 0) {

for (SObject account : result.getRecords()) {
Account a = new Account(
(String)account.getField("Id"),
(String)account.getField("Name"),
(String)account.getField("BillingCity"),
(String)account.getField("BillingState"),
(String)account.getField("Phone"),
(String)account.getField("website")
);
accounts.add(a);

}

}

} catch (ConnectionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return accounts;

}

// query for all opportunities for an account
public Vector<Opportunity> getOpportunities(String accountId) {

// get a new connection to Salesforce.com ising the Force.com Web Service Connector (WSC) toolkit
getConnection();

QueryResult result = null;
Vector<Opportunity> opportunities = new Vector<Opportunity>();

try {
result = connection.query("Select id, name, stagename, amount, closeDate, probability, ordernumber__c from Opportunity where AccountId = '"+accountId+"'");

if (result.getSize() > 0) {

for (SObject opp : result.getRecords()) {
Opportunity o = new Opportunity(
(String)opp.getField("Id"),
(String)opp.getField("Name"),
(String)opp.getField("Amount"),
(String)opp.getField("StageName"),
(String)opp.getField("Probability"),
(String)opp.getField("CloseDate"),
(String)opp.getField("OrderNumber__c")
);
opportunities.add(o);
}

}

} catch (ConnectionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return opportunities;

}

// create a new opportunity in Salesforce.com
public Boolean createOpportunity(Opportunity o) {

// get a new connection to Salesforce.com ising the Force.com Web Service Connector (WSC) toolkit
getConnection();

Boolean success = true;
Date closeDate = new Date();

// try and parse the date
try {
DateFormat df = DateFormat.getDateInstance(3);
closeDate = df.parse(o.getCloseDate());
} catch(java.text.ParseException pe) {
System.out.println("Exception " + pe);
}

// populate the new opportunity
SObject opp = new SObject();
opp.setType("Opportunity");
opp.setField("Name", o.getName());
opp.setField("Amount", new Double(o.getAmount()).doubleValue());
opp.setField("StageName", o.getStageName());
opp.setField("Probability", new Double(o.getProbability()).doubleValue());
opp.setField("CloseDate", closeDate);
opp.setField("OrderNumber__c", o.getOrderNumber());
opp.setField("AccountId", o.getAccountId());

SObject[] opportunities = {opp};

try {
connection.create(opportunities);
} catch (ConnectionException e) {
// TODO Auto-generated catch block
success = false;
e.printStackTrace();
}

return success;

}

void getConnection() {
try {
if ( connection == null ) {
log.info("Fetching new connection....");
// login to salesforce
ConnectorConfig config = new ConnectorConfig();
config.setUsername(username);
config.setPassword(password);
connection = Connector.newConnection(config);
} else {
log.info("Reusing existing connection....");
}
} catch ( ConnectionException ce) {
log.warning("ConnectionException " +ce.getMessage());
}

}

}


Our last step before uploading our application to Google App Engine is to build and export for deployment. Right click on the project name in the left panel and select Export. Choose Flex Builder folder -> Release Build -> Next. In the Export to folder section browse to your war for the project. We want to build our Flex application to this folder so that the App Engine plug-in will deploy this code to App Engine along with our Java code.

Our last step it is to create a new App Engine application and upload our code to Google's servers. There is detailed help for uploading with the Eclipse plug-in here.

Tuesday, August 11, 2009

Writing Bulk Triggers for Salesforce.com

Jeff Douglas

It's exciting to see all of the new members on the Salesforce.com message board that are just getting into cloud computing. Some of the most common questions revolve around how to write, test and debug "bulk" triggers. Programming for a multi-tenant environment is different than developing for a dedicated server and it's understandable that developers coming from a Java or .NET background will have some sort of ramp up time.

This article does not go over all aspects of triggers or bulk processing, so please see the Apex docs for more info. There is some really good documentation and tutorials on writing Apex triggers and unit testing but it seems to be spread out over different documents and wiki pages. My goal is to pull together all of this disparate info together into one tutorial and demonstrate how to write, and not write, triggers for Salesforce.com.

What exactly is a trigger? If you come from a SQL Server or Oracle background you will have some trigger experience, however a trigger in Salesforce.com is slightly different. According to the Apex documentation, a trigger is an Apex script that executes before or after insert, update, or delete events occur, such as before object records are inserted into the database, or after records have been deleted. When a trigger fires it can process anywhere from 1 to 200 records so all triggers should be written to accommodate bulk transactions. Examples of single record and bulk transactions include:

  • Data import
  • Bulk Force.com API calls
  • Mass actions, such as record owner changes and deletes
  • Recursive Apex methods and triggers that invoke bulk DML statements
First I'm going to outline the wrong way to write triggers. I think this is important to demonstrate, as it is how must new developers begin. Remember, this is wrong way to write a trigger. Here is the use case for the trigger. Each time an Account is created or updated, you want to fetch the Account owner's "favorite color" from a custom field on their User record and write it into the Account's record.

Coming from a Java or .NET background, I would probably write a trigger something like the following. This would be perfectly acceptable if you only needed to update a single record each time.

trigger AddOwnerColor on Account (before insert, before update) {

/** EXAMPLE OF HOW TO -- NOT -- WRITE A BULK TRIGGER **/

// iterate over the list of records being processed in the trigger and
// set the color before being inserted or updated
for (Account a : Trigger.new)
  a.Owner_Favorite_Color__c = [Select Favorite_Color__c from User Where Id = :a.OwnerId].Favorite_Color__c;

}


Here is the test class for the trigger. The first unit test (testSingleInsert) runs fine as it does not encounter any governors. The second unit test (testBulkInsert) will fail when trying to insert 200 records with the followning message:

System.DmlException: Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, AddOwnerColor: execution of BeforeInsert
caused by: System.Exception: Too many SOQL queries: 101

There are a number of governors that the Apex runtime engine enforces for specific contexts and entry points (trigger, tests, anonymous block execution, controllers and WSDL methods). You should study "Understanding Execution Governors and Limits" religiously as it will dictate how you develop for the Force.com platform. It is suggested that you test bulk processing with at least 25 records but I typically use 200. My reasoning is that if you only use 25 records and do not utilize the testing runtime context (Test.startTest) your trigger will run without errors. However, once you put this trigger into Production it will throw an exception when processing the 21st record. The reason is that triggers have a 20 SOQL query limit while in RunTest it has a limit of 100 SOQL queries.

@isTest
private class TestAccountColorTrigger {

static testMethod void testSingleInsert() {

  Profile p = [select id from profile where name='Standard User'];

  User u = new User(alias = 'test123', email='test123@noemail.com',
      emailencodingkey='UTF-8', lastname='Testing', languagelocalekey='en_US',
      localesidkey='en_US', profileid = p.Id, country='United States',
      Favorite_Color__c='Pretty Pink',
      timezonesidkey='America/Los_Angeles', username='test123@noemail.com');
  insert u;

  Account a = new Account(
      Name = 'Test Account',
      OwnerId = u.Id
  );

  insert a;

  // assert that the color on the account matches the account owner's color on their user record
  System.assertEquals(u.Favorite_Color__c,[Select Owner_Favorite_Color__c From Account Where Id = :a.Id].Owner_Favorite_Color__c);

}

static testMethod void testBulkInsert() {

  List<Account> accounts = new List<Account>();
  Profile p = [select id from profile where name='Standard User'];

  User u = new User(alias = 'test123', email='test1234@noemail.com',
      emailencodingkey='UTF-8', lastname='Testing', languagelocalekey='en_US',
      localesidkey='en_US', profileid = p.Id, country='United States',
      Favorite_Color__c='Pretty Pink',
      timezonesidkey='America/Los_Angeles', username='test1234@noemail.com');
  insert u;

  for (Integer i=0;i<200;i++) {

      Account a = new Account(
          Name = 'Test Account',
          OwnerId = u.Id
      );
      accounts.add(a);

  }

  insert accounts;

}
}


Writing the Bulk Safe Trigger

To write bulk safe triggers, it is critical that you understand and utilize sets and maps. Sets are used to isolate distinct records, while maps are name-value pairs that hold the query results retrievable by record id. So here is the bulk safe trigger code. When the trigger fires we initially create a set containing all of the distinct OwnerIds for the records being processed (1 -> 200). Then we query to find all of the User records for the OwnerIds in the set. This returns a map with the UserId as the key and the User object as the value. We then iterate over the list of Accounts in the trigger, use the map's get method to fetch the correct User object by its OwnerId and write the User's favorite color into a custom field (Owner_Favorite_Color__c) on the Account. When the records are committed to the database, the User's color auto-magically appears!

trigger AddOwnerColor on Account (before insert, before update) {

// create a set of all the unique ownerIds
Set<Id> ownerIds = new Set<Id>();
for (Account a : Trigger.new)
  ownerIds.add(a.OwnerId);

// query for all the User records for the unique userIds in the records
// create a map for a lookup / hash table for the user info
Map<Id, User> owners = new Map<Id, User>([Select Favorite_Color__c from User Where Id in :ownerIds]);

// iterate over the list of records being processed in the trigger and
// set the color before being inserted or updated
for (Account a : Trigger.new)
  a.Owner_Favorite_Color__c = owners.get(a.OwnerId).Favorite_Color__c;

}


To test the trigger, we've made a few modifications to test for governor limits. You can use the system static startTest and stopTest methods to ensure your code runs properly in the runtime context. You should set up all of your variables, data structures, etc and then call startTest. After you call this method, the limits that get applied are based on your first DML statement (INSERT, UPSERT, etc). The stopTest method marks the point in your test code when your test ends and any post assertions are done in the original context. The graphic below displays the resource summary in both the RunTest context as well testing context.





We've also added the modification to allow the DML statement to run as a specific user. According to the Apex docs, generally all Apex scripts run in system mode, and the permissions and record sharing of the current user are not taken into account. The system method runAs enables you to write test methods that change user contexts to either an existing user or a new user. All of that user's record sharing is then enforced.

@isTest
private class TestAccountColorTrigger {

static testMethod void testBulkInsert() {

  List<Account> accounts = new List<Account>();

  Profile p = [select id from profile where name='Marketing User'];
  // create a user to run the test as
  User u = new User(alias = 'test123', email='test1234@noemail.com',
      emailencodingkey='UTF-8', lastname='Testing', languagelocalekey='en_US',
      localesidkey='en_US', profileid = p.Id, country='United States',
      Favorite_Color__c='Buttercup Yellow',
      timezonesidkey='America/Los_Angeles', username='test1234@noemail.com');
  insert u;

  Profile p1 = [select id from profile where name='Standard User'];
  // create a user to own the account
  User u1 = new User(alias = 'test123', email='test12345@noemail.com',
      emailencodingkey='UTF-8', lastname='Testing', languagelocalekey='en_US',
      localesidkey='en_US', profileid = p1.Id, country='United States',
      Favorite_Color__c='Pretty Pink',
      timezonesidkey='America/Los_Angeles', username='test12354@noemail.com');
  insert u1;

  // add 200 accounts to the list to be inserted
  for (Integer i=0;i<200;i++) {

      Account a = new Account(
          Name = 'Test Account',
          OwnerId = u1.Id
      );
      accounts.add(a);

  }

  // Switch to the runtime context
  Test.startTest();

  // run as a different user to test security and rights
  System.runAs(u) {
      insert accounts;
  }

  // Switch back to the original context
  Test.stopTest();

  // query for all accounts created and assert that the color was added correctly
  for (Account acct : [Select Owner_Favorite_Color__c from Account Where OwnerId = :u1.Id])
      System.assertEquals(acct.Owner_Favorite_Color__c,'Pretty Pink');

}
}


When you run your tests you should see the following:



Monday, August 3, 2009

Using Talend to Export Data from Salesforce.com

Ward Loving

There is a new software company which is offering an Open Source alternative for data migration and integration to the vendors such as Informatica and Cast Iron Systems. The company is called Talend and they have recently released version 3.1 of their Talend Open Studio – an Eclipse-based data integration tool. Talend’s approach is very interesting. Remember all those code generation tools of yore. Well, it seems that this paradigm hasn’t completely gone out the window – the Talend Open Studio is a powerful code generator –either in Java or Perl. After the code is generated and compiled, it can then be run directly from the Studio workspace or exported and run in standalone mode. Talend also gives you the capability of implementing parameters for your jobs so the same code can run in different environments or contexts. But I’m getting ahead of myself. Let’s walk through a relatively simple scenario to introduce the tool. You’ll probably want to go to http://www.talend.com and download the latest version of the Talend Open Studio to follow along with steps outlined below.

When you download the tool the first thing you’ll have to do is set up your local Repository. Click the ‘…’ button to start the setup.

Fig 1 – Welcome Dialog



Only the User E-mail field is required to setup your local repository.    Click ‘OK’ to return to the initial splash page.  You’ll only need to return to do the repository setup once.

Fig 2 – Setting up the repository



The next thing to do is to create a new local project. After you’ve selected ‘Create a local project’ from the dropdown, click ‘Go!’

Fig 3 – Welcome Dialog – Create new Local Project




Talend displays a New Project dialog box and asks you what language you’d like to use. I’m a Java man myself so I that’s what I chose. The User Guide discourages you from mixing the two programming environments in the same workspace.

Fig 4 – The new project dialog




This creates the project on the initial splash screen. Click ‘Open’ to display the Talend Open Studio workspace.

Fig 5 – Welcome Dialog – Open your project



The user interface will be somewhat familiar to those accustomed to Eclipse. Your Repository is where your Jobs will be organized and stored. The Palette is where the components for your jobs reside.

It’s best to explore the interface with an actual job so let’s go ahead and create one. Right-click on the Job Design section of the Repository.

Fig 6 – Repository View – Job Designs



And create your job in the New Job dialog box. Only the job name is required.

Fig 7 – New Job Dialog



This brings you back to Talend workspace with the component palette visible. At this point you can start to get a sense of the power and flexibility available with the tool. The palette has a variety of File Input and Output components, Database integration tools, business systems integration tools for vendors like SAP and Salesforce.com, custom coding capabilities, and of course a Map component for transformations between systems.

Fig 8 – Component Palette



To make use of a component simply drag it out on to the canvas in the center part of the workspace. The input components push data into your jobs and the output components draw it out. Since I’m pretty heavily involved in Salesforce.com these days, let’s use a tSalesforceInput component for our first job. It’s under Business->Salesforce in the Component Palette. Drag that component onto the canvas and right-click it to bring up the context menu. Select “Settings” from the context menu to enter your connection information.

Fig 9 -- Canvas



I’m going to download the Users in my Salesforce org to a spreadsheet so I’ve selected the User Module from the Basic Settings of the tSalesforceInput component. You’ll need to enter your connection parameters and remove the Query Condition – ‘name = talend’.

Next I’ll drag a tFileOutputDelimited component onto the canvas. To create a data flow I connect the two components. I’ll need to right-click again on tSalesforceInput and select
Row ->Main from the context menu.

Fig 10 – Context Menu



This will join the two components and create our first subjob. A subjob, which is highlighted in blue, is a grouping of components assembled to complete particular task. It is possible, though perhaps not advisable, to create several subjobs in a single job design. A particular job may also have pre-processing and post-processing components.

Fig 11 – Subjob on the canvas



Right-click on the tFileOutputDelimited component and bring up the settings menu. Click the sync columns button to copy the Salesforce User object schema to the schema of the output file component. You might also want to change some other settings in your delimited file. I generally use comma-delimited files myself so I update the field separator, and click the Include Header checkbox.

Fig 12 – Component settings tab



I also select the CSV options checkbox on the Advanced settings tab to reveal the quotes as escape characters.

Fig 13 – tFileOuputDelimited Advanced Settings



Alright, we have a simple job built and ready to go. Click the Run tab at the bottom of the screen. I like to get a little feedback to see if you my job is working so I’ve selected the Statistics checkbox. You can get even more feedback by selecting Traces. When you click ‘Run’ your job will first compile and then execute. If there are errors in the job, Talend will display a stack trace in the console by default and conveniently you can switch to the code tab of the canvas to see what part of the job is causing the issue. You don’t modify Talend-generated code directly unless your code is hosted within a Custom Code component like a tJava or tJavaRow component.

Fig 14 – Run tab



I click ‘Run’ and can see from the statistics on the canvas that I successfully pulled down three rows into my delimited file.

So your might be asking yourself, "Hey wait a second, can’t I just do this with Data Loader?"  And the answer is yes of course you can. In fact, Talend doesn’t have some of capabilities that we’ll need to do production ready loads of data with Salesforce – most notably it doesn’t have the success and failure reporting capabilities of Data Loader – so you can’t really pitch Data Loader yet. What we haven’t shown in this simple example is power of Talend to do lookups with external information , to visually map between fields and to do transformations to the data itself. We’ll take on these more potent capabilities in Part 2 of the series when we take a closer look at the tMap component. In one of my recent projects I used Talend to do the transformation and lookups of related information and used Data Loader to actually perform the inserts and updates to Salesforce.com.

 
2006-2012 Appirio Inc. All rights reserved.
Appirio.com | Support | Resource Center | Contact | Careers | Privacy Policy