Всем привет!

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

Сегодня хочу поговорить о средствах связи контрагента.
Итак, рассмотрим коробочную версию данного функционала (хотя как правило, оно достается уже с высокой степенью кастомизации) для Terrasoft XRM 3.2 например (более низкие версии тоже подойдут).
Имеем: средства связи контрагента у нас хранятся в 2-х местах:
1) В таблице контрагента tbl_Account (поля Communication1, Communication2,...,Communication5 и Communication1TypeID, Communication2TypeID,...Communication5TypeID)
2) В Таблице средств связи контрагента tbl_AccountCommunication

Плюсов такого подхода, если честно, не вижу (кроме может быть случая, когда мы данные импортируем, например, из файла Excel и в "грязном" виде заливаем в таблицу tbl_Account, что так же считаю не правильным: их можно залить в какую-то таблицу, специально созданную для импорта, а потом после обработки уже переносить в целевую таблицу).

Минусы ниже:
1) Главный: нам нужно поддерживать целостность данных в 2-х местах: операция Insert/Update/Delete в средствах связи ведет к необходимости выполнить операцию Update в контрагенте и наоборот.
2) Очень часто изменения в таблицах логируются (например, в обеих таблицах). Изменяем 1 телефон в таблице контрагента, вопрос: Сколько записей будет вставлено в таблицы логов? Зависит от реализации механизма поддержки целостности: от 2-х до 6.
3) Бизнес-пользователям захотелось видеть на форме компаний 6 средств связи... или даже 10. Придется добавлять новые поля в таблицу, изменять механизм поддержки целостности, апдейтить таблицу контрагентов (не забыв/забыв о том, что изменения логируются)
4) Часто CRM интегрирована с другими существующими системами. Сколько пакетов интеграции "полетит" при изменении 1 телефона в таблице контрагентов? Аналогично: от 2 до 6.
Возможно, забыл что-то еще. Выяснить все детали того, что происходит можно профайлером на тестовой среде.

Как избавиться от данных проблем: провести нормализацию. Не буду вдаваться в подробности 1,2,3 нормальных форм, нормализации/денормализации, но, очевидно, что средства связи должны храниться в таблице средств связи :smile: (Вопрос о целесообразности tbl_AccountCommunication и tbl_ContactCommunication оставим в стороне).

Что тогда с выводом информации об N средствах связи на форме компании? Есть 2 опции:
1) Заменить вывод информации в соответствующие контролы из таблицы tbl_Account на tbl_AccountCommunication
2) Вывести на форме компании весь грид средств связи и убрать его из деталей.

Я рассмотрю 1 подход.
Нам потребуется внести незначительные изменения в 2 сервиса: wnd_AccountEdit и scr_AccountEdit.

1) Добавляем 5 невизуальных компонентов DatasetLink на форму Контрагента (dlCommunication1, dlCommunication2, ... dlCommunication5) и для каждого из них выбираем датасет ds_AccountCommunication.
Невизуальные компоненты

2) На самой форме меняем в соответствующих контролах DatasetLink на те, что мы создали в первом шаге и выбираем соответствующие поля DatafieldName (тип средства связи CommunicationTypeID и номер Number).
Изменения в контролах
Сохраняем изменения и переходим к обработчику.

3) Теперь нам нужно подтянуть данные для новых контролов при открытии формы.
Напишем функцию, которая в качестве параметра будет принимать порядковый номер средства связи (Position) и будет заполнять данными наши новые датасеты.
Если какого-то средства связи нет, то нужно добавить запись в датасет, обозначив только связь с контрагентом и номер позиции.

