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