Что такое транзакция записи документа в 1с
Перейти к содержимому

Что такое транзакция записи документа в 1с

  • автор:

Транзакции: правила использования

Область применения: управляемое приложение, мобильное приложение, обычное приложение.

Транзакции применяются для целостного изменения связанных данных, т.е. все действия с базой данных, выполняемые в рамках транзакции или выполняются целиком, или целиком откатываются.

1. Использование транзакций в 1С:Предприятии обладает рядом особенностей:

не поддерживаются вложенные транзакции (см. подробнее Вложенность транзакций);

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

транзакция может быть инициирована явно в прикладном коде при использовании метода НачатьТранзакцию . Так же платформа 1С:Предприятие неявным образом начинает транзакцию при любой записи в базу данных (см. подробнее Документация платформы. Механизм транзакций);

Эти особенности накладывают ряд требований к написанию кода с использованием транзакций. Несоблюдение этих требований может приводить к возникновению ошибок вида «В этой транзакции уже происходили ошибки», которые может быть крайне сложно воспроизвести и отладить.

1.1. Поскольку исключение не отменяет транзакцию сразу, но запрещает успешное завершение транзакции, то все вызовы НачатьТранзакцию с одной стороны и ЗафиксироватьТранзакцию или ОтменитьТранзакцию с другой стороны должны быть парными.

1.2. Начало транзакции и ее фиксация (отмена) должны происходить в контексте одного метода

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

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

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

метод НачатьТранзакцию должен быть за пределами блока Попытка-Исключение непосредственно перед оператором Попытка ;

все действия, выполняемые после вызова метода НачатьТранзакцию , должны находиться в одном блоке Попытка, в том числе чтение, блокировка и обработка данных;

метод ЗафиксироватьТранзакцию должен идти последним в блоке Попытка перед оператором Исключение , чтобы гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение;

необходимо предусмотреть обработку исключений – в блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию , а затем выполнять другие действия, если они требуются;

рекомендуется в блоке Исключение делать запись в журнал регистрации;

при использовании вложенных транзакций (см. п. 1.4) в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение . В противном случае исключение не будет передано выше по стеку вызовов, там не сработает обработка исключения, внешняя транзакция не будет явным образом отменена и платформа вызовет исключение «В данной транзакции происходила ошибка»

НачатьТранзакцию();
Попытка
БлокировкаДанных = Новый БлокировкаДанных;
ЭлементБлокировкиДанных = БлокировкаДанных.Добавить(«Документ.ПриходнаяНакладная»);
ЭлементБлокировкиДанных.УстановитьЗначение(«Ссылка», СсылкаДляОбработки);
ЭлементБлокировкиДанных.Режим = РежимБлокировкиДанных.Исключительный;
БлокировкаДанных.Заблокировать();

. // чтение или запись данных

ЗаписьЖурналаРегистрации(НСтр(«ru = ‘Выполнение операции'»),
УровеньЖурналаРегистрации.Ошибка,
,
,
ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));

ВызватьИсключение; // есть внешняя транзакция

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

1.4.1. Не стоит усложнять код, явно используя метод НачатьТранзакцию , когда кроме записи объекта другие действия c базой данных не делаются – платформа при записи сама откроет транзакцию.

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

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

Попытка
ДокументОбъект = Документы.ПриходнаяНакладная.СоздатьДокумент();
. // действия по заполнению объекта
ДокументОбъект.Записать();
Исключение
. // действия по обработке исключения
КонецПопытки;

НачатьТранзакцию();
Попытка
ДокументОбъект = Документы.ПриходнаяНакладная.СоздатьДокумент();
. // действия по заполнению объекта
ДокументОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;

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

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

Пример. Вызывается метод ДобавитьЭлектроннуюПодпись . Внутри, если что-то пошло не так, нужно обработать исключение и добавить текст вида: «Не удалось добавить электронную подпись к объекту %ПредставлениеОбъекта% по причине:%ОписаниеОшибки%». В противном случае исключение будет обработано выше по стеку вызовов, например, при записи файла и будет выдано сообщение вида: «Не удалось записать файл %ИмяФайла% по причине: %ОписаниеОшибки%», где в «%ОписаниеОшибки%», будет просто указание на строчку кода и пользователю будет непонятно, зачем вообще программа записывала файл, если он просто его подписывал.

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

2. Ограничение на длину (продолжительность) транзакции.

2.1. В общем случае в рамках одной транзакции нужно выполнять только те действия, которые неделимы, исходя из бизнес-логики.

Пример. При проведении документа записывается документ и его движения в регистрах. Если не прошла запись хотя бы в один регистр вся операция проведения должна быть отменена.

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

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

2.2. Следует избегать транзакций, которые выполняются длительное время.

Например, неправильно : для загрузки адресного классификатора записывать все данные, относящиеся к одной версии классификатора в одной транзакции, для того, чтобы в случае ошибки откатить целиком загружаемую версию классификатора. Т.к. данных по одной версии классификатора много (объем около 1 Гб), то для выполнения такой транзакции, во-первых, может не хватить оперативной памяти (особенно при использовании файловой информационной базы на 32-разрядной ОС), а, во-вторых, такая операция будет выполняться достаточно долго и ее нельзя будет оптимизировать за счет выполнения в несколько потоков.

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

2.2.1 Чем дольше выполняется транзакция, тем большее время будут заняты ресурсы сервера 1С:Предприятия и СУБД. Как правило длинные транзакции занимают следующие ресурсы:

  • в ходе выполнения транзакции все изменения в базе данных записываются в журнал транзакций, что необходимо для возможности откатить транзакцию;
  • блокировки, установленные в транзакции, остаются до конца транзакции;
  • на сервере 1С:Предприятия блокировки занимают оперативную память;
  • другие ресурсы, необходимые самой бизнес-логике, которая выполняется в транзакции.

Все это в целом может снижать эффективность использования ресурсов.

2.2.2. Если две транзакции пересекаются по блокируемым ресурсам, то транзакция, которая начала выполняться позже, будет ожидать возможность установления блокировки ограниченное время (по умолчанию – 20 секунд), после чего будет завершена с исключением «Превышено время ожидания установки блокировки». Поэтому длинные транзакции могут сильно снижать удобство параллельной работы пользователей.

