Как настроить поиск и отбор по колонке Динамического списка, заполняемой в ПриПолученииДанныхНаСервере?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке интерфейсов на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с классической архитектурной проблемой: в динамическом списке присутствует колонка, данные в которой рассчитываются или собираются программно в обработчике ПриПолученииДанныхНаСервере. Когда пользователю требуется выполнить поиск или установить отбор по этой колонке, стандартные механизмы платформы (такие как поиск по нажатию Ctrl+F) либо не работают, либо вызывают критические ошибки. В этой статье мы подробно разберем, почему так происходит, и рассмотрим три практических способа решения этой задачи с примерами кода.

Почему не работает стандартный поиск и отбор?

Для того чтобы выбрать оптимальный путь решения, нам необходимо детально проанализировать жизненный цикл выборки и отображения данных динамическим списком. Давайте посмотрим, в какой последовательности платформа выполняет операции:

  1. Пользователь вводит поисковую строку или интерактивно накладывает фильтр на динамический список.
  2. Платформа транслирует этот отбор в секцию ГДЕ (или WHERE на уровне СУБД) SQL-запроса, который отправляется к базе данных.
  3. СУБД выполняет запрос и возвращает на сервер приложений 1С только ту порцию записей (страницу), которая удовлетворяет условиям.
  4. Только после этого для полученной порции строк вызывается серверное событие ПриПолученииДанныхНаСервере, где разработчик программно заполняет виртуальные колонки.

Важный вывод: поскольку СУБД выполняет фильтрацию до того, как управление передается в код обработчика, она ничего не знает о тех значениях, которые мы собираемся записать в колонку программно. Если колонка в тексте запроса инициализирована пустой строкой, СУБД попытается найти совпадение по пустой строке и вернет пустой результат. В итоге обработчик заполнения даже не запустится.

Также разберем причину ошибки «Не найдено поле отбора», с которой столкнулся автор темы при попытке программно добавить элемент отбора компоновки данных. Данная ошибка возникает из-за того, что поле отсутствует в схеме компоновки данных (СКД), на которой базируется динамический список. Чтобы поле стало доступным для разработчика в настройках компоновщика, в произвольном запросе динамического списка обязательно должна быть объявлена хотя бы «колонка-заглушка», например:


ВЫБРАТЬ
    РегистрСведений.Измерение1 КАК Измерение1,
    РегистрСведений.Ресурс1 КАК Ресурс1,
    ВЫРАЗИТЬ("" КАК СТРОКА(1000)) КАК Контрагент
ИЗ
    РегистрСведений.РегистрСведений КАК РегистрСведений

После этого для поля Контрагент в настройках списка необходимо взвести флаг «Использовать всегда». Однако помните: это лишь сделает поле видимым в настройках компоновщика, но саму проблему поиска по реальным программно заполненным данным не решит.

Ниже мы подробно разберем три профессиональных сценария реализации поиска, которые помогут обойти это ограничение платформы.

---

Решение 1. Косвенный отбор через предварительный запрос (Массив ключей)

Это самый популярный способ быстро обойти ограничение без изменения структуры метаданных конфигурации. Суть метода заключается в том, что мы перехватываем ввод пользователя, выполняем отдельный точечный запрос к базе данных для поиска подходящих записей и накладываем на динамический список отбор по ключевому полю (например, по уникальной ссылке или измерению регистра). Похожий механизм сопоставления по произвольным реквизитам реализован в обработке, выполняющей перенос данных между похожими конфигурациями 1С при несовпадении ссылок.

Рассмотрим по шагам реализацию этого механизма:

Шаг 1. Создание элементов интерфейса. Добавим на форму строковый реквизит с именем СтрокаПоискаКонтрагента и вынесем его на форму в качестве поля ввода. Для этого элемента определим событие ПриИзменении.

Шаг 2. Написание обработчика на сервере. При изменении строки поиска мы должны запустить поисковый запрос. Обратите внимание на важный нюанс производительности: если поисковому запросу будет соответствовать слишком много записей (например, десятки тысяч документов), передача гигантского массива в условие В Списке приведет к падению производительности СУБД. Поэтому мы ограничим выборку разумным пределом (например, 500 записей) и предупредим пользователя.

Проанализируем программный код реализации:


&НаКлиенте
Процедура СтрокаПоискаКонтрагентаПриИзменении(Элемент)
    
    ВыполнитьКосвенныйПоискНаСервере();
    
