При разработке обработок массового формирования документов (например, счетов на оплату или актов за регулярные услуги) часто возникает задача предотвратить повторное создание одинаковых документов за один и тот же период. Попытки решить эту задачу «в лоб» через перебор всех объектов в циклах и обращение к реквизитам через точку приводят к резкому падению производительности и ошибкам времени выполнения.
Разберем типичные ошибки в логике создания копий документов, выясним причины сопутствующих сбоев платформы и подробно рассмотрим надежные архитектурные методы контроля дубликатов.
Проанализируем распространенную ситуацию: разработчик отбирает массив документов, в цикле вызывает метод Скопировать(), меняет реквизиты, а затем во вложенном цикле проверяет совпадение с уже существующими счетами. В таком подходе кроется сразу несколько критических проблем:
Скопировать() создает в оперативной памяти экземпляр типа ДокументОбъект. Этот объект еще не записан в базу данных. Вызов несуществующего метода ДокументКопия.Удалить() приводит к аварийному завершению программы. Чтобы отказаться от создания копии, достаточно просто не вызывать метод Записать(). Несохраненный объект будет автоматически удален сборщиком мусора платформы при выходе переменной из области видимости.Записать(), обращение к его свойству ДокументКопия.Ссылка возвращает пустую ссылку. При последующей передаче этой ссылки в типовые процедуры формирования печатных форм или сохранения файлов (например, в PDF или Excel) внутренний запрос печатной формы не находит данных и возвращает NULL. В результате возникает ошибка вида:
{ВнешняяОбработка.Счета.Форма.Форма(761)}: Преобразование значения к типу Булево не может быть выполнено
СуммаКПрописи = Сумма + ?(Шапка.СуммаВключаетНДС, 0, СуммаНДС);
?() ожидает значение типа Булево, но получает NULL из-за пустой ссылки.ЭлементМассива.Контрагент, Счет.СуммаДокумента) в двойном цикле генерирует отдельный SQL-запрос к СУБД на каждое обращение, что многократно замедляет работу системы.Наиболее производительный и масштабируемый подход — переложить всю проверку на сторону СУБД с помощью единого пакетного запроса. Мы помещаем параметры отбираемых документов во временную таблицу и соединяем их с существующими документами в базе данных.
Разберем по шагам реализацию этой схемы:
МенеджерВременныхТаблиц.ЛЕВОЕ СОЕДИНЕНИЕ с таблицей существующих счетов по ключевым полям и отбираем только те записи, для которых в базе еще нет созданных документов (где ссылка существующего документа ЕСТЬ NULL).НЕ ДокументСчет.ПометкаУдаления.Посмотрим на пример реализации серверной процедуры:
// Формируем таблицу параметров для проверки
ТаблицаДляПроверки = Новый ТаблицаЗначений;
ТаблицаДляПроверки.Колонки.Добавить("ДокументОснование", Новый ОписаниеТипов("ДокументСсылка.СчетНаОплатуПокупателю"));
ТаблицаДляПроверки.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
ТаблицаДляПроверки.Колонки.Добавить("Комментарий", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(100)));
Для Каждого СсылкаСчет Из МассивОтобранныхДокументов Цикл
СтрокаТЗ = ТаблицаДляПроверки.Добавить();
СтрокаТЗ.ДокументОснование = СсылкаСчет;
СтрокаТЗ.Контрагент = СсылкаСчет.Контрагент;
СтрокаТЗ.Комментарий = НовыйКомментарий;
КонецЦикла;
// Выполняем запрос для отсечения дублей
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.ДокументОснование КАК ДокументОснование,
| ТЗ.Контрагент КАК Контрагент,
| ТЗ.Комментарий КАК Комментарий
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТаблицаДляПроверки КАК ТЗ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.ДокументОснование КАК ДокументОснование
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.СчетНаОплатуПокупателю КАК СуществующиеСчета
| ПО ВТ.Контрагент = СуществующиеСчета.Контрагент
| И ВТ.Комментарий = СуществующиеСчета.Комментарий
| И (НЕ СуществующиеСчета.ПометкаУдаления)
|ГДЕ
| СуществующиеСчета.Ссылка ЕСТЬ NULL";
Запрос.УстановитьПараметр("ТаблицаДляПроверки", ТаблицаДляПроверки);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
МассивСозданныхСсылок = Новый Массив;
// Создаем только те документы, которые прошли фильтр
Пока Выборка.Следующий() Цикл
НовыйДокумент = Выборка.ДокументОснование.Скопировать();
НовыйДокумент.Дата = НоваяДатаДок;
НовыйДокумент.Комментарий = НовыйКомментарий;
Попытка
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
МассивСозданныхСсылок.Добавить(НовыйДокумент.Ссылка);
Исключение
Сообщить("Ошибка при записи документа: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Если требуется контролировать уникальность создаваемых данных «на лету» без постоянных обращений к базе данных (например, чтобы избежать создания повторяющихся строк внутри самого обрабатываемого пакета), целесообразно использовать коллекцию ТаблицаЗначений с предварительно построенными индексами по ключевым полям поиска.
Рассмотрим пример реализации такого алгоритма:
// Определяем поля, по которым контролируется уникальность
ПоляПоиска = "Контрагент, ДоговорКонтрагента, Комментарий";
// Инициализируем фильтр и индексированную таблицу уникальности
ФильтрПоиска = Новый Структура(ПоляПоиска);
ТаблицаУникальности = Новый ТаблицаЗначений;
ТаблицаУникальности.Колонки.Добавить("Контрагент");
ТаблицаУникальности.Колонки.Добавить("ДоговорКонтрагента");
ТаблицаУникальности.Колонки.Добавить("Комментарий");
ТаблицаУникальности.Индексы.Добавить(ПоляПоиска);
МассивСозданныхСсылок = Новый Массив;
Для Каждого СсылкаИсходная Из МассивОтобранныхДокументов Цикл
// Заполняем структуру поиска данными текущего документа
ФильтрПоиска.Контрагент = СсылкаИсходная.Контрагент;
ФильтрПоиска.ДоговорКонтрагента = СсылкаИсходная.ДоговорКонтрагента;
ФильтрПоиска.Комментарий = НовыйКомментарий;
// Проверяем наличие комбинации по индексу таблицы значений
НайденныеСтроки = ТаблицаУникальности.НайтиСтроки(ФильтрПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
// Дубликат обнаружен в текущем сеансе формирования
Продолжить;
КонецЕсли;
// Создаем и записываем документ
НовыйСчет = СсылкаИсходная.Скопировать();
НовыйСчет.Дата = НоваяДатаДок;
НовыйСчет.Комментарий = НовыйКомментарий;
НовыйСчет.Записать(РежимЗаписиДокумента.Запись);
МассивСозданныхСсылок.Добавить(НовыйСчет.Ссылка);
// Фиксируем ключевые параметры созданного документа в таблице контроля
ЗаполнитьЗначенияСвойств(ТаблицаУникальности.Добавить(), ФильтрПоиска);
КонецЦикла;
Поиск дубликатов по строке комментария или совпадению суммы является ненадежным решением: пользователь может случайно отредактировать комментарий или выставить счет на аналогичную сумму в рамках других взаиморасчетов. Для построения надежной системы учета повторных операций применяются два системных подхода:
ГДЕ Счет.ДокументОснование = &ИсходныйСчет И НЕ Счет.ПометкаУдаления. Также для анализа цепочек можно использовать типовой критерий отбора СвязанныеДокументы.ИсторияФормированияПериодическихСчетов). В измерения помещаются ДокументИсточник и ПериодНачисления, а в ресурс — СозданныйДокумент. Перед созданием документа выполняется быстрая проверка наличия записи в регистре методом СоздатьМенеджерЗаписи() или запросом к срезу.Использование запросов с временными таблицами и строгой фиксации связей между документами гарантирует высокую скорость обработки данных и полностью исключает появление дублей и ошибок времени выполнения.