Возникновение таких исключений – это повод провести анализ действий, которые выполняются в конфликтующих транзакциях

возможно, какие-то действия можно вынести за транзакцию (см. п. 2.4);
если действие вынести нельзя, то нужно постараться оптимизировать алгоритм его выполнения;

также нужно проанализировать оптимальность устанавливаемых блокировок (см. группу стандартов Избыточные блокировки и методы оптимизации )

2.3. В рамках транзакции нужно стремиться выполнять минимум действий – только те, которые нельзя в соответствии с бизнес-логикой выполнять вне транзакции. В частности:

сложные, ресурсоемкие расчеты нужно стремиться делать до начала транзакции, если это позволяет бизнес-логика;

если расчет должен выполняться в транзакции, то нужно стремиться сделать его как можно более простым. Например, контроль остатков можно делать уже после записи простым запросом к записываемому регистру;

проверка заполнения объекта должна делаться вне транзакции (см. Проверки, выполняемые в и вне транзакции записи объекта);

запросы, перед выполнением которых не нужно устанавливать блокировку данных, нужно стремиться выполнять до начала транзакции (см. Ответственное чтение данных);

запросы, выполняемые в рамках транзакций нужно стремиться оптимизировать (см. группу стандартов Оптимизация запросов )

не следует в транзакции вызывать внешние ресурсы (сетевые папки, интранет- и интернет-сайты, почтовые, FTP-, HTTP-, веб-сервисы и т.п.):


    обращение к внешнему ресурсу может быть длительным;

ресурс может быть недоступен;
может произойти ошибка тайм-аута.

3. Обязательное использование транзакции.

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

НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Валюта.Установить(ВалютаСсылка);
НаборЗаписей.Загрузить(ТаблицаКурсов);
НаборЗаписей.ОбменДанными.Загрузка = Истина;

НачатьТранзакцию();
Попытка
РегистрыСведений.КурсыВалют.УстановитьИспользованиеИтогов(Ложь);

НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Валюта.Установить(ВалютаСсылка);
НаборЗаписей.Загрузить(ТаблицаКурсов);
НаборЗаписей.ОбменДанными.Загрузка = Истина;

ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;

Вы не умеете работать с транзакциями

Заголовок вышел броским, но накипело. Сразу скажу, что речь пойдет об 1С. Дорогие 1С-ники, вы не умеете работать с транзакциями и не понимаете что такое исключения. К такому выводу я пришел, просматривая большое количество кода на 1С, рождаемого в дебрях отечественного энтерпрайза. В типовых конфигурациях с этим все достаточно хорошо, но ужасающее количество заказного кода написано некомпетентно с точки зрения работы с базой данных. Вы когда-нибудь видели у себя ошибку «В данной транзакции уже происходили ошибки»? Если да — то заголовок статьи относится и к вам. Давайте под катом разберемся, наконец, что такое транзакции и как правильно с ними обращаться, работая с 1С.

Почему надо бить тревогу

Для начала, давайте разберемся, что же такое представляет собой ошибка «В данной транзакции уже происходили ошибки». Это, на самом деле, предельно простая штука: вы пытаетесь работать с базой данных внутри уже откаченной (отмененной) транзакции. Например, где-то был вызван метод ОтменитьТранзакцию, а вы пытаетесь ее зафиксировать.

Почему это плохо? Потому что данная ошибка ничего не говорит вам о том, где на самом деле случилась проблема. Когда в саппорт от пользователя приходит скриншот с таким текстом, а в особенности для серверного кода, с которым интерактивно не работает человек — это… Хотел написать «критичная ошибка», но подумал, что это buzzword, на который уже никто не обращает внимания…. Это задница. Это ошибка программирования. Это не случайный сбой. Это косяк, который надо немедленно переделывать. Потому что, когда у вас фоновые процессы сервера встанут ночью и компания начнет стремительно терять деньги, то «В данной транзакции уже происходили ошибки» это последнее, что вы захотите увидеть в диагностических логах.

Есть, конечно, вероятность, что технологический журнал сервера (он ведь у вас включен в продакшене, да?) как-то поможет диагностировать проблему, но я сейчас навскидку не могу придумать вариант — как именно в нем найти реальную причину указанной ошибки. А реальная причина одна — программист Вася получил исключение внутри транзакции и решил, что один раз — не карабас «подумаешь, ошибка, пойдем дальше».

Что такое транзакции в 1С

Неловко писать про азбучные истины, но, видимо, немножго придется. Транзакции в 1С — это то же самое, что транзакции в СУБД. Это не какие-то особенные «1С-ные» транзакции, это и есть транзакции в СУБД. Согласно общей идее транзакций, они могут либо выполниться целиком, либо не выполниться совсем. Все изменения в таблицах базы данных, выполненные внутри транзакции, могут быть разом отменены, как будто ничего не было.

Далее, нужно понимать, что в 1С не поддерживаются вложенные транзакции. Собственно говоря, они не поддерживаются не «в 1С», а вообще не поддерживаются. По-крайней мере, теми СУБД, с которыми умеет работать 1С. Вложенных транзакций, например, нет в MS SQL и Postgres. Каждый «вложенный» вызов НачатьТранзакцию просто увеличивает счетчик транзакций, а каждый вызов «ЗафиксироватьТранзакцию» — уменьшает этот счетчик. Данное поведение описано в множестве книжек и статей, но выводы из этого поведения, видимо, разобраны недостаточно. Строго говоря, в SQL есть т.н. SAVEPOINT, но 1С их не использует, да и вещь это достаточно специфичная.

Здесь и далее, специально для Воинов Истинной Веры, считающих, что код должен писаться только на английском, под спойлерами будет приведен аналог кода в англоязычном синтаксисе 1С.

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

Код на английском

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

Вы же наверняка пишете такой код, да? Приведенный пример кода содержит ошибки. Как минимум, три. Знаете какие? Про первую я скажу сразу, она связана с объектными блокировками и не имеет отношения непосредственно к транзакциям. Про вторую — чуть позже. Третья ошибка — это deadlock, который возникнет при параллельном исполнении этого кода, но это тема для отдельной статьи, ее рассматривать сейчас не будем, дабы не усложнять код. Ключевое слово для гугления: deadlock управляемые блокировки.

