Преамбула
- Программа для обработки сметной документации, ведения сметного документооборота и исполнительной документации;
- Имеем более 7 тысяч смет на одном объекте (а объектов не 1). В каждой из которых в среднем более 100 строк. В каждой строке при этом есть определенные ресурсы (материалы, трудозатраты, механизмы);
- Необходимо выполнить сводный расчет материалов по N количеству смет (в среднем более 100).
Сам расчет – это, свернутая таблица по материалам. И понятно, что в итоге мы имеем ну 1000-2000 разных материалов для выполнения работ. Но, эти пару тысяч материалов собираются путем агрегирования данных из нескольких сотен (и более) смет, т.е. нескольких тысяч сметных позиций, а также вложенных в них ресурсов.
Расчет выполняется в документе, поскольку результат должен быть сохранен, для дальнейшей отправки в обработку, тендер и т.д.
Для понимания пользователя, что расчет выполнен верно, требуется реализовать ряд возможностей:
- из формы документа, при нажатии на свернутой позиции материалов, вывести данные: Смета - Позиция / Ресурс – Количество;
- печать утвержденной формы расшифровки к данной материальной ведомости;
- при объединении, перемещении и других операциях со строками все расшифровки должны переходить к новым строкам владельцам и т.д.
Табличная часть и 99'999 строк
Собственно, с самого начала понятно, что сводную таблицу материалов храним в табличной части документа, поскольку она будет отображаться в момент открытия. И лучше для этого использовать штатные механизмы. Но, вот расшифровку сбора данных, с самого начала не было никакого желания хранить в табличной части. Да и на первом же эксперименте, на примере не самой большой материальной ведомости, мы поймали данное ограничение.
В результате был придуман обходной путь:
- регистр сведений, не подчиненный регистратору (сбор выполняется редко, при каждой перезаписи документа нет нужды перезаписывать регистр) с ведущим измерением ДокументСсылка.МатериальнаяВедомость (имя документа не важно);
- на форме при создании на сервере записываем пустое значение во временное хранилище с указанием уникального идентификатора формы:
- таким образом, получили постоянный адрес для хранения данных;
- гарантировали жизнь данного адреса, до момента закрытия формы.
- реализовали фоновое чтение данных из регистра по частям, с указанием условия какие данные запросил пользователь. При повторном чтении, обращения к БД не происходит, программа хранит информацию что данные по указанной строке прочитаны;
- Маленькая деталь: чтение данных не обязательно делать при открытии, она может быть реализована порциями, согласно отбору, полученному на основании запроса пользователя.
- перед записью на сервере, передаем адрес хранилища в модуль объекта документа, с целью создания записей в регистре сведений. Естественно, не производим запись, если в этом нет необходимости (например: повторный сбор данных сводной ведомости не производился).
Таким образом, кроме того, чтобы мы обеспечили хранение и чтение данных, мы к тому же, не помещаем их в реквизит формы, в конечном итоге облегчая "вес пакета данных формы" при обмене между клиентом и сервером. Хотя, мы нагружаем сервер, но кто его жалеет, да? Принимая во внимание, что с данным документом работу выполняет крайне ограниченный круг лиц, а для остальных есть отчеты и поэтому посчитали, что сервер нагрузку переживет.
Остается существенный вопрос, зачем мы храним данные расшифровки истории сбора сводной ведомости? Во-первых, периодически, в процессе формирования актов КС-2, нам необходимо выполнять чтение данных о реальной стоимости материалов согласно данной ведомости (она, к слову, подписывается с заказчиком). Наименования материалов могут меняться, строки объединяться, но в КС-2 мы должны показать информацию о сметном названии материала, позиции в данной ведомости и ценой согласованной с заказчиком (или субподрядчиком). Таким образом, регистр расшифровки выполняет не только роль хранения "истории", но и инструмента достаточно точного получения данных.
Реализация в коде
Модуль формы документа
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// АдресХранилища - реквизит формы, тип Строка, длина - 0
ЭтотОбъект.АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, ЭтотОбъект.УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// запуск фоновой процедуры считывания данных во временное хранилище
// может быть выполнен полностью, или частями, или по запросу пользователя
КонецПроцедуры
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// для записи данных из хранилища в регистр сведений
// АдресХранилища - переменная модуля объекта (см. МодульОбъекта)
ТекущийОбъект.АдресХранилища = ЭтотОбъект.АдресХранилища;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеИзХранилищаНаСервере(знач АдресХранилища, знач НастройкаОтбора)
// пример получения данных из хранилища
Если НЕ ЭтоАдресВременногоХранилища(АдресХранилища) Тогда
Возврат;
КонецЕсли;
ТаблицаДанных = ПолучитьИзВременногоХранилища(АдресХранилища);
Если НЕ ТипЗнч(ТаблицаДанных) = Тип("ТаблицаЗначений") Тогда
// здесь можно разместить процедуру считывания данных согласно настройкам отбора
Возврат;
КонецЕсли;
КопияТаблицы = ТаблицаДанных.Скопировать(НастройкаОтбора);
Возврат ОбщегоНазначения.ТаблицаЗначенийВМассив(КопияТаблицы);
КонецФункции
Модуль объекта документа
// обязательная переменная, для передачи данных между формой и модулем объекта
Перем АдресХранилища Экспорт;
Процедура ПриЗаписи(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
ЗаписатьДанныеВременногоХранилища(Отказ);
КонецПроцедуры
Процедура ЗаписатьДанныеВременногоХранилища(Отказ)
Если НЕ ЭтоАдресВременногоХранилища(АдресХранилища) Тогда
Возврат;
КонецЕсли;
ТаблицаДанных = ПолучитьИзВременногоХранилища(АдресХранилища);
Если НЕ ТипЗнч(ТаблицаДанных) = Тип("ТаблицаЗначений") Тогда
Возврат;
КонецЕсли;
// дополнительно можно проверить, что таблица данных получена путем чтения
// а не повторного заполнения пользователем
Попытка
// обратите внимание, у регистра сведений есть Измерение - ДокументСсылка
// рекомендуется делать данное измерение ведущим, чтобы при удалении документа записи очищались
// при этом сам регистр не подчинен регистратору, иначе при повторной записи данные будут очищаться
БлокировкаДанных = Новый БлокировкаДанных;
ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.ХранениеДанныхДокумента");
ЭлементБлокировки.УстановитьЗначение("ДокументСсылка", ЭтотОбъект.Ссылка);
БлокировкаДанных.Заблокировать();
НаборЗаписей = РегистрыСведений.ХранениеДанныхДокумента.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ДокументСсылка.Установить(ЭтотОбъект.Ссылка);
Для Каждого СтрокаЗаписи Из ТаблицаДанных Цикл
ЗаписьРегистра = НаборЗаписей.Добавить();
ЗаполнитьЗначенияСвойств(ЗаписьРегистра, СтрокаЗаписи);
ЗаписьРегистра.ДокументСсылка = ЭтотОбъект.Ссылка;
КонецЦикла;
НаборЗаписей.Записать(Истина);
Исключение
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(НСтр("ru='При записи доп. сведений произошла ошибка: '") + Символы.ПС + ТекстОшибки,,,, Отказ);
КонецПопытки;
КонецПроцедуры
Процедура ОчиститьРегистрСведенийХранениеДанныхДокумента()
НаборЗаписей = РегистрыСведений.ХранениеДанныхДокумента.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ДокументСсылка.Установить(ЭтотОбъект.Ссылка);
НаборЗаписей.Прочитать();
НаборЗаписей.Очистить();
НаборЗаписей.Записать(Истина);
КонецПроцедуры
Ну вот как то так.
Постскриптум
Мы понимаем, что могли бы использовать что-нибудь "стильное, модное, молодежное", внешнюю базу данных и может быть даже NoSQL. Но, у нас просто было мало времени на подумать и еще меньше времени на реализовать. Как всегда результат нужен был "здесь и вчера". Решение было создано очень быстро, запущено в работу и проходит анализ на предмет скрытых просчетов. Более того, не у всех заказчиков возможно использование сторонних БД, поэтому данный вопрос будет скорее всего повторно обсуждаться гораздо позже.
Повторюсь, решение возможно не уникальное и не лишенное недостатков. Поэтому прошу направить на путь истинный, если имеются дельные предложения.