Некоторые решения, найденные при разработке инструмента для импорта и обновления данных из справочника БИК РФ в систему Microsoft Dynamics CRM.

В ходе внедрения решений на базе Microsoft Dynamics CRM перед специалистами часто встает задача импорта различных данных в CRM-систему. В большинстве случаев для миграции данных применяют инструмент Microsoft CRM Data Migration Framework, позволяющий импортировать информацию в CRM из различных источников. Однако существует и другая задача: пользователям нужно оперативно обновлять большие объемы информации из внешних источников в интерактивном режиме. Для этого потребуется разработать дополнительный компонент, что возможно благодаря развитым средствам Microsoft CRM 3.0 Software Development Kit (SDK), предназначенным для расширения функционала стандартной поставки продукта. В ходе реализации одного из таких компонентов было найдено несколько интересных решений, о которых мы и расскажем в данной статье.

Коротко о задаче: ЦБ РФ предоставляет банкам справочники, в том числе «Справочник банковских идентификационных кодов участников расчетов на территории Российской Федерации» («Справочник БИК РФ») в файле формата DBF с известной структурой. Обновления его происходят с периодичностью в 1–3 месяца. При реализации CRM-проекта в банке требуется импортировать данные из этого справочника в Microsoft Dynamics CRM (где определена специальная сущность new_bik с полями, соответствующими структуре таблицы DBF). Для этого необходим компонент с автоматическим импортом кодов из внешнего файла в базу данных Microsoft Dynamics CRM, который позволит создать и поддерживать в актуальном состоянии справочник БИК РФ. Записи справочника применяются при заполнении соответствующих реквизитов организаций и контактов в формах CRM; таким образом, можно не вводить их вручную, что снижает вероятность ошибок при заполнении.

Импорт данных

Импорт должен проводиться по следующему алгоритму:

  • добавление новых записей БИК из файла-справочника в CRM-систему;
  • обновление уже существующих записей в CRM-системе;
  • деактивация тех записей в CRM-системе, которые отсутствуют в справочнике ЦБ.

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

Самое первое решение было таким. Первый цикл: получаем набор активных записей из CRM и перебираем его. Если существующая запись найдена в файле-справочнике, то обновляем ее, если не найдена — деактивируем. Второй цикл: получаем набор записей из файла-справочника и перебираем его. Если запись справочника не найдена в CRM — добавляем ее.

Здесь следует учесть, что есть два способа проверить, существует ли в CRM определенная запись. Первый способ («одно обращение к CRM»): сначала загрузить в память все активные записи и перебирать их (перебор нужен для поиска нужной записи, так как в классе BusinessEntityCollection, представляющего набор данных в CRM, не определен метод для быстрого поиска, но можно получить коллекцию в DataSet). Второй способ («1000 и 1 запрос к CRM»): fetch-запросом опрашивать CRM на предмет наличия конкретной записи.

Исходя из собственного опыта автора, оба способа нельзя считать оптимальными с точки зрения производительности: в первом случае при поиске записей для деактивации («второй цикл») необходимо 4000 раз (это примерное количество кодов в справочнике) повторно перебирать 4000 записей, во втором — делать 4000 лишних запросов к CRM через Web-сервис (в дополнение к тем запросам, которые будут непосредственно обновлять данные). Однако было найдено более красивое решение с использованием метода Merge класса DataSet, позволяющее делать всего один проход по всем записям.

Метод Merge служит для слияния двух объектов DataSet, которые имеют схожую (но не обязательно одинаковую) схему. В большинстве случаев такое слияние используется в клиентских приложениях, чтобы определить, какие изменения произошли с данными. Это позволяет отслеживать только модифицированные записи [1].

В итоге импорт справочника БИК выглядит следующим образом.

1. Получение существующих записей из Microsoft Dynamics CRM. В целях оптимизации процесса получим набор активных записей БИК напрямую из базы данных Microsoft Dynamics CRM, обращаясь к Filtered View. Данная методика, полностью поддерживаемая системой, наиболее уместна в этом случае и обеспечивает максимальную производительность [2, 3].

private DataSet GetBikCodes()
{
DataSet bikCodes = null;
SqlConnection conn = new SqlConnection
(«Data Source=crmserver; Initial
Catalog=SOFTLINE_MSCRM; Integrated
Security=SSPI»);
try
{
// Open MSCRM Database using integrated
// authentication (required by Filtered Views)
conn.Open();
string sqlCommandString =
@»SELECT
new_bikid, new_vkey AS VKEY, ...
FROM
FilteredNew_bik
WHERE
(statecode = 0)»; // get only active records
SqlDataAdapter adapter = new SqlDataAdapter
(sqlCommandString, conn);
bikCodes = new DataSet();
adapter.Fill(bikCodes, «bikCodes»);
// Setting up the Primary Key
bikCodes.Tables[«bikCodes»].PrimaryKey =
new DataColumn[] { bikCodes.Tables
[«bikCodes»].Columns[«NEWNUM»] };
bikCodes.AcceptChanges();
}
catch (Exception ex)
{
throw new Exception(«Не удалось получить
данные БИК из CRM. GetBikCodes()», ex);
}
finally
{
conn.Close();
}
return bikCodes;
}

