Как исправить ошибку несоответствия версий при обновлении родительской группы в иерархическом справочнике 1С?

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

При работе с иерархическими справочниками часто возникает задача актуализации агрегированных данных в группе при изменении ее подчиненных элементов. Например, необходимо вычислять минимальное или суммарное значение реквизитов всех элементов группы и записывать результат в соответствующий реквизит родительской группы.

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

Причины возникновения ошибки

Проанализируем механизмы работы платформы 1С:Предприятие, которые вызывают данную ошибку:

  1. Оптимистическая блокировка: Платформа отслеживает версию каждого объекта информационной базы. Если объект родительской группы считывается в память, а затем записывается из другого контекста или сеанса, версия объекта в базе данных меняется. Последующая попытка записать устаревший экземпляр СправочникОбъект вызывает конфликт версий.
  2. Некорректный контекст записи: Вызов методов записи объектов из обработчиков событий формы списка или клиентских обработчиков формы выполняется до или вне транзакции записи самого элемента. Это создаёт риск параллельного доступа и гонки процессов.
  3. Конкурентный доступ: Если несколько пользователей одновременно изменяют элементы, входящие в одну и ту же группу, оба процесса попытаются изменить родительский объект, что приведет к ошибкам блокировки у одного из пользователей.

Правильная архитектура обновления родителя

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

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

Пример реализации в модуле объекта

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


Процедура ПриЗаписи(Отказ, Замещение)
	
	Если ОбменДанными.Загрузка Тогда
		Возврат;
	КонецЕсли;

	// Обновляем актуальную группу
	Если ЗначениеЗаполнено(Родитель) Тогда
		ОбновитьМинимальноеЗначениеРодителя(Родитель);
	КонецЕсли;

	// Если элемент перенесен в другую группу, обновляем старого родителя
	Если Не Ссылка.Пустая() И Ссылка.Родитель <> Родитель И ЗначениеЗаполнено(Ссылка.Родитель) Тогда
		ОбновитьМинимальноеЗначениеРодителя(Ссылка.Родитель);
	КонецЕсли;

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

Процедура ОбновитьМинимальноеЗначениеРодителя(СсылкаНаРодителя)

	// Устанавливаем блокировку на родительский объект
	Блокировка = Новый БлокировкаДанных;
	ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура");
	ЭлементБлокировки.УстановитьЗначение("Ссылка", СсылкаНаРодителя);
	Блокировка.Заблокировать();

	ОбъектРодитель = СсылкаНаРодителя.ПолучитьОбъект();
	Если ОбъектРодитель <> Неопределено Тогда
		
		Запрос = Новый Запрос;
		Запрос.Текст = 
			"ВЫБРАТЬ
			|	МИНИМУМ(Справочник.МинимальноеЗначение) КАК МинЗначение
			|ИЗ
			|	Справочник.Номенклатура КАК Справочник
			|ГДЕ
			|	Справочник.Родитель = &Родитель
			|	И НЕ Справочник.ПометкаУдаления";
		
		Запрос.УстановитьПараметр("Родитель", СсылкаНаРодителя);
		Выборка = Запрос.Выполнить().Выбрать();
		
		Если Выборка.Следующий() Тогда
			Если ОбъектРодитель.МинимальноеЗначение <> Выборка.МинЗначение Тогда
				ОбъектРодитель.МинимальноеЗначение = Выборка.МинЗначение;
				ОбъектРодитель.ОбменДанными.Загрузка = Истина;
				ОбъектРодитель.Записать();
			КонецЕсли;
		КонецЕсли;
		
	КонецЕсли;

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

Альтернативные варианты решения

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

  1. Регистр сведений: Вычисляемые данные можно хранить не в реквизитах справочника, а в независимом или подчиненном регистре сведений с измерением Группа. Запись в регистр выполняется быстрее и не меняет версию объекта справочника.
  2. Динамический расчет в формах и отчетах: Если значения нужны только для отображения пользователю, целесообразно рассчитывать их «на лету» запросом в динамическом списке или виртуальных таблицах, избегая физической записи в базу данных.
  3. Фоновый пересчет: При большом объеме данных и глубокой иерархии задачу актуализации агрегатов выносят в асинхронные фоновые задания.
← На главную