КонецПроцедуры

&НаСервере
Процедура ВыполнитьКосвенныйПоискНаСервере()
    
    // Очищаем старый отбор по ключевому полю, если он был установлен ранее
    ИмяПоляКлюча = "Документ"; // Ключевое поле нашего списка
    ПолеКомпоновки = Новый ПолеКомпоновкиДанных(ИмяПоляКлюча);
    
    ЭлементОтбораСписка = Неопределено;
    Для Каждого ЭлементИзОтбора Из Список.Отбор.Элементы Цикл
        Если ТипЗнч(ЭлементИзОтбора) = Тип("ЭлементОтбораКомпоновкиДанных") 
            И ЭлементИзОтбора.ЛевоеЗначение = ПолеКомпоновки Тогда
            ЭлементОтбораСписка = ЭлементИзОтбора;
            Прервать;
        КонецЕсли;
    КонецЦикла;
    
    // Если поисковая строка пустая — просто отключаем или удаляем отбор
    Если ПустаяСтрока(СтрокаПоискаКонтрагента) Тогда
        Если ЭлементОтбораСписка <> Неопределено Тогда
            ЭлементОтбораСписка.Использование = Ложь;
        КонецЕсли;
        Возврат;
    КонецЕсли;
    
    // Выполняем запрос для поиска подходящих ключей
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 501
        |	ДокументТабличнаяЧасть.Ссылка КАК Ссылка
        |ИЗ
        |	Документ.РеализацияТоваровУслуг.Контрагенты КАК ДокументТабличнаяЧасть
        |ГДЕ
        |	ДокументТабличнаяЧасть.Контрагент.Наименование ПОДОБНО &СтрокаПоиска";
        
    Запрос.УстановитьПараметр("СтрокаПоиска", "%" + СтрокаПоискаКонтрагента + "%");
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        // Если ничего не найдено, передаем пустой массив, чтобы список очистился
        МассивКлючей = Новый Массив;
    Иначе
        ТаблицаДанных = РезультатЗапроса.Выгрузить();
        Если ТаблицаДанных.Количество() > 500 Тогда
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
                "Найдено слишком много совпадений. Пожалуйста, уточните поисковый запрос!");
            // Удаляем лишнюю строку, чтобы не перегружать СУБД
            ТаблицаДанных.Удалить(500);
        КонецЕсли;
        МассивКлючей = ТаблицаДанных.ВыгрузитьКолонку("Ссылка");
    КонецЕсли;
    
    // Добавляем или активируем отбор компоновки данных
    Если ЭлементОтбораСписка = Неопределено Тогда
        ЭлементОтбораСписка = Список.Отбор.Элементы.Добавить(Тип("ЭлементОтбораКомпоновкиДанных"));
        ЭлементОтбораСписка.ЛевоеЗначение = ПолеКомпоновки;
    КонецЕсли;
    
    ЭлементОтбораСписка.ВидСравнения = ВидСравненияКомпоновкиДанных.ВСписке;
    ЭлементОтбораСписка.ПравоеЗначение = МассивКлючей;
    ЭлементОтбораСписка.Использование = Истина;
    
КонецПроцедуры

Этот метод хорош тем, что он не требует изменения структуры хранения данных в СУБД, однако при частых поисковых запросах он создает дополнительную точечную нагрузку на сервер СУБД. Для контроля корректности данных после подобных доработок используется автоматическая сверка бухгалтерских и налоговых проводок между базами 1С.

---

Решение 2. Перенос логики в SQL-запрос динамического списка (ЛЕВОЕ СОЕДИНЕНИЕ)

Очень часто разработчики используют обработчик ПриПолученииДанныхНаСервере необоснованно, опасаясь усложнять SQL-запрос динамического списка. Но если связующие данные реально физически присутствуют в базе данных (например, контрагент привязан к документу через табличную часть или подчиненный регистр), правильнее всего извлечь эти данные прямо в тексте динамического списка с помощью операции ЛЕВОЕ СОЕДИНЕНИЕ.

Давайте посмотрим на пример такого запроса. Предположим, у нас есть регистр сведений, и нам нужно отображать контрагента, который содержится в реквизитах документа-регистратора:


ВЫБРАТЬ
    Регистр.Период КАК Период,
    Регистр.ДокументРегистратор КАК ДокументРегистратор,
    // Вытаскиваем контрагента напрямую через точку или левое соединение
    ЕСТЬNULL(Регистр.ДокументРегистратор.Контрагент, ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)) КАК Контрагент
