При разработке механизмов проведения документов в платформе 1С:Предприятие 8 начинающие (а иногда и опытные) разработчики сталкиваются с парадоксальной на первый взгляд ситуацией. Выполняя код в процедуре ОбработкаПроведения, которая по умолчанию выполняется в транзакции, мы очищаем движения текущего документа по регистру накопления, делаем запрос к виртуальной таблице остатков этого же регистра и... внезапно видим актуальные остатки, очищенные от движений нашего документа! При этом транзакция еще не завершена, и финальный COMMIT (зафиксировать транзакцию) не выполнен.
Давайте подробно разберем, почему так происходит, как механизмы СУБД взаимодействуют с платформой 1С, как на это влияет свойство документа «Удаление движений» и какие методики проведения документов сегодня считаются отраслевым стандартом.
---
Для того чтобы понять это поведение, обратимся к теории баз данных и стандарту ACID (Atomicity, Consistency, Isolation, Durability). Третий пункт этого стандарта — Изоляция (Isolation) — гарантирует, что параллельные транзакции не должны влиять друг на друга. Однако это ограничение действует именно на внешние сессии (других пользователей).
Проанализируем ситуацию изнутри одной транзакции:
COMMIT). Это предотвращает так называемое «грязное чтение» (Dirty Read) другими сеансами.РегистрНакопления.ИмяРегистра.Остатки) рассчитываются платформой «на лету» на основе физических таблиц движений и итогов. Поскольку запрос к виртуальной таблице выполняется в контексте текущей транзакции проведения, СУБД строит остатки с учетом всех модификаций (включая очистку старых движений), сделанных в этом сеансе.Таким образом, мнение о том, что внутри транзакции проведения нельзя увидеть изменения до её завершения, ошибочно. Транзакция не скрывает изменения от самой себя — она скрывает их от сторонних транзакций.
---
В свойствах любого документа в конфигураторе есть важный параметр — «Удаление движений». Выясним, как именно эта настройка влияет на поведение платформы перед вызовом обработчика ОбработкаПроведения:
При таком режиме платформа берет всю рутину на себя. Перед тем как передать управление в процедуру ОбработкаПроведения, система автоматически инициирует транзакцию и физически удаляет из базы данных все движения, которые этот документ сформировал при прошлых проведениях.
Когда мы заходим в процедуру ОбработкаПроведения, в таблицах регистра накопления уже нет старых записей нашего документа. Запрос к остаткам вернет «чистый» результат, не требуя от нас никаких дополнительных действий по очистке.
Если выбрано это свойство, старые движения документа остаются в базе данных на момент начала проведения. Если мы просто выполним запрос к остаткам, система вернет данные с учетом старых движений текущего документа, что приведет к неверным расчетам (например, при изменении даты документа или количества товара).
Чтобы получить «чистые» остатки в этом режиме, разработчику необходимо принудительно очистить и записать движения в самом начале процедуры:
// Очищаем движения и принудительно записываем пустой набор в базу данных
Движения.ОстаткиТоваров.Записывать = Истина;
Движения.ОстаткиТоваров.Очистить();
Движения.ОстаткиТоваров.Записать(); // Движения удалены в текущей транзакции
После вызова метода Записать() СУБД мгновенно перестроит виртуальные таблицы остатков для нашей транзакции, и последующий запрос покажет остатки без учета этого документа.
---
Понимание механизмов работы транзакций привело к эволюции подходов к списанию активов (товаров, взаиморасчетов и т.д.). Рассмотрим две ключевые методики, их плюсы и минусы.
Этот подход исторически использовался в версиях 1С:Предприятие 7.7 и ранних версиях 8. Алгоритм выглядит следующим образом:
Движения и записываются при выходе из процедуры. Если товара мало — устанавливается флаг Отказ = Истина.Посмотрим на пример реализации этой методики:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// 1. Блокируем данные по номенклатуре из документа
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = Товары;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
// 2. Получаем остатки без учета текущего документа
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ДокТовары.Номенклатура КАК Номенклатура,
| СУММА(ДокТовары.Количество) КАК КоличествоВДокументе
|ПОМЕСТИТЬ ВТ_ТоварыДокумента
|ИЗ
| &ТаблицаТовары КАК ДокТовары
|СГРУППИРОВАТЬ ПО
| ДокТовары.Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_ТоварыДокумента.Номенклатура КАК Номенклатура,
| ВТ_ТоварыДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
| ЕСТЬNULL(ОстаткиТоваровОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток
|ИЗ
| ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки(&МоментВремени, Номенклатура В (ВЫБРАТЬ ВТ.Номенклатура ИЗ ВТ_ТоварыДокумента КАК ВТ)) КАК ОстаткиТоваровОстатки
| ПО ВТ_ТоварыДокумента.Номенклатура = ОстаткиТоваровОстатки.Номенклатура";
Запрос.УстановитьПараметр("ТаблицаТовары", Товары.Выгрузить());
// Исключаем момент времени документа, чтобы не видеть его старые движения
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), РежимГраницы.Исключая));
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Если Выборка.КоличествоОстаток < Выборка.КоличествоВДокументе Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара: " + Выборка.Номенклатура + ". Требуется " + Выборка.КоличествоВДокументе + ", в наличии " + Выборка.КоличествоОстаток;
Сообщение.Сообщить();
Отказ = Истина;
КонецЕсли;
КонецЦикла;
Если Отказ Тогда
Возврат;
КонецЕсли;
// 3. Если проверка пройдена — формируем движения
Для Каждого ТекСтрока Из Товары Цикл
Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ТекСтрока.Номенклатура;
Движение.Количество = ТекСтрока.Количество;
КонецЦикла;
КонецПроцедуры
Минусы старой методики: Требует избыточного чтения данных (сначала читаем остатки, проверяем, затем пишем новые движения). Время нахождения в транзакции увеличивается, что при высокой нагрузке и большом числе пользователей приводит к частым эскалациям блокировок и взаимным блокировкам (Deadlocks).
---
Рекомендованная фирмой «1С» методика для оптимизации производительности в многопользовательских базах данных. Её ключевая идея — «сначала пишем движения, потом проверяем результат». Разберем по шагам, как это работает:
ОбработкаПроведения мы сразу формируем новые движения документа и записываем их в базу данных с помощью метода Движения.Записать().Отказ = Истина и выводим сообщение об ошибке.Отказ возвращается из процедуры проведения, платформа инициирует автоматический откат (ROLLBACK) всей транзакции. Все только что записанные движения бесследно удаляются СУБД, возвращая базу данных к исходному согласованному состоянию.Реализуем новую методику проведения на практике:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// 1. Сразу формируем движения и записываем их в СУБД
Движения.ОстаткиТоваров.Записывать = Истина;
Для Каждого ТекСтрока Из Товары Цикл
Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ТекСтрока.Номенклатура;
Движение.Количество = ТекСтрока.Количество;
КонецЦикла;
// Физически записываем движения в СУБД внутри транзакции
Движения.Записать();
// 2. Блокируем только измененные нами записи регистров
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = Товары;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
// 3. Проверяем остатки "включая" момент времени документа.
// Поскольку мы записали движения, СУБД рассчитает остатки с учетом нашего списания.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваровОстатки.Номенклатура КАК Номенклатура,
| ОстаткиТоваровОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(&МоментВремени, Номенклатура В (&СписокНоменклатуры)) КАК ОстаткиТоваровОстатки
|ГДЕ
| ОстаткиТоваровОстатки.КоличествоОстаток < 0";
СписокНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
// Берем границу "Включая", чтобы захватить движения нашего документа
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), РежимГраницы.Включая));
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара: " + Выборка.Номенклатура + ". Обнаружен дефицит в количестве: " + (-Выборка.КоличествоОстаток);
Сообщение.Сообщить();
Отказ = Истина;
КонецЦикла;
КонецЕсли;
// При Отказ = Истина платформа сделает ROLLBACK транзакции, и все движения будут аннулированы
КонецПроцедуры
Преимущества новой методики: Существенно сокращается время наложения блокировок. Чтение данных происходит только один раз в самом конце. Снижается вероятность взаимных блокировок, что критически важно для высоконагруженных и масштабируемых систем.
---
В современных конфигурациях 1С для регистров накопления активно применяется свойство «Разделение итогов». Выясним, влияет ли оно на логику получения остатков в транзакции:
Остатки, платформа 1С автоматически объединяет данные основных итогов и всех буферных таблиц, включая те строки, которые записаны текущей незавершенной транзакцией. Изоляция транзакции гарантирует получение абсолютно точных и актуальных данных в рамках нашей сессии, при этом сторонние пользователи не увидят эти промежуточные "разделенные" итоги до момента фиксации транзакции.---
Мы выяснили, почему в транзакции проведения разработчик видит «очищенные» итоги регистров. Это стандартное поведение СУБД, обеспечивающее видимость собственных изменений внутри одного сеанса. Управляя свойством документа «Удаление движений» и осознанно выбирая между старой и новой методиками проведения, мы можем проектировать быстрые, отказоустойчивые и высокопроизводительные бизнес-приложения на платформе 1С:Предприятие 8.