Как правильно выполнить пакетный SQL-запрос через ADODB.Recordset в 1С и получить сгенерированный ID?

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

При интеграции 1С со сторонними базами данных под управлением Microsoft SQL Server через интерфейс ADO часто возникает задача: вставить новую строку в таблицу и сразу же прочитать присвоенный ей автоинкрементный идентификатор (первичный ключ). Однако при выполнении составного запроса вида INSERT ...; SELECT SCOPE_IDENTITY(); разработчики сталкиваются с исключением платформы: «Произошла исключительная ситуация (ADODB.Recordset): Operation is not allowed when the object is closed».

Разберем природу возникновения этой ошибки и детально рассмотрим несколько надежных способов ее решения.

Выясним причину возникновения ошибки

Проанализируем, что происходит на стороне СУБД и провайдера ADO при выполнении пакетного запроса:

  1. Когда СУБД выполняет операцию INSERT, она формирует служебное системное сообщение о количестве затронутых строк (токен DoneInProc).
  2. Объект ADODB.Recordset воспринимает этот служебный ответ как первый результирующий набор данных. Поскольку операция вставки не возвращает строк таблицы, этот набор оказывается закрытым (свойство State = 0 / adStateClosed).
  3. Когда код 1С сразу после вызова метода Open() пытается обратиться к свойствам или полям объекта (например, проверить RS.EOF или прочитать поле), ADO выдает ошибку о невозможности выполнения операции с закрытым объектом. Второй запрос SELECT SCOPE_IDENTITY() при этом попросту еще не прочитан.
  4. Аналогичная проблема возникает, если на таблице настроены триггеры: скрытые DML-операции внутри триггеров возвращают собственные счетчики строк.

Способ 1. Использование директивы SET NOCOUNT ON (Основное решение)

Самый простой и быстрый способ решить проблему — отключить отправку служебных сообщений о количестве затронутых строк от SQL Server клиенту. Для этого в начало текста SQL-пакета добавляется команда SET NOCOUNT ON;.

Посмотрим на пример правильного кода в 1С:


Соединение = Новый COMОбъект("ADODB.Connection");
Соединение.Open(СтрокаПодключения);

ТекстЗапроса = "
|SET NOCOUNT ON;
|INSERT INTO dbo.MyTable (Name) VALUES ('Тестовая запись');
|SELECT SCOPE_IDENTITY() AS NewID;
|";

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

Если RS.State = 1 Тогда // 1 = adStateOpen
    Если Не RS.EOF() Тогда
        НовыйИдентификатор = RS.Fields("NewID").Value;
        Сообщить("Сгенерирован ID: " + Строка(НовыйИдентификатор));
    КонецЕсли;
    RS.Close();
КонецЕсли;

Соединение.Close();

В этом случае провайдер ADO пропускает служебные данные вставки и сразу связывает объект Recordset с результатом выборки SELECT SCOPE_IDENTITY().

Способ 2. Использование метода NextRecordset

Если по каким-либо причинам вы не можете изменять текст запроса и отключать подсчет строк, можно последовательно переключить указатель ADO на следующий набор данных с помощью метода NextRecordset().

Рассмотрим, как обойти закрытые наборы записей по шагам:


ТекстЗапроса = "
|INSERT INTO dbo.MyTable (Name) VALUES ('Тестовая запись');
|SELECT SCOPE_IDENTITY() AS NewID;
|";

RS = Новый COMОбъект("ADODB.Recordset");
// Для работы NextRecordset важно использовать серверный курсор
RS.CursorLocation = 2; // 2 = adUseServer
RS.Open(ТекстЗапроса, Соединение);

// Пропускаем закрытые служебные наборы данных
Пока RS.State = 0 И Не RS = Неопределено Цикл
    RS = RS.NextRecordset();
КонецЦикла;

Если RS <> Неопределено И RS.State = 1 Тогда
    Если Не RS.EOF() Тогда
        НовыйИдентификатор = RS.Fields("NewID").Value;
        Сообщить("Сгенерирован ID: " + Строка(НовыйИдентификатор));
    КонецЕсли;
    RS.Close();
КонецЕсли;

Важное ограничение: метод NextRecordset корректно поддерживается большинством провайдеров только при использовании серверного курсора (свойство CursorLocation = 2 / adUseServer).

Способ 3. Конструкция OUTPUT в MS SQL Server

В современных версиях MS SQL Server оператор INSERT поддерживает предложение OUTPUT. Это позволяет объединить вставку и получение данных в единый атомарный оператор без вызова SCOPE_IDENTITY():


ТекстЗапроса = "
|INSERT INTO dbo.MyTable (Name) 
|OUTPUT INSERTED.ID AS NewID 
|VALUES ('Тестовая запись');
|";

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

Если Не RS.EOF() Тогда
    НовыйИдентификатор = RS.Fields("NewID").Value;
    Сообщить("Сгенерирован ID: " + Строка(НовыйИдентификатор));
КонецЕсли;
RS.Close();

Способ 4. Добавление записи методами объекта Recordset (AddNew / Update)

В объектной модели ADO можно вообще отказаться от написания прямого SQL-запроса INSERT, воспользовавшись встроенными методами добавления данных. Чтобы не считывать всю таблицу целиком, выборку инициализируют с заведомо ложным условием (например, WHERE ID IS NULL или WHERE 1=0), получая лишь структуру полей.

Разберем этот вариант на примере:


RS = Новый COMОбъект("ADODB.Recordset");
// Настраиваем клиентский курсор и оптимистичную блокировку для редактирования
RS.CursorLocation = 3; // 3 = adUseClient
RS.LockType = 3;       // 3 = adLockOptimistic

ТекстЗапроса = "SELECT ID, Name FROM dbo.MyTable WHERE 1 = 0";
RS.Open(ТекстЗапроса, Соединение);

RS.AddNew();
RS.Fields("Name").Value = "Новая запись через AddNew";
RS.Update();

// После вызова Update поле автоинкремента заполняется СУБД автоматически
НовыйИдентификатор = RS.Fields("ID").Value;
Сообщить("Получен ID: " + Строка(НовыйИдентификатор));

RS.Close();

Такой подход напоминает работу с объектами НаборЗаписей в 1С и исключает ошибки ручного форматирования SQL-запросов.

Важное замечание о среде выполнения

Обратите внимание, что создание и работа с COMОбъект("ADODB.*") поддерживается платформой 1С:Предприятие только в операционных системах Windows. Если ваш сервер 1С работает под управлением Linux, использование ADO приведет к ошибке отсутствия COM-технологии. В таких сценариях рекомендуется использовать прямое подключение через внешние источники данных 1С или специализированные компоненты/веб-сервисы.

← На главную