Регистрация  |  Вход

Коварный Salesforce. Борьба с лимитами.

Следить за объемом обрабатываемых данных это одно из главных правил программирования в любом языке, и на любой платформа. Проблема Salesforce в том, что эти объемы данных здесь жестко ограничены. Я уже поднимал эту тему, но недавние проблемы, всплывшие у одного заказчика, опять заставили взглянуть в эту сторону. Приведу здесь наиболее узкие места, которые могут могут выстрелить в любую минуту, если не предпринять вовремя меры по ограничению объема обрабатываемых данных.



:) Мне это напомнило старые добрые времена, когда мы с друзьями на Speccy соревновались кто запихнет какой-нибудь визуальный эффект в 100 байт кода на assembler. Тогда борьба шла за каждый байт. Было весело. Ну а сегодня это уже серьезная тема, денежная, но salesforce заставляет поломать голову над реализацией того или иного функционала.

Давайте немного поэкспериментируем в Developer Console и понаблюдаем за цифрами.

В Developer Console можно  попасть из меню в правом верхнем углу экрана (там где имя вашего пользователя) или из Setup->Create->Classes->Developer Console Button. В самой консоли вверху есть кнопка Execute. Нажимаем ее и в открывшемся окне вводим свой apex code и запускаем на выполнение. (немного об этом я писал здесь). Дальше смотрим лог и считаем цифры.

Вот пример идеального отчета по затраченным лимитам для одной строки кода:

Integer i = 12;

  Number of SOQL queries: 0 out of 100

Number of query rows: 0 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 0 out of 150
Number of DML rows: 0 out of 10000
Number of code statements: 1 out of 200000
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 10
Number of Email Invocations: 0 out of 10
Number of fields describes: 0 out of 100
Number of record type describes: 0 out of 100
Number of child relationships describes: 0 out of 100
Number of picklist describes: 0 out of 100
Number of future calls: 0 out of 10

1. SOQL в for - первое зло. Часто просится засунуть SOQL запрос в for. Обычно это происходит когда надо использовать какую-то информация для работы куска кода в цикле. В таких случаях всю необходимую информацию, которая может понадобиться в цикле нужно вытянуть заранее и оформить в виде MAP. Этот map уже будите дергать в цикле вместо SOQL.

Вот пример неправильного кода:
for (Integer i=0; i++; i<101){

List<Contact> contacts = [SELECT Id FROM Contact LIMIT 1];
}

Вот что пишет по этому поводу log. Всего лишь 101 запрос к базе, а у меня уже Fatal Error.
04:51:13.262 (262903000)|FATAL_ERROR|System.LimitException: Too many SOQL queries: 101


AnonymousBlock: line 2, column 1
04:51:13.774 (263094000)|CUMULATIVE_LIMIT_USAGE
04:51:13.774|LIMIT_USAGE_FOR_NS|(default)|
Number of SOQL queries: 101 out of 100 ******* CLOSE TO LIMIT
Number of query rows: 100 out of 50000
Number of SOSL queries: 0 out of 20
Number of DML statements: 0 out of 150
Number of DML rows: 0 out of 10000
Number of code statements: 102 out of 200000
Maximum heap size: 0 out of 6000000
Number of callouts: 0 out of 10
Number of Email Invocations: 0 out of 10
Number of fields describes: 0 out of 100
Number of record type describes: 0 out of 100
Number of child relationships describes: 0 out of 100
Number of picklist describes: 0 out of 100
Number of future calls: 0 out of 10

2. For в for - зло поменьше, но тоже потенциальное зло. Никогда не занимайтесь переборами во вложенных циклах с целью поиска подходящих данных. Это очень хорошо кушает Code statements, которых у нас всего 200 000. Опять же если внутри цикла необходимо подмножество из какого-то набора информации, то лучше разложить эти подмножества заранее. Вот небольшой теоретический пример: посчитаем сколько контактов принадлежит каждому аккаунту (на входе 100 аккаунтов и 100 контактов). Я подготовил замечательный скрипт, который позволяет увидеть и понять разницу в подходах, хотя на выходе результат один и тот же.
List<Account> Accounts = new List<Account>();

List<Contact> Contacts = new List<Contact>();

for (Integer i=0; i<100; i++){
Accounts.add(new Account(Name='name'+i));
Contacts.add(new Contact(FirstName='name'+i, AccountID = null));
}

Integer CodeStatementsStart = Limits.getScriptStatements();

for (Account acc : Accounts) {
Integer i = 0;
for (Contact con : Contacts) {
//system.debug('x - '+Limits.getScriptStatements());
if (con.AccountID == acc.Id) {
i++;
}
}
system.debug('account ('+acc.Id+') has '+i+' contacts');
}

Integer CodeStatementsTotal = Limits.getScriptStatements() - CodeStatementsStart;
SYSTEM.DEBUG('CodeStatementsTotal - '+CodeStatementsTotal);

CodeStatementsStart = Limits.getScriptStatements();

Map<Id, Integer> AccountId_TotalContacts = new Map<Id, Integer>();
for (Contact con : Contacts){
AccountId_TotalContacts.put(con.AccountID, AccountId_TotalContacts.get(con.AccountID) == null ? 0 : AccountId_TotalContacts.get(con.AccountID)+1);
}

for (Id accId : AccountId_TotalContacts.keySet()) {
system.debug('account ('+accId+') has '+AccountId_TotalContacts.get(accId)+' contacts');
}

CodeStatementsTotal = Limits.getScriptStatements() - CodeStatementsStart;
SYSTEM.DEBUG('CodeStatementsTotal2- '+CodeStatementsTotal);

Первый цикл (со вложенным for) сожрал аж 10201 code statement из 200 000. Не мало так. А если еще пару таких циклов сделать дальше или увеличить количество итераций?

Второй подход, состоящий из двух циклов потратил на подготовку данных 103 code statement. Разница заметна :).

Замечу что мой пример далек от жизни. В реальности вложенные циклы будут содержать логику посложнее, что в разы увеличит расходы по code statements. Здесь главное понять эти два момента, и учитывать их при проектировании алгоритма.Ведь если на других платформах вы поплатитесь разве что временем выполнения скрипта, то на salesforce получите Exception и недовольного заказчика.