Почему в транзакции проведения документа видны очищенные движения регистров накопления?

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

При разработке механизмов проведения документов в платформе 1С:Предприятие 8 начинающие (а иногда и опытные) разработчики сталкиваются с парадоксальной на первый взгляд ситуацией. Выполняя код в процедуре ОбработкаПроведения, которая по умолчанию выполняется в транзакции, мы очищаем движения текущего документа по регистру накопления, делаем запрос к виртуальной таблице остатков этого же регистра и... внезапно видим актуальные остатки, очищенные от движений нашего документа! При этом транзакция еще не завершена, и финальный COMMIT (зафиксировать транзакцию) не выполнен.

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

---

1. Почему транзакция «видит» собственные изменения

Для того чтобы понять это поведение, обратимся к теории баз данных и стандарту ACID (Atomicity, Consistency, Isolation, Durability). Третий пункт этого стандарта — Изоляция (Isolation) — гарантирует, что параллельные транзакции не должны влиять друг на друга. Однако это ограничение действует именно на внешние сессии (других пользователей).

Проанализируем ситуацию изнутри одной транзакции:

Таким образом, мнение о том, что внутри транзакции проведения нельзя увидеть изменения до её завершения, ошибочно. Транзакция не скрывает изменения от самой себя — она скрывает их от сторонних транзакций.

---

2. Как свойство «Удаление движений» влияет на проведение

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

  1. Удалять автоматически / Удалять автоматически при отмене проведения:

    При таком режиме платформа берет всю рутину на себя. Перед тем как передать управление в процедуру ОбработкаПроведения, система автоматически инициирует транзакцию и физически удаляет из базы данных все движения, которые этот документ сформировал при прошлых проведениях.

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

  2. Не удалять автоматически:

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

    Чтобы получить «чистые» остатки в этом режиме, разработчику необходимо принудительно очистить и записать движения в самом начале процедуры:

    
    // Очищаем движения и принудительно записываем пустой набор в базу данных
    Движения.ОстаткиТоваров.Записывать = Истина;
    Движения.ОстаткиТоваров.Очистить();
    Движения.ОстаткиТоваров.Записать(); // Движения удалены в текущей транзакции
    

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

---

3. Старая и новая методики проведения документов

Понимание механизмов работы транзакций привело к эволюции подходов к списанию активов (товаров, взаиморасчетов и т.д.). Рассмотрим две ключевые методики, их плюсы и минусы.

Методика 1. Старая методика проведения (списание «из остатков»)

Этот подход исторически использовался в версиях 1С:Предприятие 7.7 и ранних версиях 8. Алгоритм выглядит следующим образом:

  1. Устанавливаются управляемые блокировки на регистр по аналитике документа (чтобы другие пользователи не изменили остатки параллельно).
  2. Получаются текущие остатки товаров (без учета движений нашего документа — либо за счет автоматического удаления движений, либо за счет ручной очистки и записи пустого набора).
  3. В коде на встроенном языке проверяется, хватает ли товара на складе для проведения документа.
  4. Если товара достаточно, новые движения добавляются в коллекцию Движения и записываются при выходе из процедуры. Если товара мало — устанавливается флаг Отказ = Истина.

