Приветствуем вас, уважаемые коллеги! Каждый разработчик на платформе «1С:Предприятие 8» рано или поздно сталкивается с системным сообщением: «Данные были изменены или удалены другим пользователем». Эта ошибка способна поставить в тупик начинающего специалиста, особенно когда точно известно, что в системе в этот момент работает только один человек. Давайте вместе подробно разберем природу этого явления, проанализируем механизм оптимистической блокировки и выясним, как правильно писать код, чтобы навсегда предотвратить появление этой проблемы.
Для начала проанализируем ситуацию на физическом уровне базы данных. В платформе «1С:Предприятие 8» для всех ссылочных типов (документы, справочники, бизнес-процессы и т. д.) применяется механизм оптимистической блокировки. Это означает, что система не блокирует запись в таблице СУБД на все время, пока пользователь держит открытой форму и редактирует реквизиты. Вместо этого используется скрытый системный реквизит ВерсияДанных (в СУБД MS SQL Server это реализуется через тип данных rowversion или timestamp).
Рассмотрим стандартный жизненный цикл работы с формой документа:
ВерсияДанных (например, «Версия 1»), и загружает их в память управляемой формы в объект типа ДанныеФормыСтруктура (наш реквизит Объект).ДокОбъект = Ссылка.ПолучитьОбъект().ДокОбъект.Записать(). В этот момент СУБД записывает новые значения и автоматически увеличивает версию данных в таблице (теперь там записана «Версия 2»).Форма.Записать()). Платформа отправляет данные формы на сервер. Перед фиксацией транзакции она сверяет версию данных, которая хранилась в памяти формы («Версия 1»), с актуальной версией данных в СУБД («Версия 2»).Именно эта ситуация произошла у автора темы на форуме. Код сначала считывал объект из базы данных через ПолучитьОбъект(), перезаписывал его в базе, а затем форма пыталась выполнить штатную запись устаревшей версии данных.
Выясним причину, по которой разработчики часто совершают эту ошибку. Главное заблуждение заключается в мысли: «Чтобы изменить данные в табличной части, мне обязательно нужно получить объект документа из базы данных». В управляемом приложении это абсолютно не так!
Для изменения данных, отображаемых на форме, нет никакой необходимости обращаться к базе данных через транзакции записи. Реквизит формы Объект и его табличные части (например, Объект.Товары) имеют специальные типы данных — ДанныеФормыСтруктура и ДанныеФормыКоллекция соответственно. Мы имеем возможность модифицировать их напрямую в контексте формы на сервере, а платформа сама запишет измененные данные в базу при стандартном сохранении формы.
Рассмотрим, как правильно переписать исходный проблемный алгоритм без использования ПолучитьОбъект():
&НаСервере
Процедура ОбновитьЦеныСравненияНаФорме(РезультатЗапроса)
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Настраиваем структуру отбора для поиска строк в коллекции формы
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", Выборка.Номенклатура);
// Ищем строки непосредственно в реквизите формы "Объект.Товары"
НайденныеСтроки = Объект.Товары.НайтиСтроки(Отбор);
Для Каждого СтрокаТовара Из НайденныеСтроки Цикл
// Изменяем данные прямо в элементах коллекции формы
СтрокаТовара.ЦенаСравнения = Выборка.Цена;
СтрокаТовара.СуммаСравнения = СтрокаТовара.Количество * СтрокаТовара.ЦенаСравнения;
КонецЦикла;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
При таком подходе мы не трогаем физическую базу данных во время работы алгоритма. Данные обновляются на экране пользователя, визуально отображаются в табличной части, а когда пользователь нажмет кнопку «Записать», документ сохранится в базу данных штатным образом за один проход и без конфликтов блокировок.
Иногда возникают ситуации, когда для расчета параметров табличной части или проверки реквизитов нам жизненно необходим полноценный прикладной объект типа ДокументОбъект.ЗаказПоставщику. Это требуется, например, если расчет цен завязан на сложные экспортные процедуры из модуля этого объекта, которые невозможно вызвать из контекста формы.
Для этого в управляемом приложении предусмотрен специальный механизм временной конвертации данных формы в прикладной объект и обратно. Разберем по шагам, как это работает:
Объект во временный прикладной объект в памяти сервера с помощью функции РеквизитФормыВЗначение("Объект").Записать() у этого временного объекта!ЗначениеВРеквизитФормы(ДокОбъект, "Объект"). Платформа при этом автоматически обновляет данные на форме и синхронизирует внутреннюю версию данных.Посмотрим на пример правильной реализации этого паттерна проектирования:
&НаСервере
Процедура ОбновитьДанныеЧерезПрикладнойОбъект(РезультатЗапроса)
Если Не РезультатЗапроса.Пустой() Тогда
// Преобразуем данные формы во временный объект в памяти сервера
ДокОбъект = РеквизитФормыВЗначение("Объект");
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", Выборка.Номенклатура);
НайденныеСтроки = ДокОбъект.Товары.НайтиСтроки(Отбор);
Для Каждого СтрокаТовара Из НайденныеСтроки Цикл
СтрокаТовара.ЦенаСравнения = Выборка.Цена;
СтрокаТовара.СуммаСравнения = СтрокаТовара.Количество * СтрокаТовара.ЦенаСравнения;
КонецЦикла;
КонецЦикла;
// Здесь мы МОЖЕМ вызвать экспортный метод модуля объекта, если это нужно:
// ДокОбъект.ПересчитатьВсеСуммыДокумента();
// Возвращаем измененный объект в контекст управляемой формы
ЗначениеВРеквизитФормы(ДокОбъект, "Объект");
// Метод ДокОбъект.Записать() НЕ ВЫЗЫВАЕТСЯ! Запись произойдет позже средствами формы.
КонецЕсли;
КонецПроцедуры
Проанализируем еще одну архитектурную ситуацию. Бывают редкие случаи, когда запись объекта в базу данных сторонним кодом в обход формы действительно неизбежна. Например, при проведении вашего документа из формы создается и проводится подчиненный документ, который в процессе своей работы должен принудительно изменить статус или реквизиты исходного («нашего») документа прямо в базе данных.
Если в базе данных объект уже был перезаписан и его версия изменилась, нам необходимо срочно актуализировать данные в открытой форме, чтобы избежать ошибки при ее последующей записи пользователем. Для этого у управляемой формы существует метод Прочитать() (или ЭтаФорма.Прочитать()).
Этот метод выполняет следующие действия:
Объект на форме.ВерсияДанных формы до актуального состояния.Посмотрим на пример кода:
&НаСервере
Процедура ВыполнитьВнешнееИзменениеОбъекта()
// Предположим, здесь отрабатывает внешняя транзакция, которая меняет наш документ в БД
ВыполнитьЗаписьОбъектаВФоновомРежиме(Объект.Ссылка);
// Теперь версия данных в нашей форме устарела.
// Если пользователь попытается нажать "Записать", он получит ошибку.
// Чтобы этого избежать, принудительно перечитываем данные из БД:
ЭтаФорма.Прочитать();
КонецПроцедуры
Важное предупреждение: Использование метода Прочитать() имеет серьезный побочный эффект. Если пользователь успел вручную отредактировать какие-то поля на форме перед тем, как отработал ваш серверный код, все его несохраненные изменения будут безвозвратно стерты и перезаписаны данными из базы данных. Используйте этот метод с осторожностью и только тогда, когда это действительно продиктовано логикой бизнес-процесса.
Рассмотрим популярные заблуждения среди начинающих программистов, которые пытаются избавиться от этого сообщения некорректными методами:
Миф 1. Использование метода ОбновитьОтображениеДанных() поможет убрать ошибку.
Это абсолютно неверно. Метод управляемой формы ОбновитьОтображениеДанных() отвечает исключительно за визуальное обновление интерфейса: он пересчитывает условное оформление, видимость колонок, доступность кнопок и обновляет динамические списки. Он никоим образом не взаимодействует с базой данных, не перечитывает реквизиты объекта и не синхронизирует ВерсияДанных.
Миф 2. Ошибку можно обойти, завернув запись в попытку-исключение.
Попытка перехватить ошибку конструкции типа Попытка... Исключение... КонецПопытки вокруг метода Записать() не решает проблему, а лишь маскирует ее. Данные пользователя все равно останутся незаписанными в базу данных, транзакция прервется, а пользователь окажется в ситуации, когда он не может сохранить свою работу.
Чтобы гарантированно избежать ошибки «Данные были изменены или удалены другим пользователем», придерживайтесь простых правил:
Объект (на форме), а не в базе данных. Забудьте про вызовы метода ПолучитьОбъект() в модуле управляемой формы.РеквизитФормыВЗначение и ЗначениеВРеквизитФормы, не вызывая у полученного объекта метод Записать().ЭтаФорма.Прочитать(), чтобы актуализировать версию данных в интерфейсе пользователя.Соблюдение этих архитектурных стандартов разработки на платформе «1С:Предприятие 8» позволит вам создавать отказоустойчивые, быстрые и дружелюбные к пользователю интерфейсы без досадных ошибок совместного доступа.