Обратите внимание, простой ведь код. Такого в ваших 1С-системах просто вагон. И он содержит сразу, как минимум, 3 ошибки. Задумайтесь на досуге, сколько ошибок есть в более сложных сценариях работы с транзакциями, написанных вашими программистами 1С 🙂

Объектные блокировки

Итак, первая ошибка. В 1С существуют объектные блокировки, так называемые «оптимистические» и «пессимистические». Кто придумал термин, не знаю, убил бы :). Совершенно невозможно запомнить, какая из них за что отвечает. Подробно про них написано здесь и здесь, а также в прочей IT-литературе общего назначения.

Суть проблемы в том, что в указанном примере кода изменяется объект базы данных, но в другом сеансе может сидеть интерактивный пользователь (или соседний фоновый поток), который тоже будет менять этот объект. Здесь один из вас может получить ошибку «запись была изменена или удалена». Если это произойдет в интерактивном сеансе, то пользователь почешет репу, ругнется и попробует переоткрыть форму. Если это произойдет в фоновом потоке, то вам придется искать это в логах. А журнал регистрации, как вы знаете, медленный, а ELK-стек для журналов 1С у нас в отрасли настраивают единицы… (мы, к слову, входим в число тех, кто настраивает и другим помогает настраивать :))

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

А теперь про транзакции

С первой ошибкой разобрались, давайте перейдем ко второй.

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

Именно так я бы назвал эту проблему. В нашем статическом анализаторе кода 1С на базе SonarQube мы даже отдельно встроили такую диагностику. Сейчас я работаю над ее развитием, и фантазия программистов 1С, чей код попадает ко мне на анализ, порой приводит меня в шок и трепет…

Почему? Потому что выброшенное наверх исключение внутри транзакции в 90% случаев не даст эту транзакцию зафиксировать и приведет к ошибке. Следует понимать, что 1С автоматически откатывает незавершенную транзакцию только после возвращения из скриптового кода на уровень кода платформы. До тех пор, пока вы находитесь на уровне кода 1С, транзакция остается активной.

Поднимемся на уровень выше по стеку вызовов:

Процедура ВажныйКод() СписокСсылок = ПолучитьГдеТоСписокСсылок(); ОченьПолезныйИВажныйКод(СписокСсылок); КонецПроцедуры

Смотрите, что получается. Наш проблемный метод вызывается откуда-то извне, выше по стеку. На уровне этого метода разработчик понятия не имеет — будут ли какие-то транзакции внутри метода ОченьПолезныйИВажныйКод или их не будет. А если будут — то будут ли они все завершены… Мы же все тут за мир и инкапсуляцию, верно? Автор метода «ВажныйКод» не должен думать про то, что именно происходит внутри вызываемого им метода. Того самого, в котором некорректно обрабатывается транзакция. В итоге, попытка поработать с базой данных после выброса исключения изнутри транзакции, с высокой вероятностью приведет к тому, что «В данной транзакции бла-бла…»

Размазывание транзакций по методам

Второе правило «транзакционно-безопасного» кода: счетчик ссылок транзакций в начале метода и в его конце должен иметь одно и то же значение. Нельзя начинать транзакцию в одном методе и завершать ее в другом. Из этого правила, наверное, можно найти исключения, но это будет какой-то низкоуровневый код, который пишут более компетентные люди. В общем случае так писать нельзя.

Процедура ВажныйКод() СписокСсылок = ПолучитьГдеТоСписокСсылок(); ОченьПолезныйИВажныйКод(СписокСсылок); ЗафиксироватьТранзакцию(); // Путевка в ад, серьезный разговор с автором о наших сложных трудовых отношениях. КонецПроцедуры

Выше — неприемлемый говнокод. Нельзя писать методы так, чтобы вызывающая сторона помнила и следила за возможными (или вероятными — как знать) транзакциями внутри других методов, которые она вызывает. Это нарушение инкапсуляции и разрастание спагетти-кода, который невозможно трассировать, сохраняя рассудок.

Особенно весело вспомнить, что реальный код намного больше синтетических примеров из 3-х строчек. Выискивать начинающиеся и завершающиеся транзакции по шести уровням вложенности — это прям мотивирует на задушевные беседы с авторами.

Пытаемся исправить код

Вернемся к исходному методу и попытаемся его починить. Сразу скажу, что объектную блокировку мы чинить пока не будем, просто, чтобы не усложнять код примера.

Первый подход типичного 1С-ника

Обычно программисты 1С знают, что при записи может быть выдано исключение. А еще они боятся исключений, поэтому стараются их все перехватывать. Например, вот так:

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

Ну как, стало лучше, да? Ведь теперь, возможные ошибки записи обрабатываются и даже логируются. Исключения больше не возникнут при записи объекта. И в логе даже видно — на каком объекте, не поленился, вывел в сообщение ссылку вместо лаконичного «Ошибка записи справочника», как это часто любят писать вечно торопящиеся разработчики. Иными словами, налицо забота о пользователе и рост компетенций.

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

А теперь представьте, что речь идет не о маленьком методе, а о глубоком стеке вызовов, где в самом низу кто-то взял и «выпустил» начатую транзакцию из своего метода. Верхнеуровневые процедуры могут и понятия не иметь, что кто-то там внизу начинал транзакции. В итоге, весь код валится с невнятной ошибкой, которую расследовать невозможно в принципе.

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

Методы работы с транзакциями в 1С

Не будет лишним напомнить, что вообще 1С предоставляет нам для работы с транзакциями. Это всем известные методы:

  • НачатьТранзакцию()
  • ЗафиксироватьТранзакцию()
  • ОтменитьТранзакцию()
  • ТранзакцияАктивна()

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

И есть интересная особенность. Методы выхода из транзакции (Зафиксировать и Отменить) выбрасывают исключения, если счетчик транзакций равен нулю. То есть, если вызвать один из них вне транзакции, то возникнет ошибка.

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

Как же соблюсти это правило? Давайте попробуем:

НачатьТранзакцию(); ДелаемЧтоТо(); ЗафиксироватьТранзакцию();

