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