Как избавиться от ошибки "Данные были изменены или удалены другим пользователем" в 1С

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

Приветствуем вас, уважаемые коллеги! Каждый разработчик на платформе «1С:Предприятие 8» рано или поздно сталкивается с системным сообщением: «Данные были изменены или удалены другим пользователем». Эта ошибка способна поставить в тупик начинающего специалиста, особенно когда точно известно, что в системе в этот момент работает только один человек. Давайте вместе подробно разберем природу этого явления, проанализируем механизм оптимистической блокировки и выясним, как правильно писать код, чтобы навсегда предотвратить появление этой проблемы.

Почему возникает конфликт версий данных?

Для начала проанализируем ситуацию на физическом уровне базы данных. В платформе «1С:Предприятие 8» для всех ссылочных типов (документы, справочники, бизнес-процессы и т. д.) применяется механизм оптимистической блокировки. Это означает, что система не блокирует запись в таблице СУБД на все время, пока пользователь держит открытой форму и редактирует реквизиты. Вместо этого используется скрытый системный реквизит ВерсияДанных (в СУБД MS SQL Server это реализуется через тип данных rowversion или timestamp).

Рассмотрим стандартный жизненный цикл работы с формой документа:

  1. Пользователь открывает форму существующего документа. В этот момент платформа считывает данные из базы данных, включая текущую ВерсияДанных (например, «Версия 1»), и загружает их в память управляемой формы в объект типа ДанныеФормыСтруктура (наш реквизит Объект).
  2. Пока форма открыта на экране, на сервере запускается фоновый процесс или выполняется программный код, который получает этот же документ из базы данных через объектную технику: ДокОбъект = Ссылка.ПолучитьОбъект().
  3. Этот программный код вносит изменения в документ напрямую в базе данных и вызывает метод ДокОбъект.Записать(). В этот момент СУБД записывает новые значения и автоматически увеличивает версию данных в таблице (теперь там записана «Версия 2»).
  4. Пользователь нажимает кнопку «Записать и закрыть» на форме (или программно вызывается метод Форма.Записать()). Платформа отправляет данные формы на сервер. Перед фиксацией транзакции она сверяет версию данных, которая хранилась в памяти формы («Версия 1»), с актуальной версией данных в СУБД («Версия 2»).
  5. Поскольку версии не совпадают, платформа понимает, что данные в базе изменились с момента открытия формы. Срабатывает защитный механизм оптимистической блокировки, транзакция откатывается, и пользователь получает ошибку: «Данные были изменены или удалены другим пользователем».

Именно эта ситуация произошла у автора темы на форуме. Код сначала считывал объект из базы данных через ПолучитьОбъект(), перезаписывал его в базе, а затем форма пыталась выполнить штатную запись устаревшей версии данных.

Способ 1. Модификация данных формы (Рекомендуемый подход)

Выясним причину, по которой разработчики часто совершают эту ошибку. Главное заблуждение заключается в мысли: «Чтобы изменить данные в табличной части, мне обязательно нужно получить объект документа из базы данных». В управляемом приложении это абсолютно не так!

Для изменения данных, отображаемых на форме, нет никакой необходимости обращаться к базе данных через транзакции записи. Реквизит формы Объект и его табличные части (например, Объект.Товары) имеют специальные типы данных — ДанныеФормыСтруктура и ДанныеФормыКоллекция соответственно. Мы имеем возможность модифицировать их напрямую в контексте формы на сервере, а платформа сама запишет измененные данные в базу при стандартном сохранении формы.

Рассмотрим, как правильно переписать исходный проблемный алгоритм без использования ПолучитьОбъект():


&НаСервере
Процедура ОбновитьЦеныСравненияНаФорме(РезультатЗапроса)

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

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

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

Способ 2. Использование связки РеквизитФормыВЗначение и ЗначениеВРеквизитФормы

Иногда возникают ситуации, когда для расчета параметров табличной части или проверки реквизитов нам жизненно необходим полноценный прикладной объект типа ДокументОбъект.ЗаказПоставщику. Это требуется, например, если расчет цен завязан на сложные экспортные процедуры из модуля этого объекта, которые невозможно вызвать из контекста формы.

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

  1. Мы преобразуем реквизит формы Объект во временный прикладной объект в памяти сервера с помощью функции РеквизитФормыВЗначение("Объект").
  2. Выполняем все необходимые расчеты, заполнения и манипуляции с полученным объектом (включая вызов процедур модуля объекта).
  3. Важно: мы ни в коем случае НЕ вызываем метод Записать() у этого временного объекта!
  4. Мы возвращаем измененный в памяти объект обратно в реквизит формы с помощью процедуры ЗначениеВРеквизитФормы(ДокОбъект, "Объект"). Платформа при этом автоматически обновляет данные на форме и синхронизирует внутреннюю версию данных.

