При организации интеграции между базами данных 1С (например, при переносе данных из 1С 7.7 в 1С 8.3) разработчики часто используют COM-соединение (OLE). Одним из самых узких мест в такой архитектуре является выполнение запросов к OLE-базе с наложением фильтров по сложным объектам, таким как конкретные элементы справочников, группы номенклатуры или списки значений. Попытка напрямую подставить объект, полученный через OLE, в текст запроса приводит к синтаксической ошибке «Ошибка в выражении».
В этой статье мы подробно разберем причины возникновения данной ошибки, а также рассмотрим несколько эффективных способов ее решения: от простых вариантов с использованием глобальных переменных до продвинутых архитектурных решений, значительно ускоряющих чтение данных.
Разберем механизм работы «черного запроса» 1С 7.7. Когда мы создаем объект запроса через OLE с помощью конструкции БазаОле.CreateObject("Запрос"), выполнение и компиляция этого запроса происходят во внутреннем контексте запущенного OLE-сервера 1С 7.7. Этот контекст полностью изолирован от переменных вызывающей программы (будь то 1С 8.х, скрипт VBScript или приложение на C#).
Если мы пишем условие вида Условие (Товар в ГрТовара), то парсер запросов 1С 7.7 пытается найти переменную с именем ГрТовара внутри своего окружения. Так как эта переменная определена только в коде вызывающей базы (в 1С 8.х), OLE-сервер 1С 7.7 ничего не знает о ней, что и вызывает закономерный сбой инициализации запроса.
Давайте рассмотрим пошагово, какими способами можно обойти это ограничение платформы.
Самый очевидный способ сделать переменную видимой для OLE-запроса — это объявить ее в Глобальном модуле базы-источника 1С 7.7 с ключевым словом Экспорт. В этом случае переменная станет частью глобального контекста системы, к которому у парсера запросов есть прямой доступ.
Выполним следующие действия по шагам:
Перем глОлеГруппаТоваров Экспорт;
Теперь мы можем записать в эту переменную нужный элемент (группу товаров) перед выполнением запроса. Обратите внимание, что напрямую присвоить значение переменной через точку от COM-объекта (например, БазаОле.глОлеГруппаТоваров = ТекЭл) платформа 1С 8 часто не разрешает, возвращая ошибку «Поле объекта не обнаружено». Для обхода этого ограничения мы используем встроенный метод ExecuteBatch или EvalExpr.
Посмотрим на пример кода на стороне 1С 8.х:
// 1. Инициализируем справочник в OLE-базе и находим нужную группу
ГрТовара = БазаОле.CreateObject("Справочник.Номенклатура");
Если ГрТовара.НайтиПоНаименованию(СокрЛП(ГруппаТов.Наименование), 0, 1) > 0 Тогда
ТекЭл = ГрТовара.ТекущийЭлемент();
Иначе
Сообщить("Группа не найдена в базе 1С 7.7!");
Возврат;
КонецЕсли;
// 2. Помещаем ссылку во внутреннюю строку для безопасной передачи
ВнутрСтрока = БазаОле.ЗначениеВСтрокуВнутр(ТекЭл);
// 3. Заполняем глобальную переменную в контексте 1С 7.7
БазаОле.ExecuteBatch("глОлеГруппаТоваров = ЗначениеИзСтрокиВнутр(""" + СтрЗаменить(ВнутрСтрока, """", """""") + """)");
// 4. Формируем и выполняем запрос
Запрос = БазаОле.CreateObject("Запрос");
ТекстЗапроса = "Период с '" + Формат(ДатаНачала, "ДФ=yyyyMMdd") + "' по '" + Формат(ДатаКонца, "ДФ=yyyyMMdd") + "';
|Товар = Регистр.ПартииТоваров.Товар;
|ОстатокКол = Регистр.ПартииТоваров.ОстатокТовара;
|Группировка Товар без групп;
|Функция КонКол = КонОст(ОстатокКол);
|Условие (Товар в глОлеГруппаТоваров);";
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
// Обработка результатов запроса...
КонецЕсли;
Что делать, если нам необходимо передать в условие запроса не одну конкретную группу, а динамический список элементов или несколько разных папок? Для этого отлично подойдет объект СписокЗначений. Чтобы наполнить его, напишем в Глобальном модуле 1С 7.7 универсальную сервисную функцию.
Добавим в Глобальный модуль 1С 7.7 следующий код:
Перем глСписокПараметровЗапроса Экспорт;
Функция глДобавитьВСписокПараметров(ТипОбъекта, ВидОбъекта, ИдентификаторПоиска) Экспорт
Если ПустоеЗначение(глСписокПараметровЗапроса) = 1 Тогда
глСписокПараметровЗапроса = СоздатьОбъект("СписокЗначений");
КонецЕсли;
// Если передан пустой идентификатор — очищаем список
Если ИдентификаторПоиска = "ОЧИСТИТЬ" Тогда
глСписокПараметровЗапроса.УдалитьВсе();
Возврат 1;
КонецЕсли;
Попытка
ОбъектПоиска = СоздатьОбъект(ТипОбъекта + "." + ВидОбъекта);
// Пытаемся найти по коду (можно дописать поиск по наименованию)
Если ОбъектПоиска.НайтиПоКоду(ИдентификаторПоиска, 0) = 1 Тогда
глСписокПараметровЗапроса.ДобавитьЗначение(ОбъектПоиска.ТекущийЭлемент());
Возврат 1;
КонецЕсли;
Исключение
Возврат 0;
КонецПопытки;
Возврат 0;
КонецФункции
Теперь на стороне 1С 8.х мы можем последовательно вызывать эту функцию через EvalExpr, наполняя глобальный список значений нужными нам элементами, после чего использовать этот список в условии запроса:
// Очищаем список перед новым заполнением
БазаОле.EvalExpr("глДобавитьВСписокПараметров("""", """", ""ОЧИСТИТЬ"")");
// Добавляем нужные группы номенклатуры по их кодам в 1С 7.7
БазаОле.EvalExpr("глДобавитьВСписокПараметров(""Справочник"", ""Номенклатура"", ""00000125"")");
БазаОле.EvalExpr("глДобавитьВСписокПараметров(""Справочник"", ""Номенклатура"", ""00000458"")");
// Запуск OLE-запроса
Запрос = БазаОле.CreateObject("Запрос");
ТекстЗапроса = "Период с '" + Формат(ДатаЗапроса, "ДФ=yyyyMMdd") + "';
|Товар = Регистр.ПартииТоваров.Товар;
|ОстатокКол = Регистр.ПартииТоваров.ОстатокТовара;
|Группировка Товар без групп;
|Условие (Товар в глСписокПараметровЗапроса);";
Рассмотрим очень важный аспект интеграции — скорость работы. Выполнение запросов через OLE напрямую («в лоб») и последующий обход выборки во внешней программе работают крайне медленно. Причина кроется в межпроцессном взаимодействии (маршалинге): при вызове методов Запрос.Группировка() или при получении значения каждого реквизита выборки происходит полноценный сетевой/системный вызов между процессами 1С 8 и 1С 7.7. Если в выборке более 1000 строк, процесс получения данных может растянуться на часы.
Чтобы кардинально ускорить работу (в десятки и сотни раз), настоятельно рекомендуется перенести всю логику формирования, выполнения и первоначальной обработки запроса на сторону 1С 7.7. Локально запрос отработает мгновенно, после чего мы выгрузим результат в локальную ТаблицуЗначений и передадим ее целиком в 1С 8.
Посмотрим, как реализовать этот подход. Напишем в Глобальном модуле 1С 7.7 экспортную функцию:
Функция глПолучитьОстаткиПоГруппе(КодГруппы, ДатаЗапроса) Экспорт
// Инициализируем группу номенклатуры
СпрНоменклатура = СоздатьОбъект("Справочник.Номенклатура");
Если СпрНоменклатура.НайтиПоКоду(КодГруппы, 0) = 0 Тогда
Возврат "Ошибка: Группа не найдена!";
КонецЕсли;
ВыбраннаяГруппа = СпрНоменклатура.ТекущийЭлемент();
// Создаем и выполняем запрос локально в 1С 7.7
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "Период с '" + ДатаЗапроса + "' по '" + ДатаЗапроса + "';
|Товар = Регистр.ПартииТоваров.Товар;
|ОстатокКол = Регистр.ПартииТоваров.ОстатокТовара;
|ОстатокСум = Регистр.ПартииТоваров.Стоимость;
|Группировка Товар без групп;
|Функция КонКол = КонОст(ОстатокКол);
|Функция КонСум = КонОст(ОстатокСум);
|Условие (Товар в ВыбраннаяГруппа);";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат "Ошибка выполнения запроса!";
КонецЕсли;
// Выгружаем результаты запроса в Таблицу Значений
ТабРезультат = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТабРезультат, 1, 0);
// Возвращаем объект Таблицы Значений обратно во внешнюю программу
Возврат ТабРезультат;
КонецФункции
Теперь проанализируем, насколько просто и красиво выглядит вызов этого метода из нашей обработки в 1С 8.х:
// Вызываем функцию 1С 7.7 напрямую, передавая только простые типы данных
КодГруппыВ77 = "00000512"; // Передаем код строкой
ДатаВыгрузки = Формат(Объект.Дата2, "ДФ=yyyyMMdd");
ТаблицаРезультатаОле = БазаОле.глПолучитьОстаткиПоГруппе(КодГруппыВ77, ДатаВыгрузки);
// Проверяем, вернулась ли таблица или строка с ошибкой
Если ТипЗнч(ТаблицаРезультатаОле) = Тип("Строка") Тогда
Сообщить("Произошла ошибка на стороне 1С 7.7: " + ТаблицаРезультатаОле);
Возврат;
КонецЕсли;
// Локально обходим полученную таблицу значений OLE на стороне 1С 8
ТаблицаРезультатаОле.ВыбратьСтроки();
Пока ТаблицаРезультатаОле.ПолучитьСтроку() = 1 Цикл
// Чтение реквизитов из строки OLE-таблицы происходит мгновенно
НоменклатураНаименование = ТаблицаРезультатаОле.Товар.Наименование;
Количество = ТаблицаРезультатаОле.КонКол;
Сумма = ТаблицаРезультатаОле.КонСум;
// Заполняем документы или справочники в нашей новой базе 1С 8
// ...
КонецЦикла;
При использовании этого метода вы сразу заметите колоссальный прирост производительности. Локальное ядро 1С 7.7 очень быстро выполняет запросы и упаковывает их в оперативную память. Передача одного COM-объекта ТаблицаЗначений занимает доли секунды, а чтение строк из нее происходит в разы быстрее, чем последовательный перебор группировок OLE-запроса.
Бывают ситуации, когда конфигурация 1С 7.7 находится на строгой поддержке, или у разработчика нет возможности/прав на изменение глобального модуля и написание там новых функций. В этом случае мы можем применить продвинутый трюк с использованием сериализации объектов во внутренний формат 1С.
Для реализации этого метода нам понадобится функция ЗначениеИзСтрокиВнутр(), которую мы внедрим прямо в текст формируемого запроса. Таким образом, объект будет воссоздан из строкового представления непосредственно во время выполнения условия.
Разберем пример реализации этого хитрого механизма в коде 1С 8.х:
// 1. Находим нужную группу номенклатуры в базе 1С 7.7
ГрТовара = БазаОле.CreateObject("Справочник.Номенклатура");
Если ГрТовара.НайтиПоНаименованию(СокрЛП(ГруппаТов.Наименование), 0, 1) > 0 Тогда
ТекЭл = ГрТовара.ТекущийЭлемент();
Иначе
Сообщить("Не найдена группа в базе 1С 7.7");
Возврат;
КонецЕсли;
// 2. Получаем строковое представление объекта в формате 1С 7.7
СтроковоеПредставление = БазаОле.ЗначениеВСтрокуВнутр(ТекЭл);
// Обязательно экранируем двойные кавычки внутри строки для корректной вставки в текст запроса
СтроковоеПредставлениеЭкранированное = СтрЗаменить(СтроковоеПредставление, """", """""");
// 3. Формируем запрос, где в условие динамически встраиваем функцию восстановления объекта
ТекстЗапроса = "Период с '" + Формат(Объект.Дата2, "ДФ=yyyyMMdd") + "';
|Товар = Регистр.ПартииТоваров.Товар;
|ОстатокКол = Регистр.ПартииТоваров.ОстатокТовара;
|Группировка Товар без групп;
|Функция КонКол = КонОст(ОстатокКол);
|Условие (Товар в ЗначениеИзСтрокиВнутр(""" + СтроковоеПредставлениеЭкранированная + """));";
Запрос = БазаОле.CreateObject("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
// Запрос выполнится успешно без каких-либо изменений в глобальном модуле 1С 7.7!
КонецЕсли;
Этот способ является наиболее универсальным, так как не требует монопольного доступа к базе 1С 7.7 для изменения конфигурации и позволяет «на лету» решать задачи интеграции любой сложности.
Мы рассмотрели четыре различных способа обхода ограничений контекста при выполнении OLE-запросов к 1С 7.7. Выбор конкретного метода зависит от архитектурных ограничений вашего проекта: