Как правильно вызвать sp_getapplock из 1С через ADO и избежать ошибок с параметрами?

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

При интеграции систем на платформе 1С с внешними базами данных MS SQL Server или при оптимизации параллельной работы пользователей (особенно в исторических конфигурациях, таких как 1С:Предприятие 7.7) разработчики часто сталкиваются с необходимостью явного управления блокировками на уровне СУБД. Одним из наиболее гибких инструментов для этого является встроенная процедура MS SQL Server sp_getapplock, которая позволяет устанавливать именованные блокировки на произвольные ресурсы приложения.

Однако при попытке вызвать эту системную процедуру из кода 1С через COM-объект ADODB.Command разработчики часто сталкиваются с неочевидным поведением драйверов и СУБД, получая ошибку: "Процедура или функция sp_getapplock ожидает параметр @LockMode, который не был указан". В этой статье мы подробно разберем причины этой проблемы, проанализируем поведение OLE DB провайдера и выработаем надежное решение для управления блокировками СУБД из кода 1С.

Почему возникает ошибка сопоставления параметров в ADODB?

Для начала разберем типичную ситуацию. Разработчик пытается использовать объектную модель ADO для вызова процедуры sp_getapplock, объявляя параметры через метод CreateParameter. На первый взгляд код выглядит логичным, однако СУБД возвращает ошибку нехватки параметров.

Выясним причину такого поведения. По умолчанию, когда мы настраиваем объект GlobalCommand с типом команды CommandType = 4 (хранимая процедура) и наполняем коллекцию Parameters, драйвер СУБД (например, устаревший, но все еще часто используемый SQLOLEDB) сопоставляет параметры позиционно (по индексу), а не по именам.

Давайте посмотрим на сигнатуру процедуры sp_getapplock в MS SQL Server:


sp_getapplock [ @Resource = ] 'resource_name' ,  
     [ @LockMode = ] 'lock_mode'  
     [ , [ @LockOwner = ] 'lock_owner' ]  
     [ , [ @LockTimeout = ] 'value' ]  
     [ , [ @DbPrincipal = ] 'database_principal' ]

Если мы передаем в коллекцию параметров ADO только один параметр, надеясь, что свойство namedParameters = "true" заставит драйвер сопоставить его по имени @LockMode, происходит следующее: драйвер берет этот параметр и отправляет его как первый позиционный аргумент процедуры. В итоге процедура считает, что аргумент со значением "exclusive" — это имя ресурса (параметр @Resource), а обязательный второй параметр @LockMode остается пустым. Именно это и вызывает ошибку СУБД.

Ситуация усугубляется тем, что старые OLE DB провайдеры некорректно считывают метаданные системных процедур из базы данных master, из-за чего механизм сопоставления именованных параметров дает сбой. Поэтому попытка передать параметры через классическую коллекцию ADO часто превращается в "шаманство" с порядком параметров и типами данных.

Анатомия параметров sp_getapplock

Прежде чем перейти к рабочему решению, кратко рассмотрим ключевые параметры процедуры СУБД, которые нам потребуются:

  1. @Resource — строковый идентификатор блокируемого ресурса. Это может быть имя таблицы СУБД (например, SC5197 для справочника 1С) или любой уникальный текстовый ключ.
  2. @LockMode — режим блокировки. Чаще всего используются значения Exclusive (монопольный режим, запрещающий любые другие блокировки) или Shared (разделяемый режим для чтения).
  3. @LockOwner — владелец блокировки. Может принимать значения Transaction (по умолчанию) или Session. Это важнейший параметр, определяющий время жизни нашей блокировки.
  4. @LockTimeout — время ожидания снятия чужой блокировки в миллисекундах. По умолчанию равен -1 (бесконечное ожидание).
  5. @DbPrincipal — роль или пользователь базы данных, имеющий права на блокировку (обычно используется значение dbo или public).

Решение проблемы: Отказ от параметров ADO в пользу прямого T-SQL текста

Чтобы обойти ограничения драйверов СУБД и гарантировать стабильную работу блокировок без ошибок сопоставления типов, самым надежным решением является формирование явного T-SQL кода и его запуск через метод Execute. В этом случае мы полностью контролируем синтаксис передачи параметров на уровне SQL-сервера.

Рассмотрим правильный шаблон реализации вызова блокировки из 1С. Обратите внимание на необходимость оборачивания вызова в явную транзакцию СУБД:


пСтрСоед = "driver={SQL Server};server=test1c;uid=Login;pwd=Password;Database=SMP_2018;";
GlobalConnection = Новый COMОбъект("ADODB.Connection"); 
GlobalConnection.Provider = "SQLOLEDB"; 
GlobalConnection.ConnectionTimeout = 15; 
GlobalConnection.Open(пСтрСоед);

// Формируем текст запроса с явным открытием транзакции
СтрокаЗапроса = 
"BEGIN TRANSACTION;
|DECLARE @res INT;
|EXEC @res = sp_getapplock 
|    @Resource = 'SC5197', 
|    @LockMode = 'Exclusive', 
|    @LockOwner = 'Transaction', 
|    @DbPrincipal = 'dbo';
|SELECT @res AS Result;";