Посмотрим на пример правильной реализации этого паттерна проектирования:


&НаСервере
Процедура ОбновитьДанныеЧерезПрикладнойОбъект(РезультатЗапроса)

    Если Не РезультатЗапроса.Пустой() Тогда
        
        // Преобразуем данные формы во временный объект в памяти сервера
        ДокОбъект = РеквизитФормыВЗначение("Объект");
        
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            
            Отбор = Новый Структура;
            Отбор.Вставить("Номенклатура", Выборка.Номенклатура);
            
            НайденныеСтроки = ДокОбъект.Товары.НайтиСтроки(Отбор);
            
            Для Каждого СтрокаТовара Из НайденныеСтроки Цикл
                СтрокаТовара.ЦенаСравнения  = Выборка.Цена;
                СтрокаТовара.СуммаСравнения = СтрокаТовара.Количество * СтрокаТовара.ЦенаСравнения;
            КонецЦикла;
            
        КонецЦикла;
        
        // Здесь мы МОЖЕМ вызвать экспортный метод модуля объекта, если это нужно:
        // ДокОбъект.ПересчитатьВсеСуммыДокумента();
        
        // Возвращаем измененный объект в контекст управляемой формы
        ЗначениеВРеквизитФормы(ДокОбъект, "Объект");
        
        // Метод ДокОбъект.Записать() НЕ ВЫЗЫВАЕТСЯ! Запись произойдет позже средствами формы.
        
    КонецЕсли;

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

Способ 3. Перечитывание данных формы методом Прочитать()

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

Если в базе данных объект уже был перезаписан и его версия изменилась, нам необходимо срочно актуализировать данные в открытой форме, чтобы избежать ошибки при ее последующей записи пользователем. Для этого у управляемой формы существует метод Прочитать() (или ЭтаФорма.Прочитать()).

Этот метод выполняет следующие действия:

Посмотрим на пример кода:


&НаСервере
Процедура ВыполнитьВнешнееИзменениеОбъекта()

    // Предположим, здесь отрабатывает внешняя транзакция, которая меняет наш документ в БД
    ВыполнитьЗаписьОбъектаВФоновомРежиме(Объект.Ссылка);
    
    // Теперь версия данных в нашей форме устарела.
    // Если пользователь попытается нажать "Записать", он получит ошибку.
    // Чтобы этого избежать, принудительно перечитываем данные из БД:
    ЭтаФорма.Прочитать();

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

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

Типичные ошибки разработчиков при борьбе с конфликтом версий

Рассмотрим популярные заблуждения среди начинающих программистов, которые пытаются избавиться от этого сообщения некорректными методами:

Миф 1. Использование метода ОбновитьОтображениеДанных() поможет убрать ошибку.
Это абсолютно неверно. Метод управляемой формы ОбновитьОтображениеДанных() отвечает исключительно за визуальное обновление интерфейса: он пересчитывает условное оформление, видимость колонок, доступность кнопок и обновляет динамические списки. Он никоим образом не взаимодействует с базой данных, не перечитывает реквизиты объекта и не синхронизирует ВерсияДанных.

Миф 2. Ошибку можно обойти, завернув запись в попытку-исключение.
Попытка перехватить ошибку конструкции типа Попытка... Исключение... КонецПопытки вокруг метода Записать() не решает проблему, а лишь маскирует ее. Данные пользователя все равно останутся незаписанными в базу данных, транзакция прервется, а пользователь окажется в ситуации, когда он не может сохранить свою работу.

Резюме и правила хорошего тона при разработке в 1С

Чтобы гарантированно избежать ошибки «Данные были изменены или удалены другим пользователем», придерживайтесь простых правил:

  1. Если вам нужно изменить данные в открытой форме — меняйте их в реквизите Объект (на форме), а не в базе данных. Забудьте про вызовы метода ПолучитьОбъект() в модуле управляемой формы.
  2. Если для расчетов вам нужен объект метаданных со всеми его процедурами и функциями — используйте пару РеквизитФормыВЗначение и ЗначениеВРеквизитФормы, не вызывая у полученного объекта метод Записать().
  3. Если объект все же был записан в базу данных в обход формы (фоновым заданием, бизнес-процессом, подпиской на событие другого документа) — обязательно вызовите метод ЭтаФорма.Прочитать(), чтобы актуализировать версию данных в интерфейсе пользователя.

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

← На главную