在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:mitchspano/apex-trigger-actions-framework开源软件地址:https://github.com/mitchspano/apex-trigger-actions-framework开源编程语言:Apex 100.0%开源软件介绍:Apex Trigger Actions FrameworkThis project is meant to demonstrate an Apex Trigger Framework which is built with the following goals in mind:
Metadata Driven Trigger ActionsIn order to use this trigger framework, we start with the trigger OpportunityTrigger on Opportunity (
before insert,
after insert,
before update,
after update,
before delete,
after delete,
after undelete
) {
new MetadataTriggerHandler().run();
} To define a specific action, we write an individual class which implements the correct context interface. public class TA_Opportunity_StageInsertRules implements TriggerAction.BeforeInsert {
@TestVisible
private static final String PROSPECTING = 'Prospecting';
@TestVisible
private static final String INVALID_STAGE_INSERT_ERROR = 'The Stage must be \'Prospecting\' when an Opportunity is created';
public void beforeInsert(List<Opportunity> newList){
for (Opportunity opp : newList) {
if (opp.StageName != PROSPECTING) {
opp.addError(INVALID_STAGE_INSERT_ERROR);
}
}
}
} This allows us to use custom metadata to configure a few things from the setup menu:
The setup menu provides a consolidated view of all of the actions that are executed when a record is inserted, updated, deleted, or undeleted. The Now, as future development work gets completed, we won't need to keep modifying the bodies of our triggerHandler classes, we can just create a new class for each new piece of functionality that we want and configure those to run in a specified order within a given context. Note that if an Apex class is specified in metadata and it does not exist or does not implement the correct interface, a runtime error will occur. With this multiplicity of Apex classes, it would be wise to follow a naming convention such as {
"packageDirectories": [
{
"path": "application/base",
"default": true
},
{
"path": "application/opportunity-automation",
"default": false
}
],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "50.0"
} Support for FlowsThe trigger actions framework can also allow you to invoke a flow by name, and determine the order of the flow's execution amongst other trigger actions in a given trigger context. Here is an example of a trigger action flow that checks if a record's status has changed and if so it sets the record's description to a default value. Enable Flows for an sObjectTo enable Trigger Action Flows on a given sObject, you must first author a class which creates an Apex defined data type to be referenced in flows and can generate the required input to launch the flow from a trigger context. This class must extend public with sharing class OpportunityTriggerRecord extends FlowTriggerRecord {
public OpportunityTriggerRecord() {
super();
}
public OpportunityTriggerRecord(
Opportunity newRecord,
Opportunity oldRecord,
Integer newRecordIndex,
Integer triggerActionFlowIdentifier
) {
super(newRecord, oldRecord, newRecordIndex, triggerActionFlowIdentifier);
}
@AuraEnabled
public Opportunity newRecord {
get {
return (Opportunity) this.newSObject;
}
set {
this.newSObject = value;
}
}
@AuraEnabled
public Opportunity oldRecord {
get {
return (Opportunity) this.oldSObject;
}
}
public override Map<String, Object> getFlowInput(
List<SObject> newList,
List<SObject> oldList,
Integer triggerActionFlowIdentifier
) {
List<SObject> collection = newList != null ? newList : oldList;
List<OpportunityTriggerRecord> triggerRecords = new List<OpportunityTriggerRecord>();
for (Integer i = 0; i < collection.size(); i++) {
Opportunity newRecord = newList != null ? (Opportunity) newList.get(i) : null;
Opportunity oldRecord = oldList != null ? (Opportunity) oldList.get(i) : null;
triggerRecords.add(
new OpportunityTriggerRecord(
newRecord,
oldRecord,
i,
triggerActionFlowIdentifier
)
);
}
return new Map<String, Object>{
TriggerActionFlow.TRIGGER_RECORDS_VARIABLE => triggerRecords
};
}
} Once this class is defined, the name of the class must be specified on the Define a FlowTo make your flows usable, they must be auto-launched flows and you need to create the following flow resource variable:
To enable this flow, simply insert a trigger action record with Apex Class Name equal to Compatibility with sObjects from Installed PackagesThe Trigger Actions Framework supports standard objects, custom objects, and objects from installed packages. To use the framework with an object from an installed package, separate the Object API Name from the Object Namespace on the sObject Trigger Setting itself. For example, if you want to use the Trigger Actions Framework on an sObject called
Recursion PreventionUse the public class TA_Opportunity_RecalculateCategory implements TriggerAction.AfterUpdate {
public void afterUpdate(List<Opportunity> newList, List<Opportunity> oldList) {
Map<Id,Opportunity> oldMap = new Map<Id,Opportunity>(oldList);
List<Opportunity> oppsToBeUpdated = new List<Opportunity>();
for (Opportunity opp : newList) {
if (
TriggerBase.idToNumberOfTimesSeenAfterUpdate.get(opp.id) == 1 &&
opp.StageName != oldMap.get(opp.id).StageName
) {
oppsToBeUpdated.add(opp);
}
}
if (!oppsToBeUpdated.isEmpty()) {
this.recalculateCategory(oppsToBeUpdated);
}
}
private void recalculateCategory(List<Opportunity> opportunities) {
//do some stuff
update opportunities;
}
} Bypass MechanismsYou can also bypass execution on either an entire sObject, or for a specific action. Bypass from Setup MenuTo bypass from the setup menu, simply navigate to the sObject Trigger Setting or Trigger Action metadata record you are interested in and check the Bypass Execution checkbox. These bypasses will stay active until the checkbox is unchecked. Static BypassesYou can bypass all actions on an sObject as well as specific Apex or Flow actions for the remainder of the transaction using Apex or Flow. Bypass from ApexTo bypass from Apex, use the static public void updateAccountsNoTrigger(List<Account> accountsToUpdate) {
TriggerBase.bypass('Account');
update accountsToUpdate;
TriggerBase.clearBypass('Account');
} public void insertOpportunitiesNoRules(List<Opportunity> opportunitiesToInsert) {
MetadataTriggerHandler.bypass('TA_Opportunity_StageInsertRules');
insert opportunitiesToInsert;
MetadataTriggerHandler.clearBypass('TA_Opportunity_StageInsertRules');
} public void updateContactsNoFlow(List<Contacts> contactsToUpdate) {
TriggerActionFlow.bypass('Contact_Flow');
update contactsToUpdate;
TriggerActionFlow.clearBypass('Contact_Flow');
} Bypass from FlowTo bypass from Flow, use the Clear Apex and Flow BypassesThe Apex and Flow bypasses will stay active until the transaction is complete or until cleared using the Bypass Execution with PermissionsBoth the Bypass PermissionDevelopers can enter the API name of a permission in the Required PermissionDevelopers can enter the API name of a permission in the Avoid Repeated QueriesIt could be the case that multiple triggered actions on the same sObject require results from a query to implement their logic. In order to avoid making duplicative queries to fetch similar data, use the singleton pattern to fetch and store query results once then use them in multiple individual action classes. public class TA_Opportunity_Queries {
private static TA_Opportunity_Queries instance;
private TA_Opportunity_Queries() {
}
public static TA_Opportunity_Queries getInstance() {
if (TA_Opportunity_Queries.instance == null) {
TA_Opportunity_Queries.instance = new TA_Opportunity_Queries();
}
return TA_Opportunity_Queries.instance;
}
public Map<Id, Account> beforeAccountMap { get; private set; }
public class Service implements TriggerAction.BeforeInsert {
public void beforeInsert(List<Opportunity> newList) {
TA_Opportunity_Queries.getInstance().beforeAccountMap = getAccountMapFromOpportunities(
newList
);
}
private Map<Id, Account> getAccountMapFromOpportunities(
List<Opportunity> newList
) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity myOpp : newList) {
accountIds.add(myOpp.AccountId);
}
return new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
}
}
} Now configure the queries to be the first action to be executed within the given context, and the results will be available for any subsequent triggered action. public class TA_Opportunity_StandardizeName implements TriggerAction.BeforeInsert {
public void beforeInsert(List<Opportunity> newList) {
Map<Id, Account> accountIdToAccount = TA_Opportunity_Queries.getInstance()
.beforeAccountMap;
for (Opportunity myOpp : newList) {
String accountName = accountIdToAccount.get(myOpp.AccountId)?.Name;
myOpp.Name = accountName != null
? accountName + ' | ' + myOpp.Name
: myOpp.Name;
}
}
} Use of Trigger MapsTo avoid having to downcast from public void beforeUpdate(List<Opportunity> newList, List<Opportunity> oldList) {
Map<Id,Opportunity> newMap = new Map<Id,Opportunity>(newList);
Map<Id,Opportunity> oldMap = new Map<Id,Opportunity>(oldList);
...
} This will help the transition process if you are migrating an existing Salesforce application to this new trigger actions framework. DML-Less Trigger TestingPeforming DML operations is extremely computationally intensive and can really slow down the speed of your unit tests. We want to avoid this at all costs. Traditionally, this has not been possible with existing Apex Trigger frameworks, but this Trigger Action approach makes it much easier. Included in this project is a @IsTest
public class TestUtility {
static Integer myNumber = 1;
public static Id getFakeId(Schema.SObjectType sObjectType) {
String result = String.valueOf(myNumber++);
return (Id) (sObjectType.getDescribe().getKeyPrefix() +
'0'.repeat(12 - result.length()) +
result);
}
} We can also use Take a look at how both of these are used in the @IsTest
private static void beforeUpdateTest() {
List<Opportunity> newList = new List<Opportunity>();
List<Opportunity> oldList = new List<Opportunity>();
//generate fake Id
Id myRecordId = TestUtility.getFakeId(Opportunity.SObjectType);
newList.add(
new Opportunity(
Id = myRecordId,
StageName = Constants.OPPORTUNITY_STAGENAME_CLOSED_WON
)
);
oldList.add(
new Opportunity(
Id = myRecordId,
StageName = Constants.OPPORTUNITY_STAGENAME_QUALIFICATION
)
);
new TA_Opportunity_StageChangeRules().beforeUpdate(newList, oldList);
//Use getErrors() SObject method to get errors from addError without performing DML
System.assertEquals(
true,
newList[0].hasErrors(),
'The record should have errors'
);
System.assertEquals(
1,
newList[0].getErrors().size(),
'There should be exactly one error'
);
System.assertEquals(
newList[0].getErrors()[0].getMessage(),
String.format(
TA_Opportunity_StageChangeRules.INVALID_STAGE_CHANGE_ERROR,
new List<String>{
Constants.OPPORTUNITY_STAGENAME_QUALIFICATION,
Constants.OPPORTUNITY_STAGENAME_CLOSED_WON
}
),
'The error should be the one we are expecting'
);
} Notice how we performed zero DML operations yet we were able to cover all of the logic of our class in this particular test. This can help save a lot of computational time and allow for much faster execution of Apex tests. |
2022-08-15
2022-08-17
2023-10-27
2022-09-23
2022-08-18
请发表评论