При разработке сложных систем интеграции между 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 еще не «подтянулся» — он идет вторым или третьим в очереди.
Это самый простой и эффективный способ, который рекомендует большинство специалистов. Инструкция 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.
Бывают ситуации, когда вам действительно нужно получить несколько результатов из одного запроса или вы не можете гарантировать отсутствие служебных сообщений. В этом случае мы можем использовать метод 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();
КонецЦикла;
Этот подход более универсален, так как он позволяет корректно обрабатывать сложные хранимые процедуры, которые возвращают несколько разных таблиц.
В сообщении №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 перед выгрузкой.
Чтобы ваша работа с прямыми запросами была стабильной и безопасной, придерживайтесь следующих правил:
NULL в 1С превращается в объект типа Null. Если вы попытаетесь записать его напрямую в реквизит справочника (например, строку), 1С может выдать ошибку. Используйте конструкцию ISNULL(Field, '') в самом SQL-запросе или проверяйте значение в 1С: НоваяСтрока.Поле = ?(Rs.Fields(0).Value = Null, "", Rs.Fields(0).Value);.Rs.CursorType = 0; (adOpenForwardOnly) и Rs.LockType = 1; (adLockReadOnly). Это минимизирует нагрузку на SQL-сервер и исключает блокировки таблиц.
Rs.Close();
Соединение.Close();
Rs = Неопределено;
Соединение = Неопределено;
o.ObjectGarNum like '% %442% %'). Это делает код уязвимым к SQL-инъекциям и ошибкам формата дат. Лучше использовать объект ADODB.Command с коллекцией Parameters.Подводя итог, можно сказать, что ошибка «Объект закрыт» — это не признак поломки соединения, а сигнал о том, что SQL Server вернул технический ответ раньше, чем основные данные. Использование SET NOCOUNT ON является "золотым стандартом" для решения этой проблемы в 99% случаев.