2. Чтение записей файла-справочника. Извлечение данных из файла-таблицы DBF тоже не составляет особого труда.

public DataSet GetBikCodesFromDbf()
{
DataSet bikDataSet = null;
try
{
string connString = «Provider=Microsoft.
Jet.OLEDB.4.0;Data Source=c:\BIK;
Extended Properties=dBASE IV;
User ID=;Password=;»;

string sqlCommand = «SELECT VKEY, PZN, ...
FROM filename»;
OleDbConnection conn = new OleDbConnection();
conn.ConnectionString = connString;
OleDbCommand cmd = new OleDbCommand();
cmd.CommandText = sqlCommand;
cmd.Connection = conn;
OleDbDataAdapter da = new OleDbDataAdapter();
da.SelectCommand = cmd;
bikDataSet = new DataSet();
da.Fill(bikDataSet, «bikCodes»);
// Setting up the Primary Key
bikDataSet.Tables[«bikCodes»].PrimaryKey =
new DataColumn[] { bikDataSet.Tables
[«bikCodes»].Columns[«NEWNUM»] };
foreach (DataRow row in bikDataSet.Tables
[«bikCodes»].Rows)
{
row.SetAdded();
}
}
catch (Exception ex)
{
throw new Exception(«Не удалось прочитать
данные из файла.», ex);
}
finally
{
conn.Close();
}
return bikDataSet;
}

Следует обратить внимание на то, что в качестве значения параметра Data Source строки подключения указывается не сам файл DBF, а имя директории, в которой он находится. В выражении же SQL таблица имеет то же имя, что и файл, но без расширения (filename [.dbf]). Немаловажен также фрагмент кода, выставляющий всем только что загруженным в DataSet записям статус DataRowState.Added. Это необходимо для корректного добавления и обновления записей при слиянии.

3. Слияние двух наборов данных. Слияние — это тоже несложная операция. Здесь в качестве базового набора данных выступают записи, полученные из CRM, которые обновляются данными из справочника в формате dbf.

private Counters ProcessFile()
{
// Get codes from DBF
DbfProcessor dbfProc = new DbfProcessor();
DataSet dbfBikCodes = dbfProc.
GetBikCodesFromDbf();
// Get codes from CRM
DataSet crmBikCodes = this.GetBikCodes();
// Merge datasets with codes
crmBikCodes.Merge(dbfBikCodes);
// Update codes in CRM according changes
return this.UpdateBikCodes(crmBikCodes);
}

Обновление, создание новых записей и деактивация

Теперь просто перебираем записи получившегося в процессе слияния набора, проверяем состояние каждой из строк и вызываем соответствующий метод: создание, обновление или деактивацию. Это предоставляется возможным, так как метод Merge при слиянии обновляет статус каждой строки исходного DataSet (с данными из CRM) в соответствии с данными DataSet, переданного в качестве источника обновлений. Строки, которые содержатся в обоих наборах, обновляются, и в исходный набор добавляются новые записи из справочника. Те записи, которые не были затронуты в процессе слияния, считаются устаревшими и подлежат деактивации.

private Counters UpdateBikCodes(DataSet 
crmBikCodes)
{
Bik bik = new Bik();
DataTable bikTable = crmBikCodes.Tables
[«bikCodes»];
Counters cntr = new Counters();
int createCount = 0;
int updateCount = 0;
int deleteCount = 0;
int passedCount = 0;
foreach (DataRow row in bikTable.Rows)
{
if (DataRowState.Added == row.RowState)
{
bik.ReadFromRow(row);
bik.Create();
createCount++;
}
else if (DataRowState.Modified ==
row.RowState)
{
bik.ReadFromRow(row);
bik.Update();
updateCount++;
}
else if (DataRowState.Unchanged ==
row.RowState) // in our case ==
deactivate
{
bik.ReadFromRow(row);
bik.Deactivate();
deleteCount++;
}
else
{
passedCount++;
}
}
cntr.TotalCount = bikTable.Rows.Count;
cntr.CreateCount = createCount;
cntr.UpdateCount = updateCount;
cntr.DeleteCount = deleteCount;
cntr.PassedCount = passedCount;
return cntr;
}

Таким образом, происходит один-единственный перебор записей с единственным запросом к CRM на добавление, обновление или деактивацию необходимых данных.