Выше мы уже поняли, что метод ДелаемЧтоТо — потенциально опасен. Он может выдать какое-то исключение, и транзакция «вылезет» наружу из нашего метода. Окей, добавим обработчик возможного исключения:

НачатьТранзакцию(); Попытка ДелаемЧтоТо(); Исключение // а что же написать тут? КонецПопытки; ЗафиксироватьТранзакцию();

Отлично, мы поймали возникающую ошибку, но что с ней делать? Записать сообщение в лог? Ну, может быть, если код логирования ошибок должен быть именно на этом уровне и ошибку мы тут ждем. А если нет? Если мы не ожидали тут никаких ошибок? Тогда мы должны просто передать это исключение выше, пусть с ними разбирается другой слой архитектуры. Делается это оператором «ВызватьИсключение» без аргументов. В этих ваших джава-сиплюсплюсах это делается точно так же оператором throw.

НачатьТранзакцию(); Попытка ДелаемЧтоТо(); Исключение ВызватьИсключение; КонецПопытки; ЗафиксироватьТранзакцию();

Так, стоп… Если мы просто прокидываем исключение дальше, то зачем тут вообще нужна Попытка? А вот зачем: правило заставляет нас обеспечить завершение начатой нами транзакции.

НачатьТранзакцию(); Попытка ДелаемЧтоТо(); Исключение ОтменитьТранзакцию(); ВызватьИсключение; КонецПопытки; ЗафиксироватьТранзакцию();

Теперь, вроде бы, красиво. Однако, мы ведь помним, что не доверяем коду ДелаемЧтоТо(). Вдруг там внутри его автор не читал этой статьи, и не умеет работать с транзакциями? Вдруг он там взял, да и вызвал метод ОтменитьТранзакцию или наоборот, зафиксировал ее? Нам очень важно, чтобы обработчик исключения не породил нового исключения, иначе исходная ошибка будет потеряна и расследование проблем станет невозможным. А мы помним, что методы Зафиксировать и Отменить могут выдать исключение, если транзакция не существует. Здесь-то и пригождается метод ТранзакцияАктивна.

Финальный вариант

Наконец, мы можем написать правильный, «транзакционно-безопасный» вариант кода. Вот он:

**UPD: в комментариях предложен более безопасный вариант, когда ЗафиксироватьТранзакцию расположен внутри блока Попытка. Здесь приведен именно этот вариант, ранее Фиксация располагалась после блока Попытка-Исключение.

НачатьТранзакцию(); Попытка ДелаемЧтоТо(); ЗафиксироватьТранзакцию(); Исключение Если ТранзакцияАктивна() Тогда ОтменитьТранзакцию(); КонецЕсли; ВызватьИсключение; КонецПопытки;

Постойте, но ведь не только «ОтменитьТранзакцию» может выдавать ошибки. Почему же тогда «ЗафиксироватьТранзакцию» не обернут в такое же условие с «ТранзакцияАктивна»? Опять же, по тому же самому правилу: код, начавший транзакцию, должен нести ответственность за ее завершение. Наша транзакция необязательно самая первая, она может быть вложенной. На нашем уровне абстракции мы обязаны заботиться только о нашей транзакции. Все прочие должны быть нам неинтересны. Они чужие, мы не должны нести за них ответственность. Именно НЕ ДОЛЖНЫ. Нельзя предпринимать попыток выяснения реального уровня счетчика транзакций. Это опять нарушит инкапсуляцию и приведет к «размазыванию» логики управления транзакциями. Мы проверили активность только в обработчике исключения и только для того, чтобы убедиться, что наш обработчик не породит нового исключения, «прячущего» старое.

Чек-лист рефакторинга

Давайте рассмотрим несколько наиболее распространенных ситуаций, требующих вмешательства в код.

Паттерн:

НачатьТранзакцию(); ДелаемЧтоТо(); ЗафиксироватьТранзакцию();

Обернуть в «безопасную» конструкцию с Попыткой, Проверкой активности и пробросом исключения.

Паттерн:

Если Не ТранзакцияАктивна() Тогда НачатьТранзакцию() КонецЕсли

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

Примерно похожий вариант:

Если ТранзакцияАктивна() Тогда ЗафиксироватьТранзакцию() КонецЕсли

аналогично: фиксация транзакции по условию — это странно. Почему тут условие? Что, кто-то иной мог уже зафиксировать эту транзакцию? Повод для разбирательства.

Паттерн:

НачатьТранзакцию() Пока Выборка.Следующий() Цикл // чтение объекта по ссылке // запись объекта КонецЦикла; ЗафиксироватьТранзакцию();
  1. ввести управляемую блокировку во избежание deadlock
  2. ввести вызов метода Заблокировать
  3. обернуть в «попытку», как показано выше

Паттерн:

НачатьТранзакцию() Пока Выборка.Следующий() Цикл Попытка Объект.Записать(); Исключение Сообщить("Не получилось записать"); КонецПопытки; КонецЦикла; ЗафиксироватьТранзакцию();

Данная транзакция уже не завершится в случае исключения. Нет смысла продолжать цикл. Код требуется переписать, сверяясь с исходной задачей. Дополнительно обеспечить более информативное сообщение об ошибке.

В заключение

Я, как вы уже, наверное, догадались, отношусь к людям, любящим платформу 1С и разработку на ней. К платформе, разумеется, есть претензии, особенно в среде Highload, но в общем и целом, она позволяет недорого и быстро разрабатывать очень качественные корпоративные приложения. Давая из коробки и ORM, и GUI, и веб-интерфейс, и Reporting, и много чего еще. В комментариях на Хабре обычно пишут всякое высокомерное, так вот, ребята — основная проблема 1С, как экосистемы — это не платформа и не вендор. Это слишком низкий порог вхождения, который позволяет попадать в отрасль людям, не понимающим, что такое компьютер, база данных, клиент-сервер, сеть и всякое такое. 1С сделала разработку корпоративных приложений слишком легкой. Я за 20 минут могу написать на ней учетную систему для закупок/продаж с гибкими отчетами и веб-клиентом. После этого, мне несложно подумать о себе, что и на больших масштабах можно писать примерно так же. Как-то там 1С сама все внутри сделает, не знаю как, но наверное сделает. Напишу-ка я «НачатьТранзакцию()».

