Как исправить ошибку «Операция не допускается, если объект закрыт» при прямом запросе к MS SQL из 1С

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

При разработке сложных систем интеграции между 1С:Предприятие и внешними базами данных MS SQL программисты часто используют технологию ADO (ActiveX Data Objects). Однако при выполнении запросов, содержащих инструкции INSERT, UPDATE, объявление переменных или создание временных таблиц, возникает специфическая ошибка: Произошла исключительная ситуация (ADODB.Recordset): Операция не допускается, если объект закрыт. При этом тот же самый запрос в SQL Server Management Studio выполняется успешно и возвращает данные.

В этой статье мы подробно разберем, почему возникает эта ситуация, и рассмотрим несколько способов ее решения, от простых настроек SQL до оптимизации кода на стороне 1С.

Почему возникает ошибка «Объект закрыт»?

Разберем механику взаимодействия 1С и SQL сервера. Когда мы отправляем запрос через объект ADODB.Recordset, драйвер ожидает получить набор данных. Однако в SQL Server по умолчанию включена отправка сообщений о количестве строк, затронутых каждой инструкцией в скрипте (например, «(1 row(s) affected)»).

Если ваш SQL-запрос содержит промежуточные действия (например, запись данных в табличную переменную, как в примере автора темы), SQL Server сначала отправляет уведомление о выполнении этой операции. ADO воспринимает это уведомление как первый результат запроса. Но поскольку это техническое сообщение, а не таблица с данными, такой Recordset оказывается «закрытым» для чтения строк. Когда код 1С пытается обратиться к свойству Rs.EOF, система выдает ошибку, так как основной набор данных с результатами SELECT еще не «подтянулся» — он идет вторым или третьим в очереди.

Решение №1: Использование инструкции SET NOCOUNT ON

Это самый простой и эффективный способ, который рекомендует большинство специалистов. Инструкция SET NOCOUNT ON запрещает SQL-серверу возвращать сообщения о количестве строк, затронутых операциями. В результате ADO получает только результирующий набор данных SELECT.

Проанализируем, куда именно нужно добавить эту команду. Она должна располагаться в самом начале вашего SQL-текста. Рассмотрим пример изменения кода:


// Добавляем SET NOCOUNT ON в начало запроса
ЗапросКSQL = "
|SET NOCOUNT ON;
|USE bn;
|DECLARE @t TABLE (tele_time DATETIME, long FLOAT, lat FLOAT);
|INSERT INTO @t
|SELECT tele_time, long, lat FROM bn.dbo.SYS_DEV_ArchiveData 
|WHERE ObjectID = 123;
|SELECT * FROM @t ORDER BY tele_time;";

Rs = Новый COMОбъект("ADODB.RecordSet");
Rs.Open(ЗапросКSQL, Соединение);

После добавления этой строки промежуточные сообщения будут подавлены, и объект Rs сразу будет содержать нужные данные, что позволит избежать ошибки при проверке Rs.EOF.

Решение №2: Перебор наборов данных через NextRecordSet()

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

Разберем алгоритм обхода наборов данных в цикле, который позволяет найти «живой» рекордсет:


// Выполняем команду через Command или напрямую через Recordset
Rs = Cmd.Execute();

Пока Rs <> Неопределено Цикл
    // Проверяем, открыт ли текущий набор данных и есть ли в нем поля
    Если Rs.State = 1 И Rs.Fields.Count > 0 Тогда
        // Обрабатываем данные, если мы нашли нужный набор
        Пока Не Rs.EOF Цикл
            НоваяСтрока = ТаблицаЗагрузки.Добавить();
            НоваяСтрока.Номер = Rs.Fields(0).Value;
            НоваяСтрока.Время = Rs.Fields(1).Value;
            Rs.MoveNext();
        КонецЦикла;
        // Если нам нужен только один (последний) результат, можно прервать цикл
        Прервать; 
    КонецЕсли;
    
    // Переходим к следующему набору (сообщению от SQL)
    Rs = Rs.NextRecordSet();
КонецЦикла;

Этот подход более универсален, так как он позволяет корректно обрабатывать сложные хранимые процедуры, которые возвращают несколько разных таблиц.

Решение №3: Оптимизация чтения данных через GetRows()

В сообщении №18 темы упоминается важный нюанс: работа с COM-объектами в цикле 1С (особенно обращение к Rs.Fields) происходит довольно медленно из-за накладных расходов на межпроцессное взаимодействие. Если вы получаете тысячи строк, обработка может затянуться.

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


Если Не Rs.EOF Тогда
    // Выгружаем все данные в массив
    ДанныеМассивом = Rs.GetRows();
    
    // Получаем границы массива (0 - колонки, 1 - строки)
    КоличествоКолонок = ДанныеМассивом.GetUpperBound(0);
    КоличествоСтрок = ДанныеМассивом.GetUpperBound(1);
    
    Для ИндексСтроки = 0 По КоличествоСтрок Цикл
        НоваяСтрока = ТаблицаЗагрузки.Добавить();
        // Данные в массиве лежат в формате [Колонка, Строка]
        НоваяСтрока.Номер = ДанныеМассивом.GetValue(0, ИндексСтроки);
        НоваяСтрока.Время = ДанныеМассивом.GetValue(3, ИндексСтроки);
    КонецЦикла;
КонецЕсли;

Важный момент: метод GetRows() после вызова автоматически перемещает курсор в конец рекордсета (EOF). Также помните, что пустой рекордсет вызовет ошибку при вызове этого метода, поэтому всегда проверяйте Rs.EOF и Rs.BOF перед выгрузкой.

Дополнительные рекомендации по работе с ADO

Чтобы ваша работа с прямыми запросами была стабильной и безопасной, придерживайтесь следующих правил:

  1. Обработка NULL: SQL-значение NULL в 1С превращается в объект типа Null. Если вы попытаетесь записать его напрямую в реквизит справочника (например, строку), 1С может выдать ошибку. Используйте конструкцию ISNULL(Field, '') в самом SQL-запросе или проверяйте значение в 1С: НоваяСтрока.Поле = ?(Rs.Fields(0).Value = Null, "", Rs.Fields(0).Value);.
  2. Типы курсоров: Для простого чтения данных (отчетов) используйте Rs.CursorType = 0; (adOpenForwardOnly) и Rs.LockType = 1; (adLockReadOnly). Это минимизирует нагрузку на SQL-сервер и исключает блокировки таблиц.
  3. Очистка памяти: После завершения работы обязательно закрывайте соединение и обнуляйте переменные, чтобы освободить лицензии и память:
    
    Rs.Close();
    Соединение.Close();
    Rs = Неопределено;
    Соединение = Неопределено;
    
  4. Безопасность: Избегайте прямой склейки строк для параметров запроса (как в o.ObjectGarNum like '% %442% %'). Это делает код уязвимым к SQL-инъекциям и ошибкам формата дат. Лучше использовать объект ADODB.Command с коллекцией Parameters.

Подводя итог, можно сказать, что ошибка «Объект закрыт» — это не признак поломки соединения, а сигнал о том, что SQL Server вернул технический ответ раньше, чем основные данные. Использование SET NOCOUNT ON является "золотым стандартом" для решения этой проблемы в 99% случаев.

← На главную