Тестирование показало, что на сервере CRM, развернутом как виртуальная машина (процессор 4x833 МГц, 1024 Мбайт памяти RAM; Microsoft Windows Server 2003 Enterprise Edition), для импорта 4500 записей требуется 15–25 мин, что при условии довольно редкого обновления справочника вполне приемлемо.

Оптимизация

А теперь самое главное — оптимизация. Достигнутый результат представлялся не слишком хорошим, но после небольшой доработки удалось почти в 10 раз ускорить процесс импорта.

Описанный выше компонент проводит обновление справочника БИК в CRM из класса Bik, в котором определены методы Create(), Update() и Deactivate(), вызывающие в свою очередь соответственно методы Create() , Update() и Execute() Web-сервиса Microsoft CRM 3.0 (класса CrmService). Для удобства экземпляр этого класса вынесен в private-свойство класса Bik (рис. 1).

Вот код свойства Service класса Bik:

private CrmService _service = null;
private CrmService Service
{
get
{
if (null == _service)
{
_service = new CrmService();
_service.Url = Configurator.
CrmServiceLocation;
_service.Credentials = System.Net.
CredentialCache.DefaultCredentials;
}
return _service;
}
}

А теперь рассмотрим внесенные изменения:

private CrmService _service = null;
private CrmService Service
{
get
{
if (null == _service)
{
_service = new CrmService();
_service.Url = Configurator.
CrmServiceLocation;
_service.Credentials = System.Net.
CredentialCache.DefaultCredentials;
_service.UnsafeAuthenticatedConnectionSharing
= true;
_service.ConnectionGroupName = «default»;
}
return _service;
}
}

Свойство UnsafeAuthenticatedConnectionSharing класса HttpWebRequest позволяет держать открытым соединение, подлинность которого установлена. Значение по умолчанию — false, что вызывает закрытие прошедшего аутентификацию соединения с сервером после каждого запроса. Таким образом, приложение должно проходить через процесс аутентификации при каждом новом запросе.

Выставление этого свойства в true позволяет после первой же проверки подлинности держать соединение открытым для всех последующих запросов, которые используют соединение без повторной аутентификации. Другими словами, после подтверждения подлинности пользователя A пользователь B может работать через уже проверенное соединение пользователя A (без процесса аутентификации), но запрос B будет выполняться с учетными данными пользователя A. Это позволяет сэкономить время на всех повторных проверках подлинности запросов.

Предостережение: поскольку пользователи получают возможность работать с проверенным (authenticated) соединением без повторного контроля, следует убедиться, что это не станет в дальнейшем уязвимым местом разрабатываемой системы. Если приложение использует аутентификацию для каждого конкретного пользователя (а именно это характерно для 90% приложений для Microsoft Dynamics CRM), не нужно включать это свойство — иначе все операции, проводимые в CRM, будут выполняться от одного и того же ответственного лица.

Если же в целях повышения производительности приложения предполагается разрешить использование проверенного соединения, то существует вариант включить такую возможность для групп пользователей. Для этого помимо выставления в true свойства UnsafeAuthenticatedConnectionSharing необходимо указать свойство ConnectionGroupName все того же класса HttpWebRequest (которому наследует CrmService). При помощи последнего организуются именованные группы проверенных соединений. Это исключает использование соединения пользователем, не прошедшим аутентификацию. Иначе говоря, пользователь A имеет уникальное имя для своей группы подключения, отличное от группы пользователя B, что предоставляет определенный слой изоляции для выполнения запросов каждого из пользователей.

Таким образом, добавление всего двух строк в уже существующий компонент позволяет импортировать 4500 записей за 2,5–3 мин, что составляет почти десятикратный прирост производительности.

Источники дополнительной информации

  1. Подробное описание метода Merge класса DataSet в MSDN (http://msdn2.microsoft. com/en-us/library/system.data.dataset.merge.aspx).
  2. Microsoft Dynamics CRM 3.0 — Reporting and Filtered Views (http://msdn2.microsoft. com/en-us/library/aa681626.aspx).
  3. Microsoft Dynamics CRM Team Blog — Writing CRM callouts with filtered views (http:// blogs.msdn.com/crm/archive/2007/05/21/writing-crm-callouts-with-filtered-views.aspx).
  4. HttpWebRequest.UnsafeAuthenticatedConnectionSharing Property (http://msdn2. microsoft.com/en-us/library/system.net.httpwebrequest.unsafeauthenticatedconnectionsharing.aspx).
  5. Microsoft Dynamics CRM 3.0: Bulk Import — Performance Best Practices (http://msdn2. microsoft.com/en-us/library/bb291036.aspx#mbs_crmbulkim_topic4).
  6. CrmService Methods (http://msdn2.microsoft.com/en-us/library/aa680899.aspx).