И знаете — самое главное, что это прекрасно. Простота разработки в 1С позволяет моментально реализовывать бизнес-идеи и встраивать их в процессы компании. Потом всегда можно отрефакторить, главное понимать как. И если вдруг вам нужна помощь в аудите вашей «медленной 1С» — обращайтесь к специалистам по оптимизации. Она совсем не медленная.

  • Программирование
  • Совершенный код
  • ERP-системы
  • Управление разработкой
  • Управление проектами

«1С» транзакции: понятие и свойства процесса

транзакция в «1С»

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

Что такое транзакция в «1С»?

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

Транзакции могут быть выполнены до конца или не выполнены вообще. Варианта «наполовину выполненная транзакция» не существует. В СУБД транзакции фиксируются методом COMMIT.

Какими свойствами должна обладать транзакция?

ACID (Atomicity, Consistency, Isolation, Durability) – самый распространенный набор требований к транзакциям «1С».

Обязательные свойства любой транзакции:

  • Атомарность (неделимость). Необходима для того, чтобы после завершения транзакции в «1С» все данные были согласованы. Даже при простом добавлении записи может произойти рассогласование. Представим ситуацию, что мы пытаемся добавить запись вне транзакции. Сначала добавляем запись в основную таблицу, а затем в индексы. Запись удастся добавить только в первый индекс. При попытке добавить во второй, что-то может помешать это сделать (например, отключится питание). Данные перейдут в несогласованное состояние – в таблице запись есть, а в индексе о ней нет сведений. И здесь на помощь приходит атомарность. Цель атомарности – довести все действия до конца.
  • Изоляция. Это свойство обеспечивает параллельную работу пользователей и предотвращает порчу общих данных. Например, чтобы не вышло ситуации, когда 2 пользователя меняют один и тот же документ и тем самым перестирают данные друг друга. В таких случаях и нужна изоляция. Цель изоляции в транзакциях «1С» – защитить данные от действия других транзакций. Блокировки выступают как средство обеспечения изоляции транзакции.
Как работать с транзакциями «1С»?

Создавать транзакции можно двумя способами:

  • Автоматически. Например, обработка проведения документа или запись элемента справочника в базу данных. Чтобы узнать, активна ли транзакция, достаточно применить опцию «Транзакция активна()».
  • Самостоятельно. (когда создает сам разработчик). Процесс проходит при помощи действий: «Начать Транзакцию()», по окончании транзакции «Зафиксировать Транзакцию()» и в случае необходимости «Отменит Транзакцию()».

Вложенные транзакции не поддерживаются в «1С». Если вы несколько раз открываете транзакцию, она «сливается» в одну. При фиксации или отмене эти действия производятся со всеми транзакциями, активируемыми ранее.

При низкой производительности «1С» правильно созданные транзакции помогут работать быстрее. Когда транзакция проводится без ошибок, то в системе не возникает блокировок и взаимоблокировок. Вы не увидите сообщений об ошибке, и отмены проведения документа не произойдет.

Если у вас возникли вопросы по созданию транзакций и оптимизации «1С», обратитесь за консультацией к специалистам «ГЭНДАЛЬФ».

Консультация бесплатна!

Правила работы с транзакциями 1С

1. Метод «НачатьТранзакцию» должен располагаться непосредственно перед оператором «Попытка»

2. Метод «ЗафиксироватьТранзакцию» должен идти последним в блоке «Попытка»

Описание диагностики

Метод ‘ЗафиксироватьТранзакцию’ должен идти последним в блоке ‘Попытка’ перед оператором ‘Исключение’, чтобы гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение.

Тем более! неправильно:

НачатьТранзакцию(); ДокументОбъект.Записать(РежимЗаписиДокумента.Запись); Попытка ЗафиксироватьТранзакцию(); Исключение ОтменитьТранзакцию(); Инфо = ИнформацияОбОшибке(); ВызватьИсключение ПодробноеПредставлениеОшибки(Инфо); КонецПопытки;

3. Метод «ОтменитьТранзакцию» должен идти первым в блоке «Исключение»

Описание диагностики

В блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются.

Такое правило необходимо, чтобы убрать потенциальную возможность выброса исключения в блоке «Исключение», что может привести к тому, что метод «ОтменитьТранзакцию» не будет вызван.

Если в Метод2() будет обращение к БД (явное или неявное) — это вызовет ошибку «В данной транзакции уже происходили ошибки»

Примеры

Неправильно:

Процедура ЗаписатьЭлемент() НачатьТранзакцию(); Попытка Метод(); ЗафиксироватьТранзакцию(); Исключение Метод2(); // 

4. Необоснованное использование метода ТранзакцияАктивна()

При жестком соблюдении правил работы с транзакциями использование метода ТранзакцияАктивна() в блоке Исключение становится лишним.

Требует обоснования:

НачатьТранзакцию(); Попытка ДелаемЧтоТо(); ЗафиксироватьТранзакцию(); Исключение Если ТранзакцияАктивна() Тогда ОтменитьТранзакцию(); КонецЕсли; ЗаписьЖурналаРегистрации(); КонецПопытки;

Использование данного паттерна нарушает инкапсуляцию и приводит к "размазыванию" логики управления транзакциями.

На нашем уровне абстракции мы обязаны заботиться только о нашей транзакции. Все прочие должны быть нам неинтересны. Они чужие, мы не должны нести за них ответственность. Именно НЕ ДОЛЖНЫ. Нельзя предпринимать попыток выяснения реального уровня счетчика транзакций. (с)

В общем и целом, мнения мейнтейтеров здесь совпадают

Alexey Lab Sosnoviy
я не пользую ТранзакцияАктивна т.к. не понял зачем оно =) Я транзакцияю начал, я же отменил, а вские там маскировки того что внутри имхо зло

Nikita Fedkin
Использование этого метода на любом уровне означает, что на уровне ниже есть ошибка в обработке транзакций. Ошибку надо исправить, и метод будет не нужен 🙂

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