GlobalCommand = Новый COMОбъект("ADODB.Command"); 
GlobalCommand.ActiveConnection = GlobalConnection; 
GlobalCommand.CommandType = 1; // adCmdText
GlobalCommand.CommandText = СтрокаЗапроса;

// Выполняем команду и получаем результат
RecordSet = GlobalCommand.Execute();
Если Не RecordSet.EOF Тогда
    РезультатБлокировки = RecordSet.Fields("Result").Value;
    // Результат >= 0 означает успешное наложение блокировки
    Если РезультатБлокировки < 0 Тогда
        ВызватьИсключение "Не удалось заблокировать ресурс! Код ошибки SQL: " + РезультатБлокировки;
    КонецЕсли;
КонецЕсли;

В данном примере мы явно указываем имена параметров внутри самого T-SQL кода, что полностью исключает ошибки интерпретации параметров на стороне драйвера ADO.

Управление жизненным циклом блокировки и ее освобождение

Разберем важнейший вопрос, который часто упускают разработчики: как и когда снимается установленная блокировка?

Попытка выполнить процедуру освобождения блокировки sp_releaseapplock из другого подключения (например, вручную через среду SQL Server Management Studio) для сессии, открытой из 1С, завершится ошибкой. Блокировки жестко привязаны к контексту выполнения. Способ освобождения ресурса зависит от параметра @LockOwner:

Вариант А: Использование владельца 'Transaction' (рекомендуется)

Если мы устанавливаем блокировку с владельцем по умолчанию (@LockOwner = 'Transaction'), то для корректной работы процедуры обязательно должна быть открыта транзакция (команда BEGIN TRANSACTION в SQL-коде).

При таком подходе вам не требуется вручную вызывать процедуру разблокировки. Как только транзакция СУБД будет зафиксирована (командой COMMIT TRANSACTION) или откачена (командой ROLLBACK TRANSACTION), SQL-сервер автоматически и мгновенно снимет все ассоциированные с ней блокировки приложения. Это самый безопасный метод, защищающий от "зависания" блокировок при аварийном завершении работы кода 1С.

Вариант Б: Использование владельца 'Session'

Если блокировка устанавливается на уровне сессии (@LockOwner = 'Session'), она не зависит от транзакций и будет удерживаться активной до тех пор, пока живо физическое подключение к СУБД, либо пока мы явно не снимем ее.

Для ручного снятия блокировки в рамках той же сессии необходимо выполнить следующий код через тот же объект соединения в 1С:


ТекстРазблокировки = "EXEC sp_releaseapplock @Resource = 'SC5197', @LockOwner = 'Session', @DbPrincipal = 'dbo';";
GlobalCommand.CommandText = ТекстРазблокировки;
GlobalCommand.Execute();

Опасности пула соединений (Connection Pooling) и борьба с зависшими блокировками

Проанализируем важную особенность работы механизмов ADODB в операционных системах Windows. По умолчанию для оптимизации производительности включен пул соединений СУБД. Это означает, что когда вы вызываете метод GlobalConnection.Close() в 1С, физическое соединение с SQL-сервером не разрывается, а возвращается в пул в режиме ожидания.

Если при этом вы использовали блокировку уровня сессии (@LockOwner = 'Session') и в коде 1С произошла критическая ошибка (из-за чего метод sp_releaseapplock не был вызван), блокировка продолжит удерживаться сервером СУБД внутри "спящего" соединения из пула. Это заблокирует работу других пользователей на длительное время.

Для предотвращения таких ситуаций всегда оборачивайте логику работы с внешними транзакциями в блоки обработки ошибок 1С:


Попытка
    GlobalConnection.Open();
    // Начинаем транзакцию СУБД и ставим блокировку
    GlobalCommand.CommandText = "BEGIN TRANSACTION; EXEC sp_getapplock 'SC5197', 'Exclusive';";
    GlobalCommand.Execute();
    
    // Выполняем полезную работу в 1С
    // ...
    
    // Фиксируем транзакцию и автоматически освобождаем ресурс
    GlobalCommand.CommandText = "COMMIT TRANSACTION;";
    GlobalCommand.Execute();
Исключение
    // В случае любой ошибки обязательно откатываем транзакцию СУБД для освобождения блокировок
    Если GlobalConnection.State = 1 Тогда
        GlobalCommand.CommandText = "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;";
        GlobalCommand.Execute();
        GlobalConnection.Close();
    КонецЕсли;
    ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;

Как принудительно снять зависшую блокировку?

Если аварийная ситуация все же произошла и ресурс оказался заблокирован "зависшей" сессией, администратор СУБД может принудительно освободить его. Для этого выполним в SQL Server Management Studio (SSMS) следующий алгоритм:

1. Найдем идентификатор сессии (SPID), которая удерживает блокировку нашего прикладного ресурса:


SELECT request_session_id, resource_description 
FROM sys.dm_tran_locks 
WHERE resource_type = 'APPLICATION' 
  AND resource_description LIKE '%SC5197%';

2. Используя полученный номер сессии (например, 74), принудительно завершим процесс командой сброса сессии:

KILL 74;

После выполнения команды KILL SQL-сервер немедленно прервет соединение, откатит все незавершенные транзакции этой сессии и полностью освободит заблокированные прикладные ресурсы.

← На главную