Разработчики, работающие с платформой 1С:Предприятие 7.7 (в обиходе — «семерка» или «клюшки»), часто сталкиваются со специфическим поведением так называемых «черных» запросов. Типичная ситуация: синтаксический анализатор не выдает ошибок, метод Запрос.Выполнить() успешно возвращает единицу (истина), однако программа полностью игнорирует тело цикла Пока Запрос.Группировка() = 1 Цикл. Отладчик показывает, что метод Группировка() сразу возвращает 0, хотя программист абсолютно уверен, что данные, соответствующие условиям поиска, в базе присутствуют.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы работы запросов 1С 7.7 и рассмотрим несколько надежных способов исправления ситуации, а также альтернативные высокопроизводительные методы поиска документов.
В конфигурациях 1С 7.7 часто применяется динамическое построение текста запроса, когда имя метаданных передается в виде строковой переменной. Рассмотрим фрагмент проблемного кода:
|ТекДокумент = Документ." + ТипДок + ".ТекущийДокумент;
|Идентификатор = Документ." + ТипДок + ".Идентификатор;
Если в переменной ТипДок содержится имя документа с концевыми пробелами (например, полученное из внешнего файла или текстового реквизита без предварительной обработки) или если строка пуста, то запрос сформируется некорректно. На уровне СУБД (особенно в SQL-версии) такой запрос выполнится без ошибок инициализации, но вернет пустой результат.
Решение: Перед сборкой текста запроса всегда применяйте функцию очистки от пробелов СокрЛП() и проверяйте существование объекта в метаданных конфигурации:
ИмяДока = СокрЛП(ТипДок);
Если Метаданные.Документ(ИмяДока).Выбран() = 0 Тогда
Предупреждение("Неверно указан тип документа: " + ИмяДока);
Возврат;
КонецЕсли;
ТекстЗапроса = "
|Период с НачПериод по КонПериод;
|ОбрабатыватьДокументы все;
|ТекДокумент = Документ." + ИмяДока + ".ТекущийДокумент;
|Идентификатор = Документ." + ИмяДока + ".Идентификатор;
|Группировка ТекДокумент;
|Условие(Идентификатор = ИдентификаторПоиск);
|";
В исходном запросе автора на форуме использовалась конструкция:
|Период с НачПериод;
Хотя синтаксис языка запросов 1С 7.7 теоретически допускает опускать конечную дату периода (в этом случае система подставляет рабочую дату или конец рассчитанного периода бухгалтерских итогов), на практике это часто приводит к сбоям. При работе с таблицами документов на SQL-версиях платформы неуказанная конечная дата может сместить диапазон выборки или ограничить его некорректно.
Решение: Всегда явно ограничивайте период запроса с обеих сторон. Переменные дат в тексте запроса рекомендуется передавать в круглых скобках для исключения неоднозначности парсинга платформой:
// Явное указание периода с использованием скобок для переменных
ТекстЗапроса = "
|Период с (НачПериод) по (КонПериод);
|// ... остальной текст запроса
|";
В качестве конечной даты (КонПериод) можно передавать переменную, содержащую ТекущаяДата(), РабочаяДата() или явную дату конца интервала.
В обсуждаемом примере реквизит Идентификатор имеет строковый тип с длиной 36 символов (стандартный размер для GUID / UUID). Если в базе данных (в формате DBF или MS SQL) длина этого реквизита объявлена больше (например, 50 символов), система дополняет строку концевыми пробелами до максимальной длины. При прямом сравнении:
|Условие(Идентификатор = ИдентификаторПоиск);
Значение Идентификатор из базы (длиной 50 символов с пробелами на конце) никогда не совпадет с точным 36-символьным значением ИдентификаторПоиск. В результате условие отсечет все записи, и цикл группировки окажется пустым.
Решение: Отрезайте концевые пробелы с обеих сторон сравнения с помощью функции СокрЛП() прямо внутри секции Условие:
|Условие(СокрЛП(Идентификатор) = СокрЛП(ИдентификаторПоиск));
Важное предупреждение: Использование функций обработки строк (таких как СокрЛП) в секции Условие приводит к тому, что СУБД не сможет использовать индексы при поиске (произойдет так называемый Full Table Scan). На больших базах данных это вызовет колоссальное падение производительности. Безопаснее дополнять поисковую строку пробелами до длины реквизита в коде перед выполнением запроса либо использовать альтернативные методы поиска.
В тексте запроса была указана директива |Без итогов;. Она сообщает оптимизатору платформы, что дерево итогов строить не нужно, а результат должен быть возвращен в виде плоской таблицы. При включенной директиве Без итогов действуют следующие правила:
Запрос.Группировка() должен вызываться строго без параметров (без указания номера или имени группировки). Вызов вида Запрос.Группировка(1) или Запрос.Группировка("ТекДокумент") при плоской выборке гарантированно вернет 0 на некоторых релизах движка 7.7.
|Функция ВсегоДокументов = Счетчик();
---
Штатные "черные" запросы в 1С 7.7 к таблицам документов работают крайне медленно, так как платформа сначала выполняет тяжелый SQL-запрос во временные таблицы на сервере, а затем выкачивает их локально на клиентский компьютер для последующей фильтрации. Если вам нужно найти конкретные документы по уникальному идентификатору, рекомендуется отказаться от запросов в пользу более эффективных инструментов.
Для небольших интервалов дат обычный перебор документов определенного вида работает значительно быстрее, так как задействует встроенные механизмы кэширования платформы:
Док = СоздатьОбъект("Документ." + СокрЛП(ТипДок));
Если Док.ВыбратьДокументы(НачПериод, КонПериод) = 1 Тогда
Пока Док.ПолучитьДокумент() = 1 Цикл
// Сравниваем очищенные от пробелов строки
Если СокрЛП(Док.Идентификатор) = СокрЛП(ИдентификаторПоиск) Тогда
// Документ успешно найден!
Сообщить("Найден документ: " + Док.ТекущийДокумент());
Прервать;
КонецЕсли;
КонецЦикла;
КонецЕсли;
Самый правильный и быстрый способ поиска в 1С 7.7 без привлечения сторонних компонент — использование метода ВыбратьПоЗначению(). Чтобы этот метод работал, выполните следующие шаги:
Идентификатор (он должен быть либо Общим реквизитом документов, либо реквизитом конкретного документа)._1SJOURN или таблице конкретного документа).
// Используем универсальный объект "Документ" для поиска по общему реквизиту
Док = СоздатьОбъект("Документ");
ИскомыйВидДока = СокрЛП(ТипДок);
Если Док.ВыбратьПоЗначению(НачПериод, КонПериод, "Идентификатор", СокрЛП(ИдентификаторПоиск)) = 1 Тогда
Пока Док.ПолучитьДокумент() = 1 Цикл
// Проверяем тип найденного документа, если поиск идет среди всех видов
Если Док.Вид() = ИскомыйВидДока Тогда
Сообщить("Документ найден по индексу: " + Док.ТекущийДокумент());
// Выполняем логику...
КонецЕсли;
КонецЦикла;
КонецЕсли;
Если ваша база данных развернута на Microsoft SQL Server, использование штатных средств 1С 7.7 для сложных выборок неэффективно. С помощью популярной библиотеки 1С++ (класс ODBCRecordset) можно написать прямой SQL-запрос, который обратится непосредственно к таблицам СУБД, минуя программный слой 1С:
// Пример выполнения прямого SQL-запроса через 1С++
Recordset = СоздатьОбъект("ODBCRecordset");
ТекстSQL = "
|SELECT
| Doc.ID as [Док $Документ." + СокрЛП(ТипДок) + "]
|FROM
| $Документ." + СокрЛП(ТипДок) + " as Doc WITH (NOLOCK)
|INNER JOIN
| _1SJOURN as Journ WITH (NOLOCK) ON Journ.IDDoc = Doc.IDDoc
|WHERE
| $Doc.Идентификатор = :ИскомыйИД
| AND Journ.Date_Time_IDDoc BETWEEN :НачДата AND :КонДата
|";
Recordset.УстановитьТекстовыйПараметр("ИскомыйИД", СокрЛП(ИдентификаторПоиск));
Recordset.УстановитьТекстовыйПараметр("НачДата", НачПериод);
Recordset.УстановитьТекстовыйПараметр("КонДата", КонПериод);
ТЗ = Recordset.ВыполнитьИнструкцию(ТекстSQL);
// На выходе получаем плоскую Таблицу Значений со списком найденных документов
ТЗ.ВыбратьСтроку();
Такой подход полностью снимает проблемы с неверно сопоставленными типами данных, нечувствителен к ошибкам парсинга штатного движка запросов 1С 7.7 и выполняется со скоростью работы SQL-сервера.