При интеграции 1С со сторонними базами данных под управлением Microsoft SQL Server через интерфейс ADO часто возникает задача: вставить новую строку в таблицу и сразу же прочитать присвоенный ей автоинкрементный идентификатор (первичный ключ). Однако при выполнении составного запроса вида INSERT ...; SELECT SCOPE_IDENTITY(); разработчики сталкиваются с исключением платформы: «Произошла исключительная ситуация (ADODB.Recordset): Operation is not allowed when the object is closed».
Разберем природу возникновения этой ошибки и детально рассмотрим несколько надежных способов ее решения.
Проанализируем, что происходит на стороне СУБД и провайдера ADO при выполнении пакетного запроса:
INSERT, она формирует служебное системное сообщение о количестве затронутых строк (токен DoneInProc).ADODB.Recordset воспринимает этот служебный ответ как первый результирующий набор данных. Поскольку операция вставки не возвращает строк таблицы, этот набор оказывается закрытым (свойство State = 0 / adStateClosed).Open() пытается обратиться к свойствам или полям объекта (например, проверить RS.EOF или прочитать поле), ADO выдает ошибку о невозможности выполнения операции с закрытым объектом. Второй запрос SELECT SCOPE_IDENTITY() при этом попросту еще не прочитан.Самый простой и быстрый способ решить проблему — отключить отправку служебных сообщений о количестве затронутых строк от 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().
Если по каким-либо причинам вы не можете изменять текст запроса и отключать подсчет строк, можно последовательно переключить указатель 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).
В современных версиях 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();
В объектной модели 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С или специализированные компоненты/веб-сервисы.