//Функция подготовки данных для отображения средств связи из таблицы tbl_AccountCommunication
function InitCommunication(position){  
        eval('var CommunicationDataset = dlCommunication'+ position + '.Dataset');//Определяем целевой датасет
        ApplyDatasetFilter(CommunicationDataset, 'AccountID', dlData.Dataset('ID'), true);//Фильтр по контрагенту
        ApplyDatasetFilter(CommunicationDataset, 'Position', position, true);//Фильтр по номеру позиции   
        RefreshDataset(CommunicationDataset);
        if(CommunicationDataset.IsEmpty){//Если средства связи не существует, нужно его добавить и связать с контрагентом и присвоить номер позиции
                CommunicationDataset.Append();
                CommunicationDataset('Position') = position;
                CommunicationDataset('AccountID') = dlData.Dataset('ID');
        }                      
}

В обработчик события OnPrepare добавляем вызов функции.

function wnd_AccountEditOnPrepare(Window) {
        Initialize();
        wnd_BaseDBEditOnPrepare(Window);
/* PRODUCT XRM */
        UpdateAccountBasicCurrencyCaptions();
        InitializePostponementPayment(Self);
/* ENDPRODUCT XRM */
        InitAccountInfo();
//Заполняем датасеты средств связи
        for(var i = 1; i 6; i++)
           InitCommunication(i);
}

На этом этапе можно сохранить изменения и посмотреть что получилось в клиенте. Данные действительно будут подтягиваться из средств связи. Но при изменении средств связи на форме контрагента изменения не будут сохранены в базу данных.

4) Займемся сохранением данных в tbl_AccountCommunication при сохранении формы контрагента. Нужно добавить обработчик события OnDatasetAfterPost к невизуальному компоненту основного датасета dlData.
Это самый простой вариант: если запись контрагента новая, то при срабатывании этого события она гарантированно сохранена в базе данных, и у нас не получится так, что средства связи окажутся привязаны к несуществующей компании (или если у нас есть внешний ключ из tbl_AccountCommunication на tbl_Account мы получим ошибку от SQL сервера).
Если нам вдруг требуется валидация средств связи перед их сохранением (например у нас нет обработки правильности ввода информации "на лету" еще при заполнении данных на форме), то можем соответствующий функционал добавить в обработчик OnDatasetBeforePost к невизуальному компоненту основного датасета, с возможностью отменить сохранение основной записи контрагента.

function dlDataOnDatasetAfterPost(Dataset) {
        for(var i = 1; i 6; i++)
           CommitCommunication(i);
}

function CommitCommunication(position){
        eval('var CommunicationDataset = dlCommunication'+ position + '.Dataset');
        if(GetFieldsValuesAreChanged(CommunicationDataset, 'Number', 'CommunicationTypeID'))   
                CommunicationDataset.Post();
}

Основная идея здесь - нам нужно сохранять записи только в том случае, если они действительно менялись, причем менялись или тип или номер. Если например мы добавили пустые датасеты (при подготовке данных для формы) и ничего в эти поля не ввели, то у нас не будет никакого "мусора" из пустых записей в таблице tbl_AccountCommunication.

Можно сохранять изменения и проверять работу.

Замечания.
Поскольку это "учебный" пример, для его доработки до уровня продакшена нужно будет самостоятельно посмотреть следующие моменты:
1) Стоит сохранить число средств связи на форме в глобальной переменной AccountEdit в одном месте (чтобы не приходилось искать каждый раз где нужно изменить его если число контролов на форме потом увеличится/уменьшится), и соответственно исправить код вызова функций.
2) Нужно будет найти и отключить функционал поддержки целостности при изменении записи в детали Средства связи (чтобы запись контрагента не обновлялась) и, на всякий случай, такой же функционал у контрагента.
3) Возможно у кого-то возникнет вопрос, что если раньше при открытии формы контрагента шел 1 запрос на выборку из tbl_Account а теперь 1 + 5 запросов из tbl_AccountCommunication и в чем тогда преимущество? Преимущество в том, что мы избавляемся от 4-х серьезных минусов, которые я описал в самом начале, но да, получаем увеличение числа запросов на выборку данных при открытии формы контрагента, но эти запросы очень "легкие" и вряд ли скажутся на производительности (+ мы всегда можем ее увеличить "допиливанием" индексов).
4) Потребуется доработка функционала для отображения телефонов в гриде контрагентов.

