При интеграции систем на платформе 1С с внешними базами данных MS SQL Server или при оптимизации параллельной работы пользователей (особенно в исторических конфигурациях, таких как 1С:Предприятие 7.7) разработчики часто сталкиваются с необходимостью явного управления блокировками на уровне СУБД. Одним из наиболее гибких инструментов для этого является встроенная процедура MS SQL Server sp_getapplock, которая позволяет устанавливать именованные блокировки на произвольные ресурсы приложения.
Однако при попытке вызвать эту системную процедуру из кода 1С через COM-объект ADODB.Command разработчики часто сталкиваются с неочевидным поведением драйверов и СУБД, получая ошибку: "Процедура или функция sp_getapplock ожидает параметр @LockMode, который не был указан". В этой статье мы подробно разберем причины этой проблемы, проанализируем поведение OLE DB провайдера и выработаем надежное решение для управления блокировками СУБД из кода 1С.
Для начала разберем типичную ситуацию. Разработчик пытается использовать объектную модель 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 часто превращается в "шаманство" с порядком параметров и типами данных.
Прежде чем перейти к рабочему решению, кратко рассмотрим ключевые параметры процедуры СУБД, которые нам потребуются:
SC5197 для справочника 1С) или любой уникальный текстовый ключ.Exclusive (монопольный режим, запрещающий любые другие блокировки) или Shared (разделяемый режим для чтения).Transaction (по умолчанию) или Session. Это важнейший параметр, определяющий время жизни нашей блокировки.-1 (бесконечное ожидание).dbo или public).Чтобы обойти ограничения драйверов СУБД и гарантировать стабильную работу блокировок без ошибок сопоставления типов, самым надежным решением является формирование явного 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:
Если мы устанавливаем блокировку с владельцем по умолчанию (@LockOwner = 'Transaction'), то для корректной работы процедуры обязательно должна быть открыта транзакция (команда BEGIN TRANSACTION в SQL-коде).
При таком подходе вам не требуется вручную вызывать процедуру разблокировки. Как только транзакция СУБД будет зафиксирована (командой COMMIT TRANSACTION) или откачена (командой ROLLBACK TRANSACTION), SQL-сервер автоматически и мгновенно снимет все ассоциированные с ней блокировки приложения. Это самый безопасный метод, защищающий от "зависания" блокировок при аварийном завершении работы кода 1С.
Если блокировка устанавливается на уровне сессии (@LockOwner = 'Session'), она не зависит от транзакций и будет удерживаться активной до тех пор, пока живо физическое подключение к СУБД, либо пока мы явно не снимем ее.
Для ручного снятия блокировки в рамках той же сессии необходимо выполнить следующий код через тот же объект соединения в 1С:
ТекстРазблокировки = "EXEC sp_releaseapplock @Resource = 'SC5197', @LockOwner = 'Session', @DbPrincipal = 'dbo';";
GlobalCommand.CommandText = ТекстРазблокировки;
GlobalCommand.Execute();
Проанализируем важную особенность работы механизмов 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-сервер немедленно прервет соединение, откатит все незавершенные транзакции этой сессии и полностью освободит заблокированные прикладные ресурсы.