Salesforce предоставляет широкий набор удобных инструментов для работы с базой данных. Более того, Salesforce, где это возможно, самостоятельно поддерживает консистентность данных, производит оптимизацию запросов к базе, рассчитывает доступы пользователей к тем или иным записям. При этом, система умудряется обрабатывать миллиарды транзакций в день со средним временем отклика порядка 300 милисекунд. Однако, в лучших традициях басен Крылова, даже на этот бал производительности может ворваться большой, неуклюжий медведь, имя которому - плохая модель данных. “Плохая” звучит довольно субъективно, так что давайте рассмотрим подробнее.
В качестве примера представим, что в нашей организации существует 1000000 записей контактов. Но так исторически сложилось, что по каким-то причинам объект Account почти не использовался, и 500000 наших контактов привязаны к одному аккаунту с именем “MyFakeAccount” или вроде того. Пользователи работают с записями контактов и, естественно, изменяют их в процессе. Для каждого обновления записи контакта система блокирует не только изменяемую запись, но и родительскую запись аккаунта.
Это делается для поддержания “правильности” данных, ведь 2 разных процесса не смогут одновременно изменить связанные записи и что-либо поломать. Таким образом, транзакция остаётся атомарной.
Не смотря на то, что каждая запись блокируется на очень короткий промежуток времени, высока вероятность возникновения ошибки в случае, когда один контекст пытается заблокировать запись родителя, которая всё ещё занята предыдущим контекстом (вероятность ошибки пропорциональна количеству параллельных контекстов, выполняющих обновления разных записей контакта).
Также это может вызвать проблемы с расчётом доступов к записям. В приватной шаринг модели (private sharing model) существует неявное предоставление доступа на чтение записи аккаунта для пользователей, имеющих доступ к записям стандартных дочерних объектов аккаунта (Contacts, Cases, Opportunities, и т.д.).
Предположим, что у нашего “MyFakeAccount” есть 500000 контактов и пользователь Василий Пупкин имеет доступ к одному из них.
Новогодний корпоратив прошёл настолько хорошо, что руководитель Василия решил изменить владельца данной записи контакта и вместо Василия указал другого сотрудника.
Перерасчёт доступов (или sharing calculations) в данном случае займёт длительное время, так как системе нужно вычислить, оставлять ли Василию доступ на чтение записи родительского аккаунта(оставлять ли его в таблице Account Sharing).
То есть, нужно проверить, имеет ли Василий доступ хотя бы к одному из оставшихся 499999 дочерних контактов.
Если другой сотрудник попытается создать новый контакт под “MyFakeAccount” аккаунтом пока расчёты не окончены, то чтобы дать ему доступ на чтение родительского аккаунта его контекст вынужден будет ждать, когда запись(AccountShare) будет освобождена.
И если время ожидания превысит 10 секунд, возникнет ошибка “UNABLE_TO_LOCK_ROW”. Сама по себе операция перерасчёта доступов не такая уж и затратная, но когда речь идёт о сотнях тысяч записей, она может занять довольно длительное время.
Описанные выше проблемы вызваны явлением, которое называется Account Data Skew, а skewed Account-ом обычно называют Аккаунт, у которого более 10000 дочерних записей (например контактов).
Для того чтобы избежать появления skewed Account в вашей организации, Salesforce даёт несколько простых рекомендаций:
- Избегать появления более 10000 дочерних записей у одного аккаунта. Даже если представить, что в некой бизнес модели у аккаунта должно быть больше 10000 “детей”,
проще создать несколько аккаунтов, распледелить дочерние записи между ними и добавить немного кода для контроля получившейся системы,
чем встретиться с вышеупомянутыми проблемами, которые всё чаще и чаще будут возникать по мере роста количества записей в вашей базе данных. - Если возможно, использовать Public Read/Write модель шаринга, где доступы к записям не пересчитываются.
- Если в организации уже есть skewed Account, распределить его дочерние записи между другими аккаунтами. Сделать это можно явно сменив родителя на дочерних записях при помощи Batch Apex или Bulk API.