В процессе эксплуатации информационных систем нередко возникает задача консолидации данных. Типичный сценарий: в информационной базе за определенный период накопилось множество документов одного вида (например, обращений, заказов или медицинских случаев обслуживания), оформленных на одних и тех же физических лиц или контрагентов. Требуется найти все повторяющиеся документы, объединить их табличные части в один итоговый документ, актуализировать даты и корректно вывести из оборота исходные дубли.
Проанализируем ситуацию, разберем типичные ошибки при разработке подобного алгоритма и рассмотрим эффективные варианты реализации объединения данных средствами платформы 1С:Предприятие 8.3.
Когда разработчику требуется быстро решить задачу объединения, часто создается алгоритм на основе выгрузки данных в таблицу значений с последующим поиском через НайтиСтроки(). Посмотрим на пример подобной реализации:
&НаСервере
Процедура ОбъединитьНаСервере()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Случай.Ссылка,
| Случай.Пациент
|ИЗ
| Документ.Случай КАК Случай
|ГДЕ
| Случай.ДатаНачала МЕЖДУ &ДатаНачала И &ДатаОкончания";
Запрос.УстановитьПараметр("ДатаНачала", Период.ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", Период.ДатаОкончания);
Случаи = Запрос.Выполнить().Выгрузить();
МассивПациентов = Новый Массив;
Для Каждого Случай Из Случаи Цикл
Если МассивПациентов.Найти(Случай.Пациент) = 0 ИЛИ МассивПациентов.Найти(Случай.Пациент) = Неопределено Тогда
МассивПациентов.Добавить(Случай.Пациент);
НовыйСлучай = Документы.Случай.СоздатьДокумент();
Отбор = Новый Структура;
Отбор.Вставить("Пациент", Случай.Пациент);
НайденныеДокументы = Случаи.НайтиСтроки(Отбор);
Если НайденныеДокументы.Количество() > 1 Тогда
ДатаНачалаСлучая = ТекущаяДата();
ДатаОкончанияСлучая = ТекущаяДата();
Для Каждого Документ Из НайденныеДокументы Цикл
ДокументОбъект = Документ.Ссылка.ПолучитьОбъект();
Если ДокументОбъект.ДатаНачала < ДатаНачалаСлучая Тогда
НовыйСлучай.ДатаНачала = ДатаНачалаСлучая;
Иначе
ДатаНачалаСлучая = ДокументОбъект.ДатаНачала;
КонецЕсли;
Если ДокументОбъект.ДатаОкончания < ДатаОкончанияСлучая Тогда
НовыйСлучай.ДатаОкончания = ДатаОкончанияСлучая;
Иначе
ДатаОкончанияСлучая = ДокументОбъект.ДатаОкончания;
КонецЕсли;
ЗаполнитьЗначенияСвойств(НовыйСлучай, ДокументОбъект);
Для Каждого Строка Из ДокументОбъект.Услуги Цикл
СтрокаНовУслуга = НовыйСлучай.Услуги.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаНовУслуга, Строка);
КонецЦикла;
НовыйСлучай.Номер = "";
НовыйСлучай.Объединен = Истина;
КонецЦикла;
НовыйСлучай.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Выясним причину, почему такой код содержит критические уязвимости:
Массив.Найти() возвращает индекс элемента либо значение Неопределено. Проверка МассивПациентов.Найти(...) = 0 ошибочно считает первый добавленный элемент массива отсутствующим, провоцируя повторную обработку первого пациента.ЗаполнитьЗначенияСвойств(НовыйСлучай, ДокументОбъект) находится внутри цикла по исходным документам. В результате реквизиты нового документа будут заполнены значениями из самого последнего документа группы, затирая предыдущие данные.ПолучитьОбъект() в цикле генерирует избыточную нагрузку на СУБД.Рассмотрим более производительный подход. Вместо выгрузки всего массива документов в память и использования метода НайтиСтроки() отберем дублирующие документы непосредственно на уровне СУБД с помощью предложений СГРУППИРОВАТЬ ПО и ИМЕЮЩИЕ. Кроме того, функции МИНИМУМ() и МАКСИМУМ() избавят нас от необходимости вручную вычислять граничные даты в цикле.
Разберем по шагам построение запроса для группировки:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Случай.Пациент КАК Пациент,
| МИНИМУМ(Случай.ДатаНачала) КАК ДатаНачалаМинимум,
| МАКСИМУМ(Случай.ДатаОкончания) КАК ДатаОкончанияМаксимум,
| КОЛИЧЕСТВО(Случай.Ссылка) КАК КоличествоДокументов
|ПОМЕСТИТЬ ВТ_Дубли
|ИЗ
| Документ.Случай КАК Случай
|ГДЕ
| Случай.ДатаНачала МЕЖДУ &ДатаНачала И &ДатаОкончания
| И НЕ Случай.ПометкаУдаления
|СГРУППИРОВАТЬ ПО
| Случай.Пациент
|ИМЕЮЩИЕ
| КОЛИЧЕСТВО(Случай.Ссылка) > 1
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Дубли.Пациент КАК Пациент,
| ВТ_Дубли.ДатаНачалаМинимум КАК ДатаНачала,
| ВТ_Дубли.ДатаОкончанияМаксимум КАК ДатаОкончания,
| Случай.Ссылка КАК ДокументСсылка
|ИЗ
| ВТ_Дубли КАК ВТ_Дубли
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.Случай КАК Случай
| ПО ВТ_Дубли.Пациент = Случай.Пациент
| И (Случай.ДатаНачала МЕЖДУ &ДатаНачала И &ДатаОкончания)
| И (НЕ Случай.ПометкаУдаления)
|ИТОГИ ПО
| Пациент";
Использование обхода по группировкам через ВыборкаПациент = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) позволит организовать понятный двухуровневый цикл без промежуточных коллекций и поиска соответствий в памяти.
Вместо создания нового документа и физического удаления старых целесообразно использовать подход с выбором ведущего документа. Самый ранний по дате документ назначается основным, в него переносятся данные из остальных, а дублирующие документы помечаются на удаление.
Преимущества такой стратегии:
UUID) ведущего документа, что защищает внешние интеграции и ссылки.ТабличнаяЧасть.Загрузить(), что работает значительно быстрее построчного заполнения методом Добавить().Соберем решение воедино, добавив управление транзакциями и свойство ОбменДанными.Загрузка, которое позволяет обойти лишние проверки в обработчиках событий при массовой обработке:
&НаСервере
Процедура ВыполнитьОбъединениеДокументов(СтруктураПараметров)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Случай.Пациент КАК Пациент,
| МИНИМУМ(Случай.ДатаНачала) КАК ДатаНачалаМинимум,
| МАКСИМУМ(Случай.ДатаОкончания) КАК ДатаОкончанияМаксимум
|ПОМЕСТИТЬ ВТ_ПациентыДубли
|ИЗ
| Документ.Случай КАК Случай
|ГДЕ
| Случай.ДатаНачала МЕЖДУ &ДатаНачала И &ДатаОкончания
| И НЕ Случай.ПометкаУдаления
|СГРУППИРОВАТЬ ПО
| Случай.Пациент
|ИМЕЮЩИЕ
| КОЛИЧЕСТВО(Случай.Ссылка) > 1
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Пациент КАК Пациент,
| ВТ.ДатаНачалаМинимум КАК ДатаНачалаМинимум,
| ВТ.ДатаОкончанияМаксимум КАК ДатаОкончанияМаксимум,
| Случай.Ссылка КАК ИсходныйДокумент
|ИЗ
| ВТ_ПациентыДубли КАК ВТ
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.Случай КАК Случай
| ПО ВТ.Пациент = Случай.Пациент
| И (Случай.ДатаНачала МЕЖДУ &ДатаНачала И &ДатаОкончания)
| И (НЕ Случай.ПометкаУдаления)
|УПОРЯДОЧИТЬ ПО
| Пациент,
| Случай.ДатаНачала ВОЗР
|ИТОГИ ПО
| Пациент";
Запрос.УстановитьПараметр("ДатаНачала", СтруктураПараметров.ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", СтруктураПараметров.ДатаОкончания);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
ВыборкаПациент = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаПациент.Следующий() Цикл
НачатьТранзакцию();
Попытка
ВыборкаДокументов = ВыборкаПациент.Выбрать();
ВедущийДокументОбъект = Неопределено;
Пока ВыборкаДокументов.Следующий() Цикл
Если ВедущийДокументОбъект = Неопределено Тогда
// Первый по дате документ принимаем в качестве ведущего
ВедущийДокументОбъект = ВыборкаДокументов.ИсходныйДокумент.ПолучитьОбъект();
ВедущийДокументОбъект.ДатаНачала = ВыборкаПациент.ДатаНачалаМинимум;
ВедущийДокументОбъект.ДатаОкончания = ВыборкаПациент.ДатаОкончанияМаксимум;
Иначе
// Данные из остальных документов переносим в ведущий
ДубльОбъект = ВыборкаДокументов.ИсходныйДокумент.ПолучитьОбъект();
Для Каждого СтрокаТЧ Из ДубльОбъект.Услуги Цикл
НоваяСтрока = ВедущийДокументОбъект.Услуги.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЧ);
КонецЦикла;
// Снимаем с проведения и помечаем дубль на удаление
ДубльОбъект.ОбменДанными.Загрузка = Истина;
ДубльОбъект.УстановитьПометкуУдаления(Истина);
КонецЕсли;
КонецЦикла;
Если ВедущийДокументОбъект <> Неопределено Тогда
ВедущийДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Объединение документов",
УровеньЖурналаРегистрации.Ошибка,,,
ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Рассмотренный шаблон обеспечивает атомарность операции объединения для каждого физического лица, сохраняет ссылочную целостность базы данных и корректно обновляет движения документов в регистрах.