При разработке прямых запросов к базе данных SQL в 1С:Предприятии 7.7 с использованием внешней компоненты 1С++ разработчики часто сталкиваются с ошибкой ODBC Connection is busy with results for another hstmt. Она возникает в процессе построчного обхода выборки через объект ODBCRecordSet при попытке прочитать типизированные поля или получить ссылки на объекты 1С.
Разберем природу возникновения этой ошибки и детально рассмотрим способы её корректного устранения.
Проанализируем механизм работы компоненты. По умолчанию объект ODBCRecordSet использует текущее активное подключение платформы к Microsoft SQL Server. При открытии выборки методом Открыть() создается последовательный серверный курсор (SQL_CURSOR_FORWARD_ONLY).
В такой конфигурации дескриптор соединения с базой (HDBC) остается полностью занят активной выборкой (HSTMT) до тех пор, пока все записи не будут прочитаны или пока не будет вызван метод rs.Закрыть().
Если внутри цикла обхода выборки мы пытаемся:
[$Справочник.МестаХранения];MDW.ЗначениеИзСтрокиБД();Платформа пытается отправить дополнительный SQL-запрос к серверу через то же самое соединение, которое в этот момент уже занято обработкой открытого курсора. ODBC-драйвер блокирует эту операцию и возвращает ошибку Connection is busy with results for another hstmt.
Чтобы освободить поток подключения сразу после открытия набора данных, настроим свойства курсора перед вызовом метода Открыть(). Для этого используем метод УстСвойстваКурсора() с параметром статического курсора (SQL_CURSOR_STATIC = 2).
Посмотрим на пример правильной настройки:
rs = СоздатьОбъект("ODBCRecordSet");
// Устанавливаем статический тип курсора (2 - SQL_CURSOR_STATIC)
rs.УстСвойстваКурсора(2, 0, 0, 0);
Если rs.Открыть(ТекстЗапроса) = 1 Тогда
// Задаем типы колонок строго после успешного открытия курсора
rs.УстТипыКолонок1С("Справочник.МПЗ,Справочник.Контрагенты,Справочник.МестаХранения,Число,Число");
rs.ВНачало();
Пока rs.Конец() = 0 Цикл
Сообщить("МПЗ: " + rs.ПолучитьЗначение(1));
Сообщить("Остаток: " + rs.ПолучитьЗначение(4));
rs.След();
КонецЦикла;
rs.Закрыть();
Иначе
Сообщить("Ошибка открытия выборки: " + rs.ПолучитьОписаниеОшибки());
КонецЕсли;
При статическом курсоре весь результат запроса сразу кэшируется на стороне клиента, после чего дескриптор подключения освобождается, позволяя платформе беспрепятственно выполнять сопутствующие запросы к базе данных.
Если объем данных умещается в оперативной памяти, наиболее надежным и производительным решением является полный отказ от построчного серверного курсора в пользу пакетной выгрузки в ТаблицуЗначений.
Рассмотрим, как выполнить запрос с типизацией через метод ВыполнитьИнструкцию():
rs = СоздатьОбъект("ODBCRecordSet");
ТекстЗапроса = "
|SELECT
| Спр.ID as [МПЗ $Справочник.МПЗ],
| Спр.DESCR as Наименование,
| Рег.ОстатокТовара as [Остаток #Число.15.2]
|FROM
| $Справочник.МПЗ as Спр
|";
ТЗ = rs.ВыполнитьИнструкцию(ТекстЗапроса);
Если ПустоеЗначение(ТЗ) = 0 Тогда
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
Сообщить(Строка(ТЗ.МПЗ) + " - " + ТЗ.Остаток);
КонецЦикла;
КонецЕсли;
В данном сценарии типизация выполняется на этапе формирования таблицы значений, а соединение с сервером закрывается мгновенно, что полностью исключает конфликты дескрипторов.
Если требуется непрерывный потоковый обход выборки очень большого объема без загрузки всех строк в память, создадим отдельную физическую сессию к базе данных с помощью объекта ODBCDataBase.
Разберем по шагам создание отдельного подключения:
ODBCDataBase и подключаемся через строку соединения или DSN (не используем метод Attach1C(), так как он задействует общий дескриптор платформы);rs.УстБД(БазаДанных);
БД = СоздатьОбъект("ODBCDataBase");
Если БД.Соединить("DRIVER={SQL Server};SERVER=MyServer;UID=sa;PWD=secret;DATABASE=MyBase1C;") = 1 Тогда
rs = СоздатьОбъект("ODBCRecordSet");
rs.УстБД(БД);
Если rs.Открыть(ТекстЗапроса) = 1 Тогда
Пока rs.След() = 1 Цикл
// Теперь запросы платформы и чтение выборки идут по разным соединениям
Ид = rs.ПолучитьЗначение(1);
Объект = MDW.ЗначениеИзСтрокиБД(11, "Контрагенты", Ид);
Сообщить("Элемент: " + Объект);
КонецЦикла;
rs.Закрыть();
КонецЕсли;
БД.Отсоединить();
КонецЕсли;
Обратим внимание на ошибку "Данный метод возможно вызывать только после подготовки запроса или открытия курсора", которая часто возникает при попытке типизации полей. Чтобы избежать этой ошибки, соблюдаем следующие требования:
УстТипыКолонок1С() строго после успешного выполнения метода rs.Открыть() (когда rs.ВыборкаОткрыта() = 1) или после вызова rs.Подготовить();SELECT запроса;