Future Methods

ליאור נכתב על ידי ליאור לביא, עודכן בתאריך 10/05/2022

בוא נתחיל עם ההגדרה היבשה: Future methods הן מתודות שרצות ב-Thread נפרד משלהן בזמן לא מוגדר בעתיד ונהנות מ-Gevernor Limits גבוהים יותר.

עכשיו כדי להבין מה הן Future methods, בוא נפרק את המשפט הקודם ונבין מה בדיוק הוא אומר.

  1. "Future methods הן מתודות שרצות ב-Thread נפרד משלהן"

    במילים אחרות, Future method רצה בטרנזקציה נפרדת, ב-Execution context נפרד. העובדה הזאת מאפשרת לנו להשתמש ב-Future method כדי להימנע משגיאת Mixed DML Operation. מידע נוסף על הודעת השגיאה Mixed DML Operation תוכל לקרוא פה באתר בפוסט Mixed DML Operation.

  2. "בזמן לא מוגדר בעתיד"

    Future method לא רצה מיידית לאחר שהיא נקראת, אלא מקבלת מקום בתור בשרת. כאשר תורה מגיע, בהתאם לזמינות המשאבים בשרת, היא תורץ. התנהגות זאת מכונה "הרצה א-סינכרונית". מידע נוסף על הרצת קוד אסינכרונית תוכל לקרוא פה באתר בפוסט קוד אסינכרוני.

  3. "ונהנות מ-Governor Limits גבוהים יותר."

    כל פעילות כנגד שרתי Salesforce נאכפת על ידי שורה של מגבלות שתפקידן להבטיח שמשאבי השרתים יהיו זמינים לכל המשתמשים כל הזמן, כך שלא יווצר מצב שלקוח אחד משתלט על אחוז ניכר ממשאבי המערכת בצורה לא הוגנת. עם זאת, Salesforce כן מקצה כמות משאבים גדולה יותר לשימוש ב-Future methods, כך שאם נתקלנו בקוד שצורך כמות גבוה של משאבים, אם התרחיש העסקי מאפשר זאת, נוכל להעביר את הלוגיקה לרוץ בצורה א-סינכרונית באמצעות Future method וכך נאפשר לפלטפורמה לספק לנו משאבים רבים יותר.

מתי נשתמש ב-Future Method

המקרים הנפוצים לשימוש ב-Future methods הם:

  1. קריאה לשירות חיצוני - קריאות לשירותי חיצוניים, Callouts, כמו Web Service APIs חייבות להיות מבוצעות באמצעות Future method או Queueable Apex כשהן מורצות מ-Trigger או אחרי ביצוע פעולת DML. הסיבה לכך היא שאם ננסה לבצע פנייה לשירות חיצוני מתוך Trigger, החיבור שלנו ל-Database יצטרך להישאר פתוח לאורך כל זמן ההמתנה למענה מהשירות חיצוני, וזה אסור בתכלית בסביבות מרובות משתמשים (Multi-Tenant) כמו Salesforce.
  2. הרצה של תהליכים זוללי משאבים - אם התהליך העסקי מאפשר זאת, לעיתים נרצה להשתמש ב-Future methods כדי להריץ תהליכים שאורכים זמן רב או מצריכים שימוש במספר גדול של שאילתות או פעולות כנגד ה-Database משום שהן זוכות ל-Governor limits גבוהות יותר מהפלטפורמה.
  3. הרצת מתודה בטרנסאקציה נפרדת כדי להימנע משגיאת Mixed DML Operation - לעיתים נרצה להפעיל לוגיקה שמצריכה ביצוע פקודות DML על Setup ו-Non-Setup objects, למשל User ו-Account, אלא שהרצה של פעולות רשומות אלו באותו Execution context אסורה ב-Salesforce. העובדה ש-Future methods רצות ב-Thread משלהן בעתיד מאפשרת לנו לפצל את הלוגיקה לשתיים: מתודה סינכרונית בה מעודכן סוג אחד של אובייקטים, למשל Setup כמו User, ומתודה שנייה, אסינכרונית, בה מעודכנות רק רשומות Non-Setup כמו Account למשל.

איך כותבים Future Method

התחביר של Future methods פשוט למדי ומסתמך על סימון פונקציה באמצעות התגית future@ באופן הבא:

public class myCoolClass{
	@future
	public static void myAwesomeFutureMethod(List<Id> accountsIDsList){
		List<Account> myAccounts = [SELECT Name FROM Account WHERE ID IN :accountsIDsList];
		for(Account singleAccount : myAccounts){
			//Run some awesome code here... 
		}
	} 
}

דוגמה לביצוע Callout באמצעות Future Method

בדוגמה הבאה נבצע פנייה למתודה Send במחלקה לדוגמה בשם JIRAWebService שתבצע קריאה ל-Web Service ליצירת Issue במערכת JIRA.

public class JIRAConnector{
	@future(callout = true)
	public static void createIssueInJIRA(Id myRecordID){
		
		//Invoking the send method in the JIRAWebService class will result in a callout
		String response = JIRAWebService.send();
		
		//Get the record that fired the callout and update it with the key of the issue that was created in JIRA for it.
		MyCustomObject__c myCustomObjectRecored = [SELECT Id, Key__c FROM MyCustomObject__c WHERE Id = :myRecoredID LIMIT 1];
		myCustomObjectRecored.Key__c = response.get('key');
		Update myCustomObjectRecored;
		System.debug(response);
	} 
}