ИЗ
    РегистрСведений.ИндексныеДанные КАК Регистр

При использовании такого подхода:

Важное ограничение: данный подход отлично работает, если связь между сущностями "один к одному". Если же у одного документа может быть множество контрагентов (например, табличная часть с перечнем участников сделки), то простое левое соединение приведет к дублированию строк в динамическом списке, что недопустимо. В таких случаях следует переходить к денормализации.

---

Решение 3. Архитектурный паттерн денормализации данных при записи

В крупных высоконагруженных конфигурациях (таких как ERP 2.5, Комплексная автоматизация или Управление торговлей 11) разработчики фирмы «1С» массово применяют концепцию денормализации. Для планирования миграции на новые версии систем подготовлен раздел с решениями для переноса данных в ERP.

Денормализация — это намеренное добавление избыточных данных в структуру БД при записи для того, чтобы существенно ускорить чтение и упростить последующие поисковые запросы. Вместо динамического соединения таблиц или программного перебора строк на лету, мы заранее формируем готовую строку поиска и физически сохраняем её в базе данных.

Давайте реализуем этот паттерн на практике:

Шаг 1. Модернизация метаданных. Добавим в наш объект метаданных (документ или регистр сведений) новый реквизит с именем СписокКонтрагентовСтрокой (тип: Строка, длина 1000 символов). Рекомендуется установить для этого поля свойство «Индексировать», чтобы СУБД могла моментально производить поиск по этой колонке.

Шаг 2. Заполнение поля при записи объекта. В модуле объекта (например, документа) в обработчике события ПередЗаписью напишем код, который будет обходить табличную часть, собирать имена контрагентов через запятую и записывать полученную строку в наш новый реквизит:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Сворачиваем табличную часть для получения уникальных контрагентов
    ТаблицаКонтрагентов = КонтрагентыСделки.ВыгрузитьКолонку("Контрагент");
    
    // Удаляем дубликаты
    ОбщегоНазначенияКлиентСервер.СвернутьМассив(ТаблицаКонтрагентов);
    
    СтроковыеПредставления = Новый Массив;
    Для Каждого ТекущийКонтрагент Из ТаблицаКонтрагентов Цикл
        Если ЗначениеЗаполнено(ТекущийКонтрагент) Тогда
            СтроковыеПредставления.Добавить(Строка(ТекущийКонтрагент));
        КонецЕсли;
    КонецЦикла;
    
    // Записываем результат денормализации
    СписокКонтрагентовСтрокой = СтрСоединить(СтроковыеПредставления, ", ");
    
КонецПроцедуры

Шаг 3. Отображение в динамическом списке. Теперь в динамическом списке мы просто выводим поле СписокКонтрагентовСтрокой. Поскольку это реальное проиндексированное поле таблицы базы данных, стандартный полнотекстовый поиск платформы, отборы, группировки и сортировки будут работать идеально и с максимальной скоростью, не нагружая сервер приложений процедурами постобработки в ПриПолученииДанныхНаСервере.

---

Соответствие стандартам разработки «1С:Совместимо»

Если специфика вашей задачи всё же заставляет вас использовать программное заполнение колонок через ПриПолученииДанныхНаСервере, то, согласно официальным стандартам разработки фирмы «1С», вы обязаны принудительно запретить пользователям выполнять интерактивный отбор и сортировку по этим программным полям. Это необходимо для того, чтобы исключить возникновение системных ошибок SQL или отображение некорректно отфильтрованных (пустых) списков.

Для реализации этого стандарта добавьте в процедуру формы ПриСозданииНаСервере следующий код:


&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    
    // Запрещаем использовать программную колонку "Контрагент" в интерактивных отборах
    ПолеКонтрагент = Список.Поля.Найти("Контрагент");
    Если ПолеКонтрагент <> Неопределено Тогда
        ПолеКонтрагент.ОграничениеИспользования.Условие = Истина;
        ПолеКонтрагент.ОграничениеИспользования.Порядок = Истина;
        ПолеКонтрагент.ОграничениеИспользования.Группировка = Истина;
    КонецЕсли;
    
КонецПроцедуры

Этот код надежно защитит форму от непредусмотренных действий пользователей, скрыв возможность настройки фильтрации по неиндексируемой программной колонке в стандартном меню «Настроить список».

← На главную