Вроде все. Надеюсь, получилось не слишком сложно о простом :smile:
Будут какие-то мысли, комментарии, вопросы, предложения - welcome!

Нравится

Поделиться

2 комментария

"komgbu" написал:3) Возможно у кого-то возникнет вопрос, что если раньше при открытии формы контрагента шел 1 запрос на выборку из tbl_Account а теперь 1 + 5 запросов из tbl_AccountCommunication и в чем тогда преимущество?

А если вытягивать эти все 4 средства связи всё в том же sq_Account, используя LEFT JOIN или в колонках подзапросов?
Хранить в обычных текстовых и лукапных полях без привязки к БД, на AfterPost делать проверку на изменение значения, а затем INSERT или UPDATE в таблицу средств связи контрагента программно.
Будет ли при этом некоторый выигрыш в производительности?

"Зверев Александр" написал:А если вытягивать эти все 4 средства связи всё в том же sq_Account, используя LEFT JOIN или в колонках подзапросов?

Спасибо, Александр, я совсем забыл про этот вариант...
Это хороший вариант - причем у него есть плюсы по сравнению с тем, который я привел в своем примере:
- не нужно трогать стандартные обработчики событий и форму контрагента
"Зверев Александр" написал:Хранить в обычных текстовых и лукапных полях без привязки к БД, на AfterPost делать проверку на изменение значения, а затем INSERT или UPDATE в таблицу средств связи контрагента программно.

Вот этого делать не придется - контролы на форме остаются абсолютно те же самые, никаких дополнительных изменений не нужно. При сохранении датасета ядро (или не ядро - не важно :smile:) само определяет что поля не относятся к таблице tbl_Account и все сохраняется корректно.
- не нужно беспокоиться об отображении средств связи в гриде контрагентов

Я попробовал сделать через соединение:
1) Добавляем 5 раз таблицы tbl_AccountCommunication (с алиасами tbl_AccountCommunication1, tbl_AccountCommunication2,...) связываем их с tbl_Account по AccountID используя LEFT JOIN
Соединение таблиц
2) Удаляем существующие 5 соединений с tbl_CommunicationType и создаем новые с аналогичными именами, но связанные с соответствующими таблицами средств связи
Соединение таблиц
3) Создаем 5 параметров целого типа с цифрами 1...5 (Int1, Int2,...Int5)
Параметры
4) Добавляем условия в WHERE (причем все условия оставляем включенными) в них пишем что tbl_AccountCommunication1.Position = Int1
tbl_AccountCommunication2.Position = Int2
...
Условия
5) В полях запроса Communication1, Communication1TypeID ...Communication5, Communication5TypeID меняем таблицы с tbl_Account на tbl_AccountCommunication1,... tbl_AccountCommunication5 и поля соответственно на Number
Поля
6) Проверяем что все хорошо с полями CommunicationType1Name,...CommunicationType5Name (там не придется ничего менять, если сохранили оригинальное наименование алиасов таблиц)
7) Из таблицы tbl_Account удаляем сначала индексы, затем внешние ключи а затем и поля, относящиеся к средствам связи (ведь главная цель - хранить информацию в одном месте). (На продакшене это делать не обязательно, особенно если нет уверенности в том, что данные в tbl_Account соответствуют данным в tbl_AccountCommunication :smile:)
Таблица контрагентов
Все готово, можно запускать клиент и проверять, что все работает.

По поводу того, какой вариант будет работать быстрее - нужно смотреть (к сожалению под рукой сейчас нет профайлера чтобы собрать запросы которые генерируются в первом и втором вариантах а потом проверить планы выполнения). И это нужно делать на копии продакшена с актуальным объемом данных (этого тоже нет :smile:). Плюс как-то нужно учесть логирование изменений, отправку пакетов интеграции...

Мое мнение - и тот и другой вариант не будут сильно "перегружать" SQL сервер запросами.
Поверьте, не это "убивает" производительность системы, а те причины, по которым стоит переделать данный функционал :smile:

Показать все комментарии