Андрей Овсянкин
В той статье есть еще комментарии. И там я согласился с доводами, что есть более хорошее решение "общего вида". Мое решение тоже хорошее, но для условий, когда "вокруг говнокод и никому верить нельзя". Для вменяемой кодовой базы и с вменяемой командой - лучше брать паттерн с ИТС

Артур Аюханов
в модульных тестах активно юзаю этот метод перед отменой транзакции и рекомендую всем его юзать в серверных модульных тестах
т.к. не всегда можно гарантировать наличие транзакции из-за ошибок в коде и т.п.

Сергей Старых
Так делать не всегда неправильно. Более правильной я считаю безусловную отмену транзакции в исключении.

Олег Тымко
Я считаю, что проверка «ТранкзакцияАктивна» перед отменой - это заметание под ковер. Но иногда это нужно делать, как, например, с безумными обменами через КД, когда там в любой момент могут попорить активную транкзакцию.

Если считаем что это необходимо - просто игнорим срабатывание правила

Примечание: с помощью метода ТранзакцияАктивна() нельзя узнать что транзакция сломана

5. При обработке исключений необходимо использовать метод ЗаписьЖурналаРегистрации()

 Попытка // код, приводящий к вызову исключения . Исключение // Пояснение причин перехвата всех исключений "незаметно" от пользователя. // . // И запись события в журнал регистрации для системного администратора. ЗаписьЖурналаРегистрации(НСтр("ru = 'Выполнение операции'"), УровеньЖурналаРегистрации.Ошибка. ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())); КонецПопытки;

Источник: 6. Необходимо обязательно указывать 1, 2 и 5 параметр метода ЗаписьЖурналаРегистрации()

ТекстОшибки = ОписаниеОшибки(); // : Деление на 0 ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()); //: Деление на 0 // й=1/0; <-- присутствует текст строки, вызвавшей ошибку // для 8.3.15+ <-- присутствует стек ТекстОшибки =ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()); //Деление на 0 //:й=1/0; //:А = ОбщегоНазначения.ЗначениеРеквизитаОбъекта();