Посмотрим на пример реализации этой методики:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // 1. Блокируем данные по номенклатуре из документа
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    ЭлементБлокировки.ИсточникДанных = Товары;
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
    Блокировка.Заблокировать();
    
    // 2. Получаем остатки без учета текущего документа
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ДокТовары.Номенклатура КАК Номенклатура,
        |	СУММА(ДокТовары.Количество) КАК КоличествоВДокументе
        |ПОМЕСТИТЬ ВТ_ТоварыДокумента
        |ИЗ
        |	&ТаблицаТовары КАК ДокТовары
        |СГРУППИРОВАТЬ ПО
        |	ДокТовары.Номенклатура
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_ТоварыДокумента.Номенклатура КАК Номенклатура,
        |	ВТ_ТоварыДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
        |	ЕСТЬNULL(ОстаткиТоваровОстатки.КоличествоОстаток, 0) КАК КоличествоОстаток
        |ИЗ
        |	ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
        |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки(&МоментВремени, Номенклатура В (ВЫБРАТЬ ВТ.Номенклатура ИЗ ВТ_ТоварыДокумента КАК ВТ)) КАК ОстаткиТоваровОстатки
        |		ПО ВТ_ТоварыДокумента.Номенклатура = ОстаткиТоваровОстатки.Номенклатура";
        
    Запрос.УстановитьПараметр("ТаблицаТовары", Товары.Выгрузить());
    // Исключаем момент времени документа, чтобы не видеть его старые движения
    Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), РежимГраницы.Исключая));
    
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        Если Выборка.КоличествоОстаток < Выборка.КоличествоВДокументе Тогда
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = "Недостаточно товара: " + Выборка.Номенклатура + ". Требуется " + Выборка.КоличествоВДокументе + ", в наличии " + Выборка.КоличествоОстаток;
            Сообщение.Сообщить();
            Отказ = Истина;
        КонецЕсли;
    КонецЦикла;
    
    Если Отказ Тогда
        Возврат;
    КонецЕсли;
    
    // 3. Если проверка пройдена — формируем движения
    Для Каждого ТекСтрока Из Товары Цикл
        Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
        Движение.Период = Дата;
        Движение.Номенклатура = ТекСтрока.Номенклатура;
        Движение.Количество = ТекСтрока.Количество;
    КонецЦикла;
    
КонецПроцедуры

Минусы старой методики: Требует избыточного чтения данных (сначала читаем остатки, проверяем, затем пишем новые движения). Время нахождения в транзакции увеличивается, что при высокой нагрузке и большом числе пользователей приводит к частым эскалациям блокировок и взаимным блокировкам (Deadlocks).

---

Методика 2. Новая методика проведения (списание «от противного»)

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

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

Реализуем новую методику проведения на практике:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // 1. Сразу формируем движения и записываем их в СУБД
    Движения.ОстаткиТоваров.Записывать = Истина;
    Для Каждого ТекСтрока Из Товары Цикл
        Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
        Движение.Период = Дата;
        Движение.Номенклатура = ТекСтрока.Номенклатура;
        Движение.Количество = ТекСтрока.Количество;
    КонецЦикла;
    
    // Физически записываем движения в СУБД внутри транзакции
    Движения.Записать();
    
    // 2. Блокируем только измененные нами записи регистров
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    ЭлементБлокировки.ИсточникДанных = Товары;
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
    Блокировка.Заблокировать();
    
    // 3. Проверяем остатки "включая" момент времени документа. 
    // Поскольку мы записали движения, СУБД рассчитает остатки с учетом нашего списания.
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ОстаткиТоваровОстатки.Номенклатура КАК Номенклатура,
        |	ОстаткиТоваровОстатки.КоличествоОстаток КАК КоличествоОстаток
        |ИЗ
        |	РегистрНакопления.ОстаткиТоваров.Остатки(&МоментВремени, Номенклатура В (&СписокНоменклатуры)) КАК ОстаткиТоваровОстатки
        |ГДЕ
        |	ОстаткиТоваровОстатки.КоличествоОстаток < 0";
        
    СписокНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
    Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
    // Берем границу "Включая", чтобы захватить движения нашего документа
    Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), РежимГраницы.Включая));
    
    РезультатЗапроса = Запрос.Выполнить();
    
    Если Не РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = "Недостаточно товара: " + Выборка.Номенклатура + ". Обнаружен дефицит в количестве: " + (-Выборка.КоличествоОстаток);
            Сообщение.Сообщить();
            Отказ = Истина;
        КонецЦикла;
    КонецЕсли;
    
    // При Отказ = Истина платформа сделает ROLLBACK транзакции, и все движения будут аннулированы
    
КонецПроцедуры

Преимущества новой методики: Существенно сокращается время наложения блокировок. Чтение данных происходит только один раз в самом конце. Снижается вероятность взаимных блокировок, что критически важно для высоконагруженных и масштабируемых систем.

---

4. Влияние режима «Разделение итогов»

В современных конфигурациях 1С для регистров накопления активно применяется свойство «Разделение итогов». Выясним, влияет ли оно на логику получения остатков в транзакции:

---

Резюме

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

← На главную