דוגמה לכתיבת Test Method לבדיקת Future Method

כתיבת Unit Test עבור Future method ב-Salesforce מתבססת על שני עקרונות:

  1. שימוש ב-Mock response class - מתודות בדיקה ב-Salesforce אינן מבצעות קריאות בפועל לשירותים חיצוניים. במקום זאת, Salesforce מאפשרת לנו להגדיר מחלקות מיוחדות המדמות שירות חיצוני ומחזירות תשובה, Response, בפורמט המתאים.
  2. שימוש במתודות test.startTest() ו-test.stopTest() כדי לאסוף ולהריץ בצורה סינכרונית את כל הקריאות ה-אסינכרוניות שלנו.

כתיבת Mock Response Class

בדוגמה הבאה נכתוב מחלקה שתחזיר עבורנו שני סוגי Response, אחד עבור הצלחה ואחד עבור כישלון. הדבר יקל עלינו לכתוב Test Methods שיבדוק את התנהגות הקוד שלנו בשני המקרים. אנחנו יכולים לעשות זאת על ידי העברת פרמטר ל-Constructor של המחלקה.

@IsTest
public class JIRAMockResponse implements HttpCalloutMock{
	
	//Best Practice: Always make your properties private so access will only be made through the use of public methods.
	private Boolean success;

	//Note: Constructors in APEX are public, named after the class name, and don't have return type.
	public JIRAMockResponse(Boolean success){
	
		//Use the 'this' tag to initialize the success property of our class's instances from the parameter we passed to the constructor. 
		this.success = succes;
	}

	public HTTPResponse respond(HttpRequest req){
		HttpResponse res = new HttpResponse();
		if(this.success){
			res.setHeader('Content-Type', 'application/json');
			res.setBody('{"id":"001", "key":"PRJ-001"}');
			res.setStatusCode(200);
		}
		else{
			res.setHeader('Content-Type', 'application/json');
			res.setBody('{"errorMessages":[], "errors":{"customfield_001":"This is an error message."}}');
			res.setStatusCode(200);
		}
	
		return res;
	}
}

דוגמה לכתיבת Unit Test עבור המתודה createIssueInJIRA

@IsTest
Private class JIRAConnector_Tests{
	
	@IsTest
	Private static void createIssueInJIRA_Success(){
		//Note: We are passing the value 'true' to our mock class constructor.
		Test.setMock(HttpCalloutMock.class, new JIRAMockResponse(true));
		
		Test.startTest();
		
		JIRAConnector.createIssueInJIRA('abc123456789');
	
		//Note: This command will force the execution of our async future methods right now, and not in the future.
		Test.stopTest();

		//Let's get our initial record and see if the mock response key was saved to it.
		MyCustomObject__c myCustomObjectRecored = [SELECT Id, Key__c FROM MyCustomObject__c WHERE Id = :myRecoredID LIMIT 1];
		System.assertEquals('PRJ-001', myCustomObjectRecored.key);
	}
}

דגשים חשובים לכתיבת Future Methods

על מנת לכתוב Future methods חשוב לציית למספר כללים:

  • Future methods תמיד יכתבו כ-static.
  • Future methods תמיד יחזירו void.
  • Future methods מקבלות רק שלושה סוגי ארגומנטים:
    1. Data types פשוטים (String, Integer, Id לדוגמה).
    2. אוספים של Data Types פשוטים (Lists, Maps ו-Sets).
    3. מערכים של Data Types פשוטים (Arrays).

      פרקטיקה מקובלת בשימוש ב-future methods היא להעביר ל-future method רשימה של Ids של רשומות לעריכה, לשמור את הרשומות עצמן למשתנה באמצעות שאילתת SOQL, לבצע בהן שינויים ולשמור את השינויים ל-Database באמצעות פקודת DML מתאימה.

      בנקודה זאת אתה עשוי לשאול את עצמך למה future methods לא פשוט מקבלות אוסף רשומות ומבצעות עליו את הלוגיקה הנדרשת. התשובה היא שבגלל ש-future methods רצות בזמן עתידי, אם נעביר להן עותק של הרשומות, עד הזמן שבו הן ירוצו בפועל הנתונים בקריאות עלולים להשתנות והמתודות ירוצו עם נתונים לא עדכניים, מה שעלול לגרום למחיקת השינויים והודעות שגיאה.

  • Future methods לא ירוצו בעתיד לפי הסדר בו הן נקראו. אם נקרא לשתי future methods על אותה הרשומה לדוגמה, לא נוכל לדעת מי מהן תרוץ קודם, מה שעלול לגרום לנעילת רשומה לעריכה בזמן ריצה והופעת הודעות שגיאה.
  • לא ניתן להריץ Future method מתוך Future method, מה שאומר שחשוב לשים לב לא ליצור מצב בו עדכון רשומה מ-Future method גורר קריאה ל-Future method נוספת.
  • קיימת מגבלת פלטפורמה של 50 Future methods בטרנזקציה, ובנוסף קיימת מגבלה יומית על הרצת קוד א-סינכרוני מכול הסוגים (Batch, future, queueable ו-Scheduled) ל-250,000 או מספר רשיונות המשתמשים בארגון שלך כפול 200, הגדול מביניהם.