7. Обращение к внешним ресурсам внутри транзакции вызывает проблемы производительности

  • Файловая система
  • http-, web-сервисы
  • ftp
  • com-вызовы в Windows
  • обращения к сторонним СУБД
  • и т.п.
  • как явные транзакции - НачатьТранзакцию
  • так и неявные - внутри системных событий 1С
    • например, код внутри события ПередЗаписью, ОбработкаПроведения и т.п.
    // Подписка ПриЗаписи - транзакция открыта Процедура усВыгрузитьДокументПриЗаписи(Источник, Отказ) Экспорт // . // Подключение к внешнему веб-сервису в транзакции - ошибка! // при недоступности которого транзакция зависнет WSПрокси = Новый WSПрокси(WSОпределение, URIПространстваИмен, ИмяСервиса); // без таймаута - ошибка! // . или Файл.Записать(); // обращение к файловой системе в транзакции - ошибка! // . или Почта.Отправить(); // обращение к SMTP серверу в транзакции - ошибка! КонецПроцедуры

    Правильно:

    Вынести обращение к внешним ресурсам за пределы транзакции

    Статья Ловля блокировок на связке "Microsoft SQL server - 1С" за авторством @fhqhelp

    Чего только не находилось за последние несколько лет в транзакциях проведения документов или записи набора регистров:

    - Предупреждение() или Вопрос() - это самое любимое

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

    - разнообразные файловые операции

    - обращения к другим базам через com-объекты

    - обращения к другим базам посредством Новый COMОбъект("ADODB.Connection") (ага, а на соседнем сервере уже запрос через то самое ADODB. тоже висит на блокировке! и такое было..)

    - работа с ftp

    - обращение к web-сервису

    - запуск на исполнение сторонних программ

    • Транзакции: правила использования - Стандарт 1C
    • Особенности использования транзакций при обмене данными - Методические рекомендации по конфигурированию от 1C

    8. Внутри транзакции недопустимо подавлять ошибки, вызывающие событие SDBL Func='setRollbackOnly'

    ИТС: Ошибки базы данных и транзакции

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

    Поясняющее видео от Апресова Игоря

    Кроме счетчика транзакций есть неявный флаг, булевая переменная "Запрет фиксации тразакции" (с) Апресов Игорь, см. Видео

    Неправильно:

    НачатьТранзакцию(); Попытка // . Попытка Объект.Записать(); // ПриЗаписи Отказ = Истина - вызовет setRollbackOnly Исключение ЗаписьЖурналаРегистрации(); КонецПопытки; ЗафискироватьТранзакцию(); // 

    Метод ЗафиксироватьТранзакцию() при глубине = 1 не всегда фиксирует фактическую транзакцию, а закрывает ее путем отката если она сломана и путем фиксации если она не сломана.

    Процедура ОбработкаПроведения() Попытка . запись в базу с ошибкой Исключение //по стандарту должно быть исключение, т.к. есть внешняя транзакция //но его не было //ВызватьИсключение; КонецПопытки; // 

    В каких случаях подавление ошибок делает транзакцию "сломанной":

    • Вызов метода ОтменитьТранзакцию() внутри "вложенной" транзакции
    • Подавление ошибки или отказа в методе Записать() в коде в транзакции
    • Подавление ошибки при выполнении некорректного запроса, вида "ВЫБРАТЬ 1/0" в транзакции

    Событие в технологическом журнале:
    20:43.385010-1, SDBL,5,process=1CV8,OSThread=12256,Usr=DefUser,DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=setRollbackOnly - установка флага наличия в транзакции ошибки (ее можно только откатить)

    Отмена "вложенной" явной транзакции:

    НачатьТранзакцию(); НачатьТранзакцию(); ОтменитьТранзакцию(); ЗафиксироватьТранзакцию();

    20:43.385006-0,SDBL,5,process=1CV8,OSThread=12256,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=BeginTransaction, Context='ВнешняяОбработка.ВнешняяОбработка2.Форма.Форма.Форма : 3 : НачатьТранзакцию();'
    20:43.385010-1,SDBL,5,process=1CV8,OSThread=12256,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=setRollbackOnly, Context='ВнешняяОбработка.ВнешняяОбработка2.Форма.Форма.Форма : 5 : ОтменитьТранзакцию();'
    20:43.385013-8,SDBL,4,process=1CV8,OSThread=12256,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=0,Func=Transaction, Func=RollbackTransaction, Context='ВнешняяОбработка.ВнешняяОбработка2.Форма.Форма.Форма : 6 : ЗафиксироватьТранзакцию();'
    20:43.400000-14996,SDBL,3,process=1CV8,OSThread=12256,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=0, Func=HoldConnection, Context='ВнешняяОбработка.ВнешняяОбработка2.Форма.Форма.Форма : 6 : ЗафиксироватьТранзакцию();'

    Отмена "вложенной" неявной транзакции:

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

    15:46.832029-_0,SDBL,5,process=1cv8,OSThread=16468,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=BeginTransaction
    15:46.848001_1,SDBL,5,process=1cv8,OSThread=16468,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=setRollbackOnly, Context='Документ.Документ1.МодульОбъекта : 14 : ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение); // ломаем транзакцию'
    15:46.910004_0,EXCP,6,process=1cv8,OSThread=16468,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Exception=DataBaseException, Descr=В данной транзакции уже происходили ошибки!
    15:46.910008_77980,SDBL,4,process=1cv8,OSThread=16468,Usr=DefUser, DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=0, Func=Transaction, Func=RollbackTransaction

    Формат записей ТЖ:

    Дополнение от 12.05.2023:

    Неправильно:

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

    Неправильно:

    // явная транзакция Процедура ЗагрузкаИзВМС() НачатьТранзакцию(); Попытка Для Каждого КорректировкаРеализации Из МассивКорректировок Цикл Попытка КорректировкаРеализации.Записать(РежимЗаписиДокумента.Проведение); Исключение // . КонецПопытки КонецЦикла; ЗафиксироватьТранзакцию(); Исключение ОтменитьТранзакцию(); // . КонецПопытки; КонецПроцедуры

    Использование такого рода кода приводит к ошибке "В данной транзакции уже происходили ошибки!"

    нельзя подавлять ошибки при работе внутри транзакции!

    Ещё один вариант этого правила:

    9. При использовании вложенных транзакций в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение

    Правильно:

    НачатьТранзакцию(); Попытка // блокировки, чтение, запись // .. ЗафиксироватьТранзакцию(); Исключение ОтменитьТранзакцию(); ВызватьИсключение; // есть внешняя транзакция КонецПопытки

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

    Если внутри транзакции произошла исключительная ситуация, она откатывается. Если внутри транзакции были вложенные транзакции или она сама являлась вложенной, откатываются все транзакции, независимо от того, на каком из уровней вложенности это произошло.

    Настольная книга 1С.Эксперта по технологическим вопросам, стр. 48

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

    Если внутри транзакции произошла исключительная ситуация, она откатывается. Если внутри транзакции были вложенные транзакции или она сама являлась вложенной, откатываются все транзакции, независимо от того, на каком из уровней вложенности это произошло.

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

    Если исключительную ситуацию отработать с помощью скобок Попытка. Исключение. КонецПопытки, расположенных внутри транзакции, то транзакция не завершится в момент возникновения исключительной ситуации, она дойдет до
    следующего обращения к данным – чтения или записи, продолжая при этом блокировать ресурсы, и откатится с сообщением: «В данной транзакции уже происходили ошибки!».

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

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

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

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

    Код выполнится. Транзакция успешно завершится. Исключительная ситуация оказалась расценена как восстановимая.

    Сначала в процедуру ПриЗаписи модуля справочника Организации добавим строку:

    ЭтотОбъект.ВыполнитьНесуществующийМетод(); // (такого метода у объекта не создавали)

    Далее изменим код из примера 1:

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

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

    10. Дополнительные параграфы

    10.1 При использовании оператора ВызватьИсключение необходимо сохранять стек ошибок

    10.2 Объектное чтение набора записей в транзакции устанавливает неявную управляемую разделяемую блокировку

    Это может вызвать взаимоблокировку в параллельных транзакциях

    10.3 Чтение данных в транзакции с их последующим изменением, необходимо производить после установки исключительной управляемой блокировки

    Это приводит к нарушению пункта 3.4

    Не рекомендуется:

    Исключение ОтменитьТранзакцию(); СтруктураРезультат.Успех = Ложь; СтруктураРезультат.ОписаниеОшибки = ОписаниеОшибки(); Возврат СтруктураРезультат; КонецПопытки;

    При переиспользовании такой функции/процедеры другими разработчиками на запись в ЖР обычно забивают - неумышленно или по незнанию как правильно использовать. В результате в ЖР нет лога ошибок, а прятать исключения от администартора v8std нам не разрашает. Поэтому рекомендую писать текст ошибки в ЖР сразу.

    10.5 Когда нужно использовать оператор ВызватьИсключение в блоке Исключение. КонецПопытки без параметра?

    Когда нет необходимости переопределять текст ошибки

    Не следует передавать параметр при вызове исключение в блоке Исключение. КонецПопытки.

    Неправильно:

    Исключение ОтменитьТранзакцию(); ИнформацияОбОшибке = ИнформацияОбОшибке(); ЗаписьЖурналаРегистрации("Удаление файла в MinIO", УровеньЖурналаРегистрации.Ошибка, , ПрисоединенныйФайл, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)); ВызватьИсключение ОписаниеОшибки(); // 

    Для этого следует вызывать оператор ВызватьИсключение; без параметров. Это позволяет сохранить стек ошибок и передать изначальное исключение в неизменённом виде. Подробнее смотри статью //infostart.ru/1c/articles/1513676/

    Правильно:

    Исключение ОтменитьТранзакцию(); ВызватьИсключение; КонецПопытки;

    При вызове оператора ВызватьИсклюение; не следует делать запись в журнал регистрации, так как это вызовет дублирование записей в журнале появятся 2 одинаковых записи. Неперехваченное исключение платформа сама делает запись в ЖР.

    Справка по оператору ВызватьИсключение (отрывок из встренной контекстной справки Ctrl+F1)

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

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

    Исключение составляют случаи,
    - когда исключение перехватывается на клиенте, где метод ЗаписьЖурналаРегистрации недоступен,
    - или когда необходимо уточнить текст ошибки, дополнив его прикладной информацией
    - или когда для подсистемы логируется имя события в ТЖ, типа "РаботаСФайлами.Удаление файла в MinIO".
    Подробнее смотри: infostart: Шаблоны для применения cтандартов и методик разработки конфигураций 1С и ИТС: Перехват исключений в коде

    Не следует использовать функцию ОписаниеОшибки, т.к. она неинформативна для разработчика, потому что не возвращает стек в тексте ошибки.

    10.6 Почему нужно использовать блокировки при многопоточной загрузке данных?

    Чтобы не было ошибки "нарушение целостности чтения объекта"

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

    Потоконебезопасный код:

    Процедура Записать(Документ, Значение) Экспорт ДокументОбъект = Документ.ПолучитьОбъект(); ДокументОбъект.Реквизит = Значение; ДокументОбъект.Записать(РежимЗаписиДокумента.Запись); // 

    Избежать ошибки можно организовав очередь к документу наложив явную управляемую блокировку перед записью документа.

    Потокобезопасный код:

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

    При успешном наложении блокировки и 2 поток будет ожидать, пока не отработает 1

    10.7 Какие действия будут отменены в результате выполнения ОтменитьТранзакцию()

    Код на встроенном языке содержит одну транзакцию, вложенную в другую. Какие действия будут отменены в результате выполнения ОтменитьТранзакцию() в коде вложенной транзакции?

    1. Будут отменены и вложенная и внешняя транзакция
    2. Только внешняя транзакция. ОтменитьТранзакцию() во вложенных транзакциях не работает, т.к. они не поддерживаются
    3. Т.к. вложенные транзакции технологическая платформа не поддерживает, то в начале вложенной транзакции возникнет ошибка
    4. Только вложенная транзакция. ОтменитьТранзакцию() во внешней транзакции нужно выполнять отдельно.

    С точки зрения СУБД работа с транзакциями ограничивается командами BeginTransaction, CommitTransaction и RollbackTransaction, и об вложенности транзакций внутри 1С она ничего не знает. Таким образом один вызов ОтменитьТранзакцию() на уровне 1С откатывает всю транзакцию на уровне СУБД

    Филиппов Е.В., Настольная книга 1С:Эксперта по технологическим вопросам. 2-е издание, с.48:

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

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

    10.8 Использование конструкции Попытка. Исключение. КонецПопытки внутри транзакции:

    1. Не имеет смысла, транзакция при возникновении любой исключительной ситуации все равно откатывается
    2. Не всегда оправданно, транзакция откатывается, если исключительная ситуация определена как восстановимая
    3. Иногда оправданно, транзакция не откатывается, если исключительная ситуация не определена как восстановимая
    4. Мешает понять, что на самом деле происходит, если исключение не логируется
    5. Верны ответы 1 и 4
    6. Верны ответы 2, 3 и 4

    Филиппов Е.В., Настольная книга 1С:Эксперта по технологическим вопросам. 2-е издание, стр. 48-49:

    "Если исключительную ситуацию отработать с помощью скобок Попытка. Исключение. КонецПопытки, расположенных внутри транзакции, то транзакция не завершится в момент возникновения исключительной ситуации, она дойдет до следующего обращения к данным –чтения или записи, продолжая при этом блокировать ресурсы, и откатится с сообщением: «В данной транзакции уже происходили ошибки!».

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

    Случается, однако, что транзакция, встретившись с ошибкой, находящейся в скобках Попытка. Исключение. КонецПопытки, продолжает работу. Такое различие в поведении будет зависеть от типа исключительной ситуации. Исключительные ситуации бывают восстановимыми (после которых можно продолжить работу и завершить транзакцию) и невосстановимыми (после которых нельзя продолжить работу и завершить транзакцию, например, ошибка базы данных).

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

    Код выполнится. Транзакция успешно завершится. Исключительная ситуация оказалась расценена как восстановимая.

    Пример 2
    Сначала в процедуру ПриЗаписи модуля справочника Организации добавим строку:

    ЭтотОбъект.ВыполнитьНесуществующийМетод(); // (такого метода у объекта не создавали)

    Далее изменим код из примера 1:

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

    Ошибка при вызове метода контекста (Выполнить)
    Результат = Запрос.Выполнить();
    По причине: Ошибка выполнения запроса.
    По причине: В данной транзакции уже происходили ошибки!

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

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

    10.9 При использовании конструкции Попытка. Исключение… КонецПопытки внутри вложенной транзакции, если внутри этой конструкции возникла восстановимая исключительная ситуация:

    1. при возврате в транзакцию верхнего уровня сведения об исключительной ситуации уже не передадутся.
    2. при возврате в транзакцию верхнего уровня исключительная ситуация будет представлена как откат вложенной транзакции, что будет расценено как невосстановимая ошибка.
    3. на уровне общей транзакции исключительная ситуация также будет расценена как восстановимая.

    Рассмотрим пример в источнике: Филиппов Е.В., Настольная книга 1С:Эксперта по технологическим вопросам. 2-е издание, стр. 49: "Пример 1

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

    Код выполнится. Транзакция успешно завершится. Исключительная ситуация оказалась расценена как восстановимая."

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

    10.10 При использовании конструкции Попытка. Исключение… КонецПопытки снаружи вложенной транзакции, если внутри этой транзакции возникла восстановимая исключительная ситуация:

    1. на уровне общей транзакции исключительная ситуация также будет расценена как восстановимая.
    2. при возврате в транзакцию верхнего уровня сведения об исключительной ситуации уже не передадутся.
    3. при возврате в транзакцию верхнего уровня исключительная ситуация будет представлена как откат вложенной транзакции, что будет расценено как невосстановимая ошибка.

    Ответ на вопрос разобран в примере в источнике: Филиппов Е.В., Настольная книга 1С:Эксперта по технологическим вопросам. 2-е издание, стр. 49-50:
    "Пример 2
    Сначала в процедуру ПриЗаписи модуля справочника Организации добавим строку:

    ЭтотОбъект.ВыполнитьНесуществующийМетод(); // (такого метода у объекта не создавали)

    Далее изменим код из примера 1:

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *