При разработке и сопровождении конфигураций на платформе 1С:Предприятие 7.7 программисты часто сталкиваются с задачей поиска дублирующихся элементов в справочниках. Это может быть поиск товаров с одинаковыми артикулами или штрихкодами, контрагентов с идентичными ИНН или любых других элементов с совпадающими реквизитами. Основная сложность заключается в том, чтобы сделать это быстро, безопасно и без избыточной нагрузки на базу данных.
В этой статье мы подробно разберем несколько практических подходов к решению данной задачи, проанализируем их сильные и слабые стороны, изучим примеры кода и выясним, какой метод является наиболее оптимальным в различных сценариях работы системы.
Начнем с самого правильного и быстрого встроенного механизма объектной модели 1С 7.7. Платформа предоставляет нам специализированный метод ВыбратьЭлементыПоРеквизиту, который позволяет открыть выборку элементов справочника по значению конкретного реквизита. Этот метод работает крайне эффективно, так как использует готовые индексы базы данных.
Прежде чем использовать данный метод в коде, необходимо выполнить обязательное условие в Конфигураторе. У реквизита справочника, по которому будет производиться поиск, на вкладке «Дополнительно» должен быть установлен флаг «Сортировка» (в некоторых версиях он называется «Отбор»). Если этот флаг не установлен, платформа выдаст ошибку времени выполнения при попытке вызвать метод.
Рассмотрим по шагам практический пример использования этого метода для поиска дубликатов номенклатуры по артикулу:
// Создаем список для хранения найденных элементов
СписокДублей = СоздатьОбъект("СписокЗначений");
// Создаем объект справочника Номенклатура
СпрНоменклатура = СоздатьОбъект("Справочник.Номенклатура");
ИскомыйАртикул = СокрЛП(ВыбАртикул);
// Выполняем выборку элементов по реквизиту
// Параметры: ИмяРеквизита, Значение, РежимИерархии (0 - без учета, 1 - с учетом), РежимГрупп (0 - только элементы)
Если СпрНоменклатура.ВыбратьЭлементыПоРеквизиту("Артикул", ИскомыйАртикул, 0, 0) = 1 Тогда
Пока СпрНоменклатура.ПолучитьЭлемент() = 1 Цикл
// Добавляем ссылку на найденный элемент в список значений
СписокДублей.ДобавитьЗначение(СпрНоменклатура.ТекущийЭлемент());
Сообщить("Найден дубликат: " + СпрНоменклатура.Наименование + " (Код: " + СпрНоменклатура.Код + ")");
КонецЦикла;
КонецЕсли;
Преимущества метода:
Недостатки метода:
А теперь проанализируем весьма специфический прием, который иногда называют «транзакционным поиском». Этот способ часто применяется разработчиками, когда у реквизита нет флага «Сортировка», менять конфигурацию нельзя, а использовать внешние компоненты не хочется.
Суть трюка заключается в следующем: стандартный метод НайтиПоРеквизиту умеет находить только первый элемент с подходящим значением реквизита. Чтобы заставить его найти второй, третий и последующие элементы, разработчики временно удаляют найденный элемент внутри транзакции. При следующем вызове метода платформа находит уже следующий дубликат. После завершения сбора данных транзакция принудительно отменяется методом ОтменитьТранзакцию, и все удаленные элементы возвращаются на свои места.
Посмотрим на пример реализации такого алгоритма с обработкой возможных исключений:
СписокДублей = СоздатьОбъект("СписокЗначений");
Спр = СоздатьОбъект("Справочник.Номенклатура");
ИскомыйАртикул = СокрЛП(ProductSku.Артикул);
// Запускаем транзакцию
НачатьТранзакцию();
// Запускаем цикл поиска
Пока Спр.НайтиПоРеквизиту("Артикул", ИскомыйАртикул, 1) = 1 Цикл
Попытка
// Сохраняем ссылку на найденный элемент
СписокДублей.ДобавитьЗначение(Спр.ТекущийЭлемент());
// Физически удаляем элемент, чтобы скрыть его для следующего шага поиска
Спр.Удалить(1);
Исключение
// Если элемент заблокирован другим пользователем, прерываем процесс
Сообщить("Ошибка блокировки! Возможно, элемент открыт другим пользователем.");
Прервать;
КонецПопытки;
КонецЦикла;
// ВНИМАНИЕ! Обязательно откатываем транзакцию, чтобы вернуть элементы в базу
ОтменитьТранзакцию();
// Выводим результаты работы алгоритма
Если СписокДублей.РазмерСписка() > 0 Тогда
Для Сч = 1 По СписокДублей.РазмерСписка() Цикл
Эл = СписокДублей.ПолучитьЗначение(Сч);
Сообщить("Найдено в транзакции: " + Эл.Наименование + " | Код: " + Эл.Код);
КонецЦикла;
КонецЕсли;
ВНИМАНИЕ! Этот метод является крайне опасным «костылем»! Рассмотрим подробнее, к каким катастрофическим последствиям может привести его необдуманное использование:
Удалить(1) инициирует выполнение предопределенной процедуры ПриУдалении(), прописанной в модуле справочника. Если там заложена тяжелая логика проверки ссылок, каскадного удаления подчиненных элементов или отправки уведомлений, это приведет к резкому падению производительности или возникновению блокировок.Удалить(1) завершится ошибкой. Даже использование конструкции Попытка — Исключение не спасет ситуацию: выполнение цикла прервется, и вы получите неполный список дублей.Если вы работаете в SQL-версии 1С:Предприятия 7.7 или используете локальный движок для DBF через компоненту 1С++, то использование прямых SQL-запросов является стандартом «де-факто» для высоконагруженных систем. Прямой запрос обходит объектную модель платформы и обращается к таблицам базы данных на сервере напрямую.
Разберем пример прямого SQL-запроса, который находит все дублирующиеся элементы в справочнике Контрагенты по реквизиту ИНН:
ТекстЗапроса = "
|SELECT
| Спр.ID [Элемент $Справочник.Контрагенты],
| $Спр.ИНН ИНН
|FROM
| $Справочник.Контрагенты Спр
|WHERE
| $Спр.ИНН IN
| (SELECT
| $Спр1.ИНН
| FROM
| $Справочник.Контрагенты Спр1
| WHERE
| $Спр1.ИНН <> ''
| GROUP BY
| $Спр1.ИНН
| HAVING
| COUNT(*) > 1)
|ORDER BY
| $Спр.ИНН";
// Создаем объект класса Query из 1С++
ЗапросSQL = СоздатьОбъект("ODBCRecordset");
ТабРезультат = ЗапросSQL.ВыполнитьИнструкцию(ТекстЗапроса);
// В ТабРезультат мы получаем готовую Таблицу Значений со списком всех дубликатов
ТабРезультат.ВыбратьСтроки();
Пока ТабРезультат.ПолучитьСтроку() = 1 Цикл
Сообщить("Найден дубль ИНН: " + ТабРезультат.ИНН + " | Ссылка: " + ТабРезультат.Элемент);
КонецЦикла;
Преимущества прямого запроса:
Недостатки метода:
Что делать, если база данных файловая (DBF), менять метаданные в Конфигураторе запрещено, а внешние компоненты использовать нельзя? Существует элегантный и абсолютно безопасный способ найти все дубликаты за один проход с помощью штатного объекта ТаблицаЗначений.
Рассмотрим алгоритм реализации этого метода:
// 1. Создаем Таблицу Значений для сбора всех элементов справочника
ТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗ.НоваяКолонка("Элемент");
ТЗ.НоваяКолонка("Артикул");
ТЗ.НоваяКолонка("Количество", "Число");
Спр = СоздатьОбъект("Справочник.Номенклатура");
// Полностью обходим справочник и заполняем Таблицу Значений
Спр.ВыбратьЭлементы(0); // Выбираем без учета групп
Пока Спр.ПолучитьЭлемент() = 1 Цикл
ТЗ.НоваяСтрока();
ТЗ.Элемент = Спр.ТекущийЭлемент();
ТЗ.Артикул = СокрЛП(Спр.Артикул);
ТЗ.Количество = 1;
EndDo;
// 2. Создаем копию таблицы значений для последующего поиска соответствий
ТЗ_Копия = СоздатьОбъект("ТаблицаЗначений");
ТЗ.Выгрузить(ТЗ_Копия);
// 3. Сворачиваем исходную таблицу значений по реквизиту поиска
ТЗ.Свернуть("Артикул", "Количество");
// Теперь в ТЗ остались только уникальные Артикулы и суммарное количество их вхождений
// Нам интересны только те строки, где Количество > 1
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
Если ТЗ.Количество > 1 Тогда
Сообщить("Найден дублирующийся артикул: " + ТЗ.Артикул + " (встречается " + ТЗ.Количество + " раз)");
// Находим все элементы с этим артикулом в копии нашей таблицы
СтрокаКопии = 0;
Пока ТЗ_Копия.НайтиЗначение(ТЗ.Артикул, СтрокаКопии, "Артикул") = 1 Цикл
ЭлементДубль = ТЗ_Копия.ПолучитьЗначение(СтрокаКопии, "Элемент");
Сообщить(" -> Элемент: " + ЭлементДубль.Наименование + " | Код: " + ЭлементДубль.Код);
// Чтобы не зациклиться, временно очищаем артикул в найденной строке копии ТЗ
ТЗ_Копия.УстановитьЗначение(СтрокаКопии, "Артикул", "ОБРАБОТАНО");
КонецЦикла;
КонецЕсли;
КонецЦикла;
Преимущества метода свертки ТЗ:
Недостатки метода:
Проанализируем все рассмотренные нами варианты и подведем итог, выбрав оптимальное решение под конкретные условия разработки: