Как в 1С 7.7 определить, что документ заблокирован или открыт другим пользователем?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

Разработка и поддержка систем на платформе 1С:Предприятие 7.7 (знаменитых «клюшек») часто сопряжена с преодолением жестких ограничений встроенного языка. Одна из классических проблем, с которой сталкиваются программисты, — это отсутствие штатного и простого механизма, позволяющего узнать, какой именно пользователь в данный момент открыл документ на редактирование или заблокировал его.

В этой статье мы подробно разберем, почему возникает эта проблема на уровне архитектуры СУБД и операционной системы, проанализируем неэффективные решения, а затем детально изучим два наиболее надежных метода: штатный обходной путь для определения самого факта блокировки и профессиональное решение на базе внешних компонент для вывода имени конкретного пользователя.

Архитектурные причины проблемы: почему платформа молчит?

Чтобы понять, почему стандартные методы платформы не дают готового ответа, давайте заглянем во внутреннее устройство механизмов блокировки 1С 7.7. Поведение системы кардинально различается в зависимости от используемой версии базы данных (DBF или SQL):

  1. Файловый режим (DBF): При открытии формы документа на редактирование или при начале его изменения платформа пытается заблокировать определенный физический байт внутри таблицы базы данных или в специальном файле-заглушке 1sjourn.$lk. Блокировка накладывается через вызовы Windows API (функция LockFile). Операционная система знает, что байт заблокирован определенным сетевым клиентом (компьютером), но она абсолютно ничего не знает об именах пользователей внутри самой 1С.
  2. Клиент-серверный режим (SQL): В этой версии блокировки реализуются средствами СУБД (например, MS SQL Server) на уровне строк таблиц. База данных сопоставляет блокировку с системным процессом (SPID). Однако 1С 7.7 часто подключается к MS SQL под одной общей учетной записью администратора базы данных (например, sa). Выяснить изнутри SQL Server, какой именно пользователь 1С инициировал транзакцию, без использования сторонних инструментов практически невозможно.

Именно поэтому стандартный метод Блокировка() без параметров возвращает информацию только в рамках текущего сеанса и не может «дотянуться» до данных других активных пользователей.

Почему популярные «наивные» методы опасны на практике?

Часто разработчики пытаются решить задачу «в лоб», совершая критические ошибки, которые могут привести к нестабильной работе базы данных.

Вариант с записью реквизита в документ. Некоторые программисты создают в документе служебный реквизит (например, ВРаботе) и при открытии формы записывают документ с признаком «1», а при закрытии — сбрасывают в «0». Проанализируем, почему это плохой подход:

Вариант с анализом Журнала регистрации. Попытка программного парсинга файла журнала регистрации (файла с расширением .elf) при открытии каждого документа — это прямой путь к критическому падению производительности. В реальных базах размер журнала регистрации может достигать сотен мегабайт и даже гигабайт. Поиск текстовых строк в таком файле в режиме реального времени вызовет длительные зависания интерфейса у пользователей.

Решение №1: Штатный обходной путь (проверка факта блокировки)

Если перед нами стоит задача просто проверить, свободен ли документ для изменения (без необходимости вывода имени конкретного пользователя), мы можем использовать элегантный финт с транзакцией и принудительным вызовом метода Блокировка(1).

Давайте разберем, как устроен этот механизм. Попытка вызвать метод Блокировка(1) вне транзакции для уже занятого объекта вызовет аварийное завершение работы программы с ошибкой «Запись заблокирована». Однако, если мы обернем это действие в транзакцию, платформа позволит нам перехватить результат попытки блокировки.

Рассмотрим пример кода, реализующий этот алгоритм:


Функция ДокументЗаблокированДругимСеансом(ТекущийДок)
    
    // Создаем независимый объект для изоляции контекста
    ВспомДок = СоздатьОбъект("Документ." + ТекущийДок.Вид());
    Если ВспомДок.НайтиДокумент(ТекущийДок) = 0 Тогда
        // Если документ не найден, работать с ним нельзя
        Возврат 1;
    КонецЕсли;
    
    // Открываем транзакцию, чтобы перехватить попытку монопольного захвата
    НачатьТранзакцию();
    
    // Параметр (1) означает попытку установить блокировку объекта.
    // Если метод возвращает 1, объект успешно заблокирован нами.
    Если ВспомДок.Блокировка(1) = 1 Тогда
        
        // Блокировка установлена успешно, значит объект был свободен.
        // Снимаем блокировку, чтобы не мешать другим.
        ВспомДок.Блокировка(0);
        
        // Фиксируем транзакцию, закрывая сеанс проверки
        ЗафиксироватьТранзакцию();
        Возврат 0; // Документ свободен
        
    Иначе
        
        // Метод вернул 0 — это означает, что объект занят в другом сеансе
        ОтменитьТранзакцию();
        Возврат 1; // Документ заблокирован
        
    КонецЕсли;

КонецФункции

Этот метод абсолютно безопасен. Он не производит физической записи в базу данных, поэтому при любом аварийном сбое транзакция просто откатится на уровне СУБД, не оставив «зависших» флагов. Единственный минус этого подхода — мы узнаем только сам факт занятости документа, но не имя коллеги, который его открыл.

Решение №2: Профессиональный метод с использованием внешних компонент (FormEx и 1CPP)

Чтобы решить задачу полностью и выводить пользователям предупреждение вида: «Документ заблокирован пользователем: Иванов И.И. (Отдел продаж) в 11:35», в сообществе разработчиков 1С 7.7 принято использовать связку внешних компонент FormEx и 1CPP.

Рассмотрим подробно логику работы этой схемы:

  1. Внешняя компонента FormEx предоставляет разработчику доступ к глобальным предопределенным событиям, которые отсутствуют в стандартной платформе. Наиболее важные для нас: ПриНачалеБлокировкиОбъекта и ОбработкаБлокировкиОбъекта.
  2. При начале работы системы программно создается общая служебная таблица блокировок (это может быть таблица в SQL-базе или локальная таблица SQLite в случае DBF-версии базы данных).
  3. Когда Пользователь А открывает документ на редактирование, срабатывает системное событие ПриНачалеБлокировкиОбъекта(). В этот момент наша программа мгновенно делает быструю запись в служебную таблицу: сопоставляет уникальный идентификатор документа (ID) с именем текущего пользователя и временем захвата.
  4. Когда Пользователь Б пытается открыть этот же документ, платформа генерирует событие ОбработкаБлокировкиОбъекта(). Код перехватывает стандартную ошибку блокировки, делает быстрый запрос к служебной таблице по ID документа, извлекает имя Пользователя А и выводит красивое и информативное окно предупреждения.

Посмотрим на пример реализации обработчиков в глобальном модуле конфигурации:


// Пример обработчика события начала блокировки в Глобальном модуле
Процедура ПриНачалеБлокировкиОбъекта(КонтекстОбъекта)
    
    // Получаем внутренний идентификатор объекта
    ИдентификаторОбъекта = ЗначениеВСтрокуВнутр(КонтекстОбъекта);
    ИмяПользователя = ИмяПользователя();
    ВремяБлокировки = ТекущееВремя();
    
    // Записываем данные в служебную SQL-таблицу блокировок
    // (Ниже представлен схематичный вызов через компоненту 1CPP)
    ЗапросSQL = СоздатьОбъект("ODBCRecordset");
    ТекстЗапроса = "
    |INSERT INTO СлужебнаяТаблицаБлокировок (ObjectID, UserName, LockTime)
    |VALUES ('" + ИдентификаторОбъекта + "', '" + ИмяПользователя + "', '" + ВремяБлокировки + "')
    |";
    ЗапросSQL.Выполнить(ТекстЗапроса);

КонецПроцедуры

// Пример обработчика перехвата ошибки блокировки другим пользователем
Процедура ОбработкаБлокировкиОбъекта(КонтекстОбъекта, ФлагПовторить, ТекстПредупреждения)
    
    ИдентификаторОбъекта = ЗначениеВСтрокуВнутр(КонтекстОбъекта);
    
    // Ищем информацию о блокировщике в базе данных
    ЗапросSQL = СоздатьОбъект("ODBCRecordset");
    ТекстЗапроса = "
    |SELECT UserName, LockTime FROM СлужебнаяТаблицаБлокировок 
    |WHERE ObjectID = '" + ИдентификаторОбъекта + "'
    |";
    
    ТаблицаРезультата = ЗапросSQL.ВыполнитьИнструкцию(ТекстЗапроса);
    
    Если ТаблицаРезультата.КоличествоСтрок() > 0 Тогда
        ИмяБлокировщика = ТаблицаРезультата.ПолучитьЗначение(1, "UserName");
        ВремяБлокировки = ТаблицаРезультата.ПолучитьЗначение(1, "LockTime");
        
        ТекстПредупреждения = "Внимание! Данный документ уже редактирует пользователь: " 
                            + ИмяБлокировщика + " (захвачен в " + ВремяБлокировки + ")";
    Иначе
        ТекстПредупреждения = "Объект заблокирован другим сеансом 1С.";
    КонецЕсли;
    
    // Отменяем стандартное системное сообщение платформы
    ФлагПовторить = 0;

КонецПроцедуры

При использовании этого метода крайне важно предусмотреть механизм очистки «зависших» блокировок. Для этого при начале работы системы (в процедуре ПриНачалеРаботыСистемы) рекомендуется выполнять полную очистку записей текущего пользователя в таблице блокировок, а также периодически удалять записи, время создания которых превышает разумные пределы (например, более 12 часов).

Резюме и рекомендации

Выбор оптимального метода зависит от критичности бизнес-процессов и архитектуры вашей информационной системы:

← На главную