Comments 237
В 1С существуют объектные блокировки, так называемые "оптимистические" и "пессимистические". Кто придумал термин, не знаю, убил бы :). Совершенно невозможно запомнить, какая из них за что отвечает.
Так все очень просто же. Пессимистическая блокировка ускоряет худший случай, когда объект уже кем-то занят. Оптимистическая же ускоряет лучший случай, когда объект свободен.
И эти термины придуманы вовсе не в 1C.
Худший случай — это случай когда объект уже кем-то занят. Для того, чтобы его ускорить, нужно обнаружить этот факт как можно раньше. Для этого и используется вызов Заблокировать()
. При этом в лучшем случае, когда объект свободен, работа замедляется из-за лишнего вызова.
Оптимистическая же блокировка построена так, чтобы в лучшем случае к базе не делался ни один лишний запрос. Ценой того что в худшем случае вся транзакция делалась впустую и будет откачена.
«Оптимист» просто поедет в заведение в надежде, что там окажутся свободные столики. «Пессимист» позвонит заранее и забронирует столик (либо узнает, что в данном заведении свободных столиков нет и не потратит время на пустую поездку).
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
Я обычно в случае отмены вместо Если делаю Пока. Кто его знает, что там в вызванной процедуре наоткрывалось.
Андрей Овсянкин — Троль 80 левела
В 1С нет механизма работы с базой данных без транзакций?
А что, где-то можно работать с базой данных без транзакций?
НачатьТранзакцию()
Пока Выборка.Следующий() Цикл
Попытка
Объект.Записать();
Исключение
Сообщить("Не получилось записать");
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
Что-то типа этого:
Запрос."Обновить 'таблицу' сделать 'поле1' = 'значение1' где 'ид записи' = 1"
другой вопрос, что часто требуется согласованно менять данные в нескольких таблицах, и это без явного управления транзакциями сделать невозможно.
p.s. возможно, конечно, на несвязанных неявных транзакциях, но в случае эксепшена вы сами себе злобный буратино.
Ну, пример.
Вы переводите деньги со счета на счет.
С одного, допустим, успешно сняли.
А вот при добавлении денег на второй счет по какой-то причине запись не прошла.
Без явных транзакций деньги просто потеряются в вашей кривой программе. Да, в тот самый момент, когда код будет начислять вам зарплату:)
Попытка
НачатьТранзакцию();
Исключение
КонецПопытки;
(https://its.1c.ru/db/content/metod8dev/src/developers/platform/metod/other/i8102313.htm?_=1533744348)
its.1c.ru/db/v83doc/bookmark/dev/TI000000528
Ну и цитата из ссылки:
В зависимости от характера произошедшей ошибки возможны различные сценарии обработки этой ситуации.
Если произошедшая ошибка не связана с базой данных, то возможно продолжение транзакции и дальнейшей работы модуля. Если разработчик считает это необходимым, он может отменить транзакцию или, наоборот, продолжить выполнение транзакции, если произошедшая ошибка не нарушает атомарность транзакции.
Если же исключительная ситуация была вызвана ошибкой базы данных, то система фиксирует факт возникновения ошибки в этой транзакции, и дальнейшее продолжение транзакции или ее фиксация становятся невозможны. Единственная операция с базой данных, которую разработчик может произвести в данной ситуации, ‑ это отмена транзакции. После этого он может осуществить попытку выполнения этой транзакции еще раз.
Причина исключения, действительно, не предоставляется
И, насколько я понимаю, невосстановимость исключения заключается в том, что в некоторых случаях нельзя продолжить исполнение встроенного языка.
Невосстановимая ошибка не ловится
не правда, у вас не верное понимание что есть восстановимая, а что нет.
Берем для примера такой код
НачатьТранзакцию();
ЗаписатьВБазу();
Попытка
а = 1 /0;
Исключение
КонецПопытки;
ПрочитатьИзБазы();
ЗафиксироватьТранзакцию();
это есть восстановимое исключение т.к. ничего страшного не произойдет в этом случаи.
Теперь меняем код на такой:
НачатьТранзакцию();
Попытка
ЗаписатьВБазу();
Исключение
КонецПопытки;
ПрочитатьИзБазы();
ЗафиксироватьТранзакцию();
в модуль объекта при записи мы переносим эту строку
а = 1 /0;
Вот теперь при попытки прочитать из базы мы получим ту самую ошибку «В данной транзакции уже происходили ошибки»
Вот это и есть невосстановимая ошибка!
А вот если мы перенесем это в событие при записи
Попытка
а = 1 /0;
Исключение
КонецПопытки;
ошибки «В данной транзакции уже происходили ошибки» не произойдет. Так что понятие «восстановимое/не восстановимое исключение» это довольно таки тонкая грань которую нельзя формализировать.
В данном случае я просто постулирую, что я прав, а вы — нет :)
Причин много, но одна вот:
Невосстановимые ошибки ‑ это ошибки, при возникновении которых нормальное функционирование системы «1С: Предприятие» может быть нарушено, например, могут быть испорчены данные. При возникновении невосстановимой ошибки выполнение системы «1С: Предприятие» прекращается в любом случае.
Это цитата из платформенной документации. Именно это фирма «1С» считает невосстановимой ошибкой. При этом совершенно безразлично, где эта ошибка случится — в транзакции или вне транзакции. Результат будет один — при возникновении невосстановимой ошибки ваш код полностью прекратит свою работу. При этом прекратит работу также и клиентское приложение, а также — может умереть и сервер.
А дальше опять читаем цитату из поста habr.com/post/419715/#comment_18990099 или или по ссылке из того же поста читаем весь раздел документации.
Не надо придумывать, что фирма «1С» понимает под какими-то терминами, особенно если сама фирма «1С» привела в своей документации прямые определения :)
Просто поверьте мне безотносительно — невосстановимое исключение вы во встроенном языке не поймаете (т.е. это аксиома). Соответственно: все остальные исключения — восстановимые. И вот как восстановимое исключение обрабатывается платформой при использовании транзакций — это и есть тема обсуждения текущей статьи.
И чтобы обсуждать вопросы с единым использованием применяемых терминов — лучше использовать терминологию вендора, а не придуманную самостоятельно.
По вашей же ссылке во втором абзаце написано обратное. Данная ошибка может возникать при восстановимом исключении на работе с БД. Причем не обязательно на записи, исключение при чтении тоже выкинет "в данной транзакции уже происходили ощибки" при повторном обращении к бд
Попытка
НачатьТранзакцию();
Действия();
ЗафиксироватьТранзакцию();
Исключение
КонецПопытки;
Почему именно так: все, что находится внутри Попытки, убивается в случае ошибок в Исключении, т.е. нет нужды отменять транзакцию, она отменится автоматически. Фиксация транзакции расположена последней в очереди внутри попытки. По-моему, вопрос закрыт. Как вы считаете?
Она отменится автоматически только в конце серверного вызова, либо когда кто-то выше по стеку не сделает отмену либо фиксацию. Без явной отмены в блоке попытки весь код выше по стеку, выполняемый после вызова данного куска с попыткой будет в открытой поломанной транзакции
Причина появления данной ошибки — вызов исключительной ситуации во вложенной транзакции. Попытка создает неявную транзакцию, тем самым исключение откатывает транзакцию полностью.
Внутри вложенной транзакции вызывается исключительная ситуация, что откатывает всю транзакцию и некорректно отрабатывает. Например:
НачатьТранзакцию();
…
…
Попытка
…
Исключение // если тут будет вызвано исключение — то вы увидите ошибку «В данной транзакции уже происходили ошибки»
…
КонецПопытки;
…
КонецТранзакцию();
Во избежания таких ситуаций, нужно избегать использования попытки внутри транзакции.
avditor.ru/index.php/programmirovanie-1s/102-1s-oshibka-v-dannoj-tranzaktsii-uzhe-proiskhodili-oshibki
1) Если внутри открытой транзакции происходит ошибка, то происходит откат ранее записанного и прерывание выполнения (без использования попыток мы вообще не попадем на «В данной транзакции уже происходили ошибки»)
2) Не очень знаком с неявными транзакциями «попыток» (они явно не полноценные, так как если в попытке сделать запись нескольких элементов и на одном из них получить ошибку, то предыдущие останутся записанными в базу), но хорошо, что они закрываются и не выходят за рамки блока try-catch. А тем временем у нас «сверху» все еще есть активная транзакция.
3) Использование «попытки» внутри активной транзакции позволяет продолжить выполнение кода после «пойманной» ошибки, но каждая попытка продолжить работу с СУБД (даже чтение) будет генерировать новую ошибку (вышеозвученную), пока транзакцию программно не закроют (или не прекратится выполнение кода, но тогда произойдет откат).
// Путевка в ад, серьезный разговор с автором о наших сложных трудовых отношениях.
То есть Вы не только сами это используете но и других учите. Более того как бы незаметно намекаете что Вы не тоько учите но вершите там судьбы. Поэтому разберу немного В/аш подход.
Вы пытаетесь обойти защиту от дурака которую разработчики 1с заложили в совю систему. Зачем Вы это делаете? Если транзакция началась (Вы же ее сами нгачинаете) и что-то внутри произошло то транзакция долна откатиться. В этом ее смысл и предназначение. если Вы хотите все же что-то сохранить. То не начинайте эту транзакцию.
О, Господи… ну вот вам, господа, и пример компетенций в отрасли.
Посмотрите внимательно — что произойдет, если «ДелаемЧтоТо()» будет написана по стандарту 1С и отменит транзакцию внутри себя? Транзакция в нашей функции не будет завершена и вывалится в исключение с незакрытой(!) транзакцией. В стандарте 1С такое исключено — при любых ошибках транзакция безусловно закрывается.
Более того, где гарантия, что ЗафиксироватьТранзакцию() — самая главная часть нашей логики вообще выполниться? В приведенной методике ловить это прискорбное событие должна почему-то вызывающая функция, которой, в общем виде, до этого нет никакого дела.
Вариант от 1С обеспечивает более целостную и модульную обработку транзакций и их ошибок, и гарантированно фиксирует или откатывает транзакцию.
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
лог.Сообщение(лог_Ошибка, ПодробноеПредставлениеОшибки());
КонецПопытки;
Вы в коде отчего-то предположили, что вложенная функция при ошибке непременно вызывает исключение или не обрабатывает возникающее внутри себя исключение. Это очень странная предпосылка — даже невнимательные 1С-ники все же пишут код который в норме не вызывает исключения или стараются хоть как-то обработать исключения в своём коде.
Т. е. в этом случае мы не потеряем текст ошибки на строке ОтменитьТранзакцию()?
Получается ещё одно исключение из правил, которое нужно знать.
Посмотрите внимательно — что произойдет, если «ДелаемЧтоТо()» будет написана по стандарту 1С и отменит транзакцию внутри себя?
Вариант от 1С обеспечивает более целостную и модульную обработку транзакций и их ошибок, и гарантированно фиксирует или откатывает транзакцию.
НачатьТранзакцию(); Попытка ДелаемЧтоТо(); ЗафиксироватьТранзакцию(); Исключение ОтменитьТранзакцию(); лог.Сообщение(лог_Ошибка, ПодробноеПредставлениеОшибки()); КонецПопытки;
Вижу противоречие или неточное формулирование
Если в «ДелаемЧтоТо()» будет откат транзакции, которую не она начала, код вывалится по ошибке на фиксации транзакции.
Автор статьи как раз и говорит, что нужно управлять только своими транзакциями, а не чужими.
Даже если внутри «ДелаемЧтоТо()» по ошибке будет отменена наша транзакция, то совсем необязательно мы попадем в Исключение, но точно не сможем зафиксировать транзакцию.
Вы невнимательны :(
я уже писал, напишу еще раз, чуть перефразировав:
Если нет активной транзакции, что верно при нашем условии "в «ДелаемЧтоТо()» будет откат транзакции", тогда ЗафиксироватьТранзакцию само выдаст исключение :(
в итоге мы в любом случае попадем в исключение.
в итоге мы в любом случае попадем в исключение.
Безусловно. Но есть момент:
1. Мы в итоге не отменили свою транзакцию.
2. Наше исключение вынужден ловить вышестоящий код, и он же должен решать что делать с нашей транзакцией. Это при том, что у него может быть и своя собственная транзакция, и если обработка транзакции написана так же, то проблема идет еще на уровень выше.
Как-то это не очень масшабируемо и модульно.
У них часто и подписки на ИТС нет, по которой эти стандарты можно прочесть.
Методическая поддержка по платформе и стандарты разработки доступны без подписки (в отличие от док на разные продукты).
Я так понимаю канал неофициальный, но ведет кто то из сотрудников.
Справедливости ради открыли они их недавно. Еще года полтора назад заходил — были закрыты.
Пользуюсь стандартами разработки с 13 года. Не верите? Смотрите сами https://web.archive.org/web/20130417015349/https://its.1c.ru/#dev
Стандарты действительно открыты недавно — с апреля 18 года.
Так стандарты же примерно полгода назад открыли всем :)
Читай без подписки на здоровье.
Кстати в АПК проверка конкретно этого стандарта по транзакциям тоже реализована.
Полгода назад — слишком мало, чтобы массы про это узнали. Я уже писал выше, что никто и не думает лезть в ИТС, зная, что там все закрыто.
То, что ИТС постепенно открывается — это очень хорошо. Но популяризируете вы этот факт мало.
Ну https://github.com/VladFrost открыл канал в телеграмме t.me/v8std
477 подписчиков сейчас
Всем советую :)
Поэтому рекомендуемый в документации подход, при котором ЗафиксироватьТранзакцию находится внутри Попытка/Исключение — более правильный.
Да и вопрос не в том, когда ЗафиксироватьТранзакцию может стрелять исключением. Вопрос в том, что в вашей логике — изъян. И если я прав (а я прав :) ), то своей статьей вы провоцируете разработчиков 1С на неправильное поведение, т.к. вы стремитесь к тому, чтобы счетчик транзакций в начале и конце метода совпадал, а я предъявляю ситуацию, когда он не совпадает.
В статье пояснил почему — из-за риска затереть возникшую проблему новой.
Если выставлять условие на отмену транзакции, то не выполняется требование безусловности фиксации или отмены транзакции внутри кода.
Чтобы не породить новую проблему (исключение при отмене) вызов ОтменитьТранзакцию() тоже должен быть обернут в Попытка-Исключение.
Условие в блоке исключений нужно только если вызывать код написанный не по стандарту.
В общем случае он не нужен.
Потому все просто: если коду вызываемому доверяешь — проверка не нужна.
Мой перфекционизм требует приводить к стандарту вызываемый код. :)
Не идеальны. Но они проверены специалистами разных групп и разработчиками платформы и согласованы между собой.
Все ошибки и пожелания к стандартам рассматриваются и исправляются в рамках партнерской конференции БСП (https://partners.v8.1c.ru/forum/forum/186/topics).
Это уже другие затраты ресурсов, которые могут не иметь отношения к решаемой задаче.
мы же пишем код под свою задачу и, возможно, переиспользуем существующий код.
и правильнее написать код с проверкой, чем непонятно почему доверять коду, который "может быть" написан по стандарту.
изменение уже существующего и используемого кода может быть совсем тяжелым :(
PS если что, принцип "бойскаута" я люблю и сам применяю
Понятное дело, но тут тоже можно поспорить :)
Есть база данных угроз ФСТЭКА, там есть замечательная угроза УБИ.165
https://bdu.fstec.ru/threat/ubi.165
Угроза включения в проект не достоверно испытанных компонентов
Переиспользование кода без испытаний влечет к понижению безопасности проекта, потому в любом случае надо делать аудит кода, и тогда уже принимать решение "байскаутить" или же пилить адаптер безопасного вызова.
Я не помню на практике таких ситуаций. Просветите, пожалуйста
Как минимум, один вендор СУБД прямо говорит, что при вызове фиксации или отмены транзакции всегда надо быть готовым поймать исключение:
Try/Catch exception handling should always be used when committing or rolling back a SqlTransaction. Both Commit and Rollback generates an InvalidOperationException if the connection is terminated or if the transaction has already been rolled back on the server.
Конечно, на практике такое встретить сложно, но ведь мы как раз о том как правильно работать с транзакциями, а не о том, что на практике что-то кто-то не видел.
Очевидный вариант — предполагает два последовательных вызова ОтменитьТранзакцию().
НачатьТранзакцию();
Попытка
ЧтоТо = ДелаемЧтоТо();
Если ЧтоТо Тогда
ЗафиксироватьТранзакцию();
Иначе
ОтменитьТранзакцию();
КонецЕсли;
Исключение
ОтменитьТранзакцию();
лог.Сообщение(лог_Ошибка, ПодробноеПредставлениеОшибки());
КонецПопытки;
сознательно бросать исключение нужно только с расчетом, что его увидит пользователь.
Блин, кто вам такое сказал? Сознательно бросать исключение нужно тогда, когда нужно, когда есть исключительная ситуация, с которой данный слой архитектуры не в состоянии справиться. Мы даже параметризовывали исключения и передавали в них Структуры, когда надо было.
Хорошая статья. Единственно только хочу обсудить правильное решение по паттерну, где используется функция ТранзакцияАктивна().
Чтобы выполнить обязательства по одному открытию и одному закрытию транзакции, может быть имеет смысл сначала этот признак записывать в переменную, а потом уже писать участок кода, работающий с транзакцией? Вот так:
ОткрытьЗакрытьТранзакцию = НЕ ТранзакцияАктивна();
Если ОткрытьЗакрытьТранзакцию Тогда
НачатьТранзакцию();
КонецЕсли;
Попытка
ДелаемЧтоТо();
Исключение
Если ТранзакцияАктивна() Тогда // здесь так, потому что
// транзакции может уже не быть
ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение ОписаниеОшибки();
КонецЕсли;
Если ОткрытьЗакрытьТранзакцию Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
ВызватьИсключение ОписаниеОшибки();
лучше не делать, просто ВызватьИсключение достаточно, так будет проброшено исходное исключение. Синтаксис доступен только в блоке catch
На счет паттерна:
- ЗафиксироватьТранзакцию(); обязательно должен быть в блоке Попытка-Исключение
- Не должно быть логических операций между НачатьТранзакцию(); и Попытка
- ОтменитьТранзакцию(); должна быть первой операцией в блоке Исключение-КонецПопытки
ТранзакцияАктивна() не обязательно использовать для начала новой транзакции, читайте документацию по транзакциям. Открытие новой транзакции в существующей — откроет вложенную транзакцию, закрытие — закроет вложенную, но нужна повторная фиксация чтобы закрыть основную (как раз ваш случай), отмена транзакции отменяет все, включая весь стек сложенных.
Весь код приходит в вид:
НачатьТранзакцию(); // Если транзакция открыта - откроется вложенная
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию(); // если это вложенная - произойдет переход к основной, иначе произойдет фиксация в базу
Исключение
ОтменитьТранзакцию(); // Отменятся все транзакции, даже если это вложенная
ВызватьИсключение; // Выбросится изначальное исключение
КонецПопытки;
лучше не делать, просто ВызватьИсключение достаточно, так будет проброшено исходное исключение
Спасибо за совет! Попробовал — яростно плюсую. Не знал что так можно.
Когда автор так написал, я думал, что он просто сократил для наглядности.
По поводу конструкции с условием, сегодня начал вспоминать, откуда оно у меня взялось? И вспомнил, что это была попытка использовать один код, который бы работал как в объектах с автоматическими блокировками, так и отдельно — с управляемыми (старая конфигурация). Нужен пример, но скорее всего там требовался просто более глубокий рефакторинг. Финальный вид, который приведён в статье — максимально правильный, и если появляется необходимость написать иначе — вопрос к правильности такой необходимости.
По остальному, спасибо за объяснение. Я уже понял, что нужно так, почитал комментарии из верхних веток.
Попытка
НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда // <--------
ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Это если посмотреть стандарт «Перехват исключений в коде», как Вы посоветовали ниже.
Это условие далеко не всегда будет Истина, к огромному сожалению. Поэтому и статья написалась.
Если в ДелаемЧтоТо(); будет ОтменитьТранзакцию(); без НачатьТранзакцию(); то будет проблема.
Но это значит, что ДелаемЧтоТо() написан не по стандарту, т.к. не соблюдает парность операций.
Потому проверка для всего кода написанного по стандарту не нужна, а если вызываешь чей-то код, которому не доверяешь — то либо надо сделать проверку, либо, поступить правильно и переписать вызываемый ДелаемЧтоТо() по стандарту.
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
И опять двадцать пять :(
Этот вариант по стандарту, но он может непредсказуемо падать, если ДелаемЧтоТо или его внутренности работают неверно.
Последствия:
1 И обнаружится это только в рантайме
2 и исходное исключение будет перекрыто исключением, возникающим при отмене транзакции в нашем же блоке исключения
т.е. мы тупо потеряем весь контекст проблемы :(
3 и наверняка возникнет там, где сложно будет проверить и восстановить исходную ситуацию с падением
нам проверку на активность транзакции диктует опыт использования различных конфигураций 1С, в т.ч. и типовых от 1С, написанных/доработанных специалистами разного же уровня, за многие годы использования 1С.
И, тут в комментариях есть фрагмент документации от Microsoft который именно так и рекомендует делать.
Про стандарты есть момент, что стандарт это не данное нам откровение свыше и вполне может меняться.
transaction = connection.BeginTransaction("SampleTransaction");
try
{
doWhatever();
transaction.Commit();
}
catch (Exception ex)
{
Console.WriteLine("Message: {0}", ex.Message);
try
{
transaction.Rollback();
}
catch (Exception ex2)
{
Console.WriteLine("Message: {0}", ex2.Message);
}
}
И весьма прозрачно транслируется в код 1С:
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Сообщить(ОписаниеОшибки());
Попытка
ОтменитьТранзакцию();
Исключение
Сообщить(ОписаниеОшибки());
КонецПопытки;
КонецПопытки;
1 Но все-таки ОтменитьТранзакцию имеет намного-намного меньше шансов упасть, если мы предварительно убедимся, что есть открытые транзакции через Транзакция Активна.
2 И пример нехороший совсем.
Зачем «Сообщить» то? и где переброс исключений на верхний уровень? и т.п. и т.д.
явное зло.
Не понимаю чем Сообщить лучше ВызватьИсключение.
Обработка вызова исключения корректно дойдет до пользователя и покажет ошибку.
Обрабатывать дополнительно ошибку отмены транзакции не нужно.
Имеет смысл код обработки ошибки отмены транзакции только в случае, когда результат кода должен дойти куда-нубудь вне инетрефейса 1С, например возвратов веб-сервиса или http-сервиса.
Но даже там проще обернуть в попытку исключение вызов бизнес-логики чтобы корректно сформировать код возврата вне зависимости от того ошибка это базы или бизнес-логики, потому что конечно потребителю это собственно все равно, ему главное получить код статуса ошибки.
Обрабатывать дополнительно ошибку отмены транзакции не нужно.
Не нужна Попытка как таковая, или не нужно ничего делать в Исключение-КонецПопытки?
Если ничего не нужно делать в блоке исключений (как с отменой транзакции) то и попытка не нужна. Гасить исключения бесследно — плохой тон. Есть очень редкие случаи, когда исключение не приводит к проблемам и можно его заменить на запись в журнал регистрации типом предупреждение, но в общем исключение должно быть выброшено пользователю и при этом платформа сама его запишет в журнал как ошибку.
Гасить исключения бесследно — плохой тон.
Какой уж там «плохой тон». Это прямо запрещено стандартом "Перехват исключений в коде":
3.4. Недопустимо перехватывать любые исключения, бесследно для системного администратора
Про запись в ЖР там тоже есть.
открытые транзакции через Транзакция Активна
Нарушается принцип безусловности — если мы открыли транзакцию, то мы должны или зафиксировать её либо отменить.
Если ОтменитьТранзакцию() выдаст исключение, то значит ДелаемЧтоТо() написана неправильно, и, чем раньше по стеку вызовов мы это отловим, тем лучше.
Зачем «Сообщить» то?
В конкретном примере это «калька» с кода выше, не более.
и где переброс исключений на верхний уровень?
А он точно есть? Может этот код прямо из команды формы вызывается — и пользователь получит «красивое» окно на полэкрана.
Наверно все же запись в ЖР обязательна, а необходимость выброса исключения зависит от бизнес-логики. В стандарте "Перехват исключений в коде" есть три варианта развития событий:
1. Вызов исключения
2. Использование кодов возврата
3. Перехват исключения незаметно для пользователя
Окошко действительно красивое. Если платформа запущена под отладкой — то доступен стек и пепеход к дизайнеру, но если запущена в штатном режиме, то это окно как обычное предупреждение.
Хотя есть случаи когда действия по стандарту приводят к исключению, которое нельзя перехватить и пользователь на себе ощущает всю дружелюбность интерфейса.
Для отладки есть отличный флаг «Останавливаться по ошибке» как раз для таких случаев.
Я не об этом. Я о том, что в зависимости от того разрешена ли отладка в текущем сеансе окно меняется.
Обработка:
&НаСервереБезКонтекста
Процедура ТестВызватьИсключениеНаСервере()
ВызватьИсключение НСтр("ru = 'Тест'");
КонецПроцедуры
&НаКлиенте
Процедура ТестВызватьИсключение(Команда)
ТестВызватьИсключениеНаСервере();
КонецПроцедуры
Если включена:
Если отключена:
Сценарий транзакции должен начинать транзакцию и завершать ее, находясь в начале стека выполнения на сервере. Вызов извне будет только из клиентского события. Тогда не нужно оборачивать транзакцию в попытку, так как она откатится естественным образом. И никаких НачатьТранзакцию в глубине стека! Можно вызывать процедуры модификации данных, но они не должны начинать транзакции. Только код обработки клиентского запроса на верхнем уровне знает, когда начинается транзакция – собственно, когда обрабатывается клиентский запрос.
никаких НачатьТранзакцию в глубине стека
Вот в корне не соглашусь! Это усложняет изоляцию модулей, увеличивает сложность и вообще…
Если вызвать эти процедуры последовательно, понадеявшись на их корректность, мы потеряем атомарность. Например, ИзменитьОкладыСотрудников сработает, а ИзменитьОкладыДолжностей – нет. В итоге в справочнике сотрудников будут новые оклады, а в справочнике должностей – старые. Нельзя менять оклады сотрудников, не меняя оклады должностей, и наоборот. Эти данные должны быть согласованы. Поэтому нужно главную процедуру обработки выполнять в транзакции.
А внутренние транзакции не имеют смысла. Зачем в процедуре, которая что-то выполняет и сделана для вызова из других процедур, внутренняя транзакция? Все равно будет более общая транзакция, которая объединит все модификации данных и сделает это согласованно. В итоге получается такой вариант:
Процедура ОченьПолезныйИВажныйКод(СписокСсылокСправочника)
Для Каждого Ссылка Из СписокСсылокСправочника Цикл
ОбъектСправочника = Ссылка.ПолучитьОбъект();
ОбъектСправочника.КакоеТоПоле = "Я изменен из программного кода";
ОбъектСправочника.Записать();
КонецЦикла;
КонецПроцедуры
Список ссылок нужно получать из запроса ДЛЯ ИЗМЕНЕНИЯ.
Список ссылок нужно получать из запроса ДЛЯ ИЗМЕНЕНИЯ.
Кхм, автоматические блокировки? В 2018 году? Сурово.
Попытка
НачатьТранзакцию();
ИзменитьОкладыСотрудников();
ИзменитьОкладыДолжностей();
ЗафиксироватьТранзакцию();
Исключение
// здесь код из статьи
КонецПопытки;
В примере нам неважно — есть ли вызовы НачатьТранзакцию внутри операций. Нам важно, что ОБЕ они должны быть выполнены атомарно. Это обеспечивает обрамляющая транзакция, а внутренние транзакции этих методов становятся просто «увеличителями счетчиков». Таким образом целостность обеспечивается просто и прозрачно.
Обычная комбинаторика модулей и повторное использование, на этом держится весь IT.
Предположим, у вас в этом методе должны атомарно выполниться изменения по всем сотрудникам сразу. И этот метод вы можете вызывать, как в рамках указанного примера (вместе с должностями), так и сам по себе — только по сотрудникам. Тогда этот метод изолирован и независим. Он внутри транзакционно по всем сотрудникам делает начисления. А еще, его можно повторно использовать в более крупной операции, когда и по сотрудникам и по должностям меняем оклады.
RollBack забыл первой строкой в исключении
Почему вы отказываете в праве вложенности явным транзакциям?
А во-вторых, во вложенных транзакциях нет никакой необходимости. Атомарность – первое свойство транзакции. Изменения данных инициирует клиентский запрос, который может быть вызван нажатием кнопки в форме, да и вообще любым интерактивным действием, это может быть вызов web-сервиса или запуск платформы из bat-файла с автоматическим выполнением обработки, обращение через COM-соединение и так далее. Во всех этих случаях есть главная процедура, которая обрабатывает клиентский запрос. Именно она и должна начинать и фиксировать транзакцию. С точки зрения пользователя вложенных транзакций нет. Он либо получает атомарно изменения, которые инициировал, либо нет. В этом и заключается атомарность.
Попытка
НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Вот только не понимаю, зачем нужно
ВызватьИсключение;
Хватит изобретать велосипед, есть пример в стандарте "Перехват исключений в коде"
3.6. При использовании транзакций следует придерживаться следующей схемы обработки исключений в коде на сервере:
// 1. Начало транзакции
НачатьТранзакцию();
Попытка
// 2. Вся логика блокировки и обработки данных размещается в блоке Попытка-Исключение
Запрос = Новый Запрос("...");
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
...
КонецЦикла;
// 3. В самом конце обработки данных выполняется попытка зафиксировать транзакцию
ЗафиксироватьТранзакцию();
Исключение
// 4. В случае любых проблем с СУБД, транзакция сначала отменяется...
ОтменитьТранзакцию();
// 5. ...затем проблема фиксируется в журнале регистрации...
ЗаписьЖурналаРегистрации(НСтр("ru = 'Выполнение операции'"), УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
// 6. ... после чего, проблема передается дальше вызывающему коду.
ВызватьИсключение;
КонецПопытки;
Поскольку исключение не отменяет транзакцию сразу, но запрещает успешное завершение транзакции, то все вызовы НачатьТранзакцию, с одной стороны, и ЗафиксироватьТранзакцию или ОтменитьТранзакцию, с другой стороны, должны быть парными.
Отсутствие обработки исключительных ситуаций приводит к зависшим транзакциям и к сложнодиагностируемым ошибкам вида «В этой транзакции уже происходили ошибки» в произвольных местах кода. При этом в случае вложенных операторов НачатьТранзакцию, следует обеспечить вызов всех парных операторов ОтменитьТранзакцию. Для этого в конце блока Исключение необходимо пробросить исключение выше по стеку с помощью ВызватьИсключение (как в примере выше) и соответствующим образом обработать исключение на каждом уровне стека.
Нормальный программист не будет помещать код, который не должен генерировать исключений в try-catch, как например это сделано с созданием объема Запрос в вашем примере. Ровно так же, как и перехват исключений потенциально разной природы (к сожалению типизации в 1С нет) одним блоком — плохой стиль.
Потом, привыкнув, программисты начинают оборачивать вызовы функций в try-catch, и сиди потом, гадай, действительно там предполагается выброс исключений или они просто от «быдлокода» страхуются.
Ну и еще с вашей т.з. очевидно «не очень профессиональные люди, которые не могут нормально настроить систему»
Не понял этот тезис. Я не говорил такого. Постараюсь раскрыть: ELK стек нужен тем, у кого много логов, т.е. на крупных внедрениях, тем у кого есть как минимум лишний сервер под Elastic и прочее. То есть, это чисто корпоративный продукт. И вполне нормально, когда компания просто покупает себе систему хранения логов от 1С не тратя собственные ресурсы на разработку и хождение по граблям. Не совсем ясно, в чем Ваша претензия?
Единицы настраивают не потому что не хотят или ленятся. А потому что фиг настроишь.
Если серьезно, то это тема для отдельной статьи или разработки. Эти знания не то чтобы сильно уникальны, но действительно единичны. Мы стараемся продавать ELK-стек для 1С, как услугу. Это не жадность и не «пилить бабло», обычное коммерческое решение, и я не вижу нет ничего крамольного, в том чтобы продавать ПО и его поддержку.
Мы можем настроить ELK или GrayLog или любую другую систему обработки логов, сэкономив компании человекомесяц классного специалиста. Разве это плохо?
Теперь и я за деньги настраиваю ELK, о котором мне когда-то поведала пуля. Намучался я с ним, правда, достаточно сильно и много.
Кстати, про ELK и 1С можно было много услышать на хакатоне isthisdesign.org. Там ~4 способа отправки ЖР в ЕЛК разобрали
Ну и, к слову, куда контрибьютить, если вы закрыли проект на github?
Если исключение из НачатьТранзакцию (платформа не увеличит счетчик транзакций), то вы откатите чужую транзакцию при обработке исключения.
Хотя на практике я не видел, чтобы НачатьТранзакцию приводило к исключению.
Так вот, на 3 из 4 поддерживаемых СУБД, при вызове НачатьТранзакцию платформа идет в СУБД и открывает там транзакцию. В этот момент может произойти все что угодно: нехватка памяти для очередного соединения с СУБД, разрыв связи, ошибка дисковой подсистемы.
Давайте все-таки делать, как написано в документации: НачатьТранзакцию до начала Попытки, Зафиксировать — внутри, Откатить — в обработке исключения. Стандартная для всех языков практика, в общем-то. У вас статья как называется?
А если и будет «перехватываемое» исключение, то в блоке Исключение оно также безопасно будет отработано за счет условия с ТранзакцияАктивна. Получается что мой вариант надежен и так и этак. А надежный код — это хорошо.
Насчет производительности — опять непонятно. Как принципиально влияет на производительность размещение НачатьТранзакцию до или внутри Попытки?
Насчет производительности — здесь неактуально. Вы написали, что руководствовались эстетическими соображениями для переноса строчки кода. Я же рассматривал более общий случай.
По статье. Первая ошибка немного притянута за уши, потому что метод Заблокировать() в свою очередь также кидает исключение, ровно как и неудачный Записать ().
Еще два вопроса практического характера.
Вдруг он там взял, да и вызвал метод ОтменитьТранзакцию или наоборот, зафиксировал ее? и финальный вариант:
Попытка НачатьТранзакцию(); ДелаемЧтоТо(); ЗафиксироватьТранзакцию(); Исключение Если ТранзакцияАктивна() Тогда ОтменитьТранзакцию(); КонецЕсли; ВызватьИсключение; КонецПопытки;
На каком основании делается вывод, что ДелаемЧтоТо(); начал и не закончил транзакцию лишь однажды, а если это произошло два или более раз?
Подскажите пожалуйста (отбросив на время все прописные истины) на практическом примере пагубность использования подхода, при котором мы будем заботиться о транзакциях там, где это нужно, а не на всякий случай везде. Давайте на минутку представим, что во всей конфигурации мы не втыкаем попытки/исключения там где видим НачатьТранзацию, а отматываем транзакции там, где логика кода позволяет случаться исключениям, ведь таковых существенно меньше, чем первых, не говоря о том, что можно допустить механическую ошибку.
Как пример: перепроведение документов; мы только в том куске кода, который отвечает за “записать” — делаем попытку/исключение с отматыванием тразнаций через ТранзакцияАктивна () в цикле. Отматывая их назад 1) Мы не можем утверждать, что мы отмотали чужую транзакцию (до нас), это мог сделать вызывающий нами ненадежный код 2) Отсутствие вложенности транзакций заставляет их открутить до 0 каждый раз, перед тем, как продолжить работать с базой.
На счет комментария:
Отменять эту внешнюю транзакцию ваш код не должен, так как внешний код сам должен обрабатывать свои транзакции!
Мне не хватает практического опыта, чтобы подтвердить это правило, учитывая, что ЗафиксироватьТранзакцию () вне любой другой пары просто работает как счетчик, ничего не фиксируя в базе.
Я это читаю как
Я очень умный специалист, эти ваши стандарты кодирования для юных падаванов, я сам решу где мне и что писать
гм… не знаю что заставляет так думать, надеялся на сугубо профессиональный ответ.
неизбежно приводит нас к вопросу «А где нужно?»
Мы с коллегами предпочитаем писать код, который понимаем (пусть даже не всегда правильно), чем писать код, который мы не понимаем, но зато по стандартам. Поэтому вопросов, где нам нужно класть транзакцию в исключение у нас нет, потому что это часть логики программы. А исходный вопрос, по сути, очень прикладной: вставлять везде начать транзакцию в попытку можно механически и забыть (не будем сейчас говорить о анализаторе не анализируемого в принципе кода), а вот если мы понимаем, где нам нужно перехватывать её (если вы не понимаете где — продумайте вначале алгоритм) — ошибиться уже сложней.
В любом случае, не хотелось бы просто цитат, был бы очень полезным практический пример.
«Не рекомендуется использовать циклы по условно бесконечной выборке внутри операторных скобок Начать и Зафиксировать транзакцию»
Очень интересно, особенно если учесть, что кол-во этих вызовов в итоге будет одинаковым, просто вынесенным из исключений нижестоящих вызовов.
На каком основании делается вывод, что ДелаемЧтоТо(); начал и не закончил транзакцию лишь однажды, а если это произошло два или более раз?
Насколько я понял, защита тут от ровно противоположного — вдруг ДелаемЧтоТо()
не начинал транзакцию, но ее закончил.
НачатьТранзакцию() Пока Выборка.Следующий() Цикл // чтение объекта по ссылке // запись объекта КонецЦикла; ЗафиксироватьТранзакцию();
ввести управляемую блокировку во избежание deadlock
ввести вызов метода Заблокировать
обернуть в «попытку», как показано выше
После управляемой блокировки применять Заблокировать () уже не нужно.
Привожу.
Секунда 1. (события внутри секунды произошли в порядке следования)
- Пользователь открыл форму документа
- Фоновый поток с вашим кодом установил упр. блокировку и сделал ПолучитьОбъект()
- Пользователь изменил строчку в форме и тем самым установил пессимистическую блокировку (считай, что вызвал ДокументОбъект.Заблокировать())
Секунда 2:
- Ваш фоновый поток не вызывал ДокументОбъект.Заблокировать() — и это ошибка
- Ваш фоновый поток, проигнорировал занятость объекта и вызвал метод Записать()
Секунда 3:
- Пользователь нажал кнопку "Записать", полагая успешную операцию и получил ошибку "Объект изменен или удален" (сработала оптимичтическая блокировка)
Секунда 28:
- Пользователь пишет письмо в саппорт с предположением (в данном случае резонным), что программисты
мудакиплохо работают.
Правильный сценарий
- В секунду №1 ваш код вызывает ДокументОбъект.Заблокировать() сразу после получения объекта. И если не смог заблокировать — отказывается от операции.
- Пользователь, пытаясь изменить поле формы на вашем уже пессимистически заблокированном объекте получает внятное диагностическое сообщение "Объект А уже редактируется пользователем Х на компьютере Y".
И теперь, даже если в секунду 28 пользователь пишет письмо в саппорт, то ему можно ответить, что это он не умеет читать сообщения на экране.
Меня резанула строгость паттерна, и если сочтете нужным, то возможно, имеет смысл смягчить формулировку в статье на счет Заброкировать (), потому что в высоко нагруженных системах, откатывать транзакцию может быть непозволительной роскошью ради забывчивого пользователя (усыпляют компьютеры в режиме редактирования, а с мобильным клиентом, эта проблема происходит чаще, плюс мы сталкивались, что оптимистичная не всегда уходит через 20 минут, а если уходит — то с сообщением Session is not available or has been dropped). Нередко наихудшими, являются последствия невыполнения сложных алгоритмом в фоне, чем выброс объекта пользователя.
НачатьТранзакцию();
Попытка
//здесь формирую и провожу документы программно
...
ЗафикситоватьТранзакцию();
Исключение
ОтменитьТранзакцию()//тут ошибка "Транзакция не активна"
КонецПопытки;
С одной стороны автор пишет «мы обязаны заботиться только о нашей транзакции», и тут же пытается проверить состояние общего счетчика транзакции «Если ТранзакцияАктивна() Тогда ОтменитьТранзакцию(); ...». Транзакцию какого уровня пытаетесь отменить?
На самом деле _код_ не так плох, как вам кажется и оптимистичные подходы имеют подавляющее большинство. Количество вызовов ОтменитьТранзакцию/ТранзакцияАктивна в коде… ммм… УТ11 имеет соотношение 504/50, т.е. 10 к 1 (опять же большая часть которых относится к логике _текущего_ контекста).
Предложенный в статье финальный вариант в общем случае является неверным и имеет смысл применять только как костыль отладки ошибки «В данной транзакции уже ...»
И автор аргументировал, и я расшифровал резоны в
habr.com/post/419715/#comment_18995839
Сонар не содержит в себе всех проверок, а только расширяет базовые проверки.
Например для проверки python сонар использует выгрузку результата проверки pylint.
ЦелоеЧисло Ж;
Ж=1;
НачатьТранзакцию();
Ж=2;
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ( ТранзакцияАктивна() И Ж=2)
Тогда ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Если коротко: Ж
Если инструкция НачатьТранзакцию() выбросит исключение
то попадаем в блок перехвата исключения и при этом
Если ( ТранзакцияАктивна() ) Тогда ОтменитьТранзакцию();
может вполне отменить некоторую другую транзакцию.
Для предотвращения этого введена целочисленная переменная Ж.
Если Ж=2 то значит команда НачатьТранзакцию() прошла успешно и в крайнем случае мы отменим «свою» транзакцию. Если Ж=1 это говорит о том, что команда НачатьТранзакцию() не была успешной (выбросила исключение) и в этом случае мы не будем выполнять ОтменитьТранзакцию(), так как «свою» мы так и не начали.
а так?
ЦелоеЧисло Ж;
Ж := 1;
Попытка
НачатьТранзакцию();
Ж := 2;
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ( ТранзакцияАктивна() И (Ж==2) )
Тогда ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Смысл такой — написать код таким образом, чтобы в случае, если исключение происходило в команде
НачатьТранзакцию()
(при этом транзакция не начнётся — поскольку — исключение выкидывает) тогда команда ОткатитьТранзакцию()
выполняться не должна, иначе возможен случай, когда мы откатим транзакцию, которую не мы начинали — т.е. откатим «чужую» транзакцию, которая не была начата в этом программном блоке.Ну и потом, перехватывать неначатую транзакцию, имхо, не стоит. Это какой-то фатальный облом, лучше выпустить это исключение наверх и пусть процесс упадет. С ним что-то явно не так, и лучше начать заново.
По-моему, предлагаемый в статье паттерн корректно обработает эту ситуацию и без введения переменной
Кто будет обрабатывать исключение?
Попытка
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Автоматический);
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию(); // ОШИБКА: Транзакция не активна
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Поймал исключение на строке ОтменитьТранзакцию();:
Ошибка при вызове метода контекста (ОтменитьТранзакцию)
ОтменитьТранзакцию();
по причине:
Транзакция не активна
Произошло следующее:
Код работает для документа на автоматических блокировках. Код был вызван при записи другого документа, работающего на управляемых блокировках. При выполнении НачатьТранзакцию получаем исключение — нельзя начать автоматическую транзакцию внутри управляемой, это понятно. Далее в обработчике исключения ТранзакцияАктивна() возвращает Истина, а ОтменитьТранзакцию вызывает новое исключение, т. к. «свою транзакцию» мы не открыли.
То есть функция ТранзакцияАктивна показывает активность транзакции в общем, она не показывает успех запуска процедуры НачатьТранзакцию здесь, или где-то в ДелаемЧтоТо. Следовательно использовать в паре с ней ОтменитьТранзакцию получается не совсем корректно.
Проблему можно обойти, если всё-таки вызывать НачатьТранзакцию снаружи попытки.
Транзакция = НачатьТранзакцию();
Если Транзакция.Активна() Тогда
Транзакция.Отменить();
КонецЕсли;
Так что я согласен, что в 1с реализация хромает.
Нет.
Я не вижу радикального преимущества такого подхода перед существующим
Радикальное преимущество такого подхода в том, что сложнее допустить ошибку. Когда транзакция — объект, программист уже не может случайно отменить чужую транзакцию. А если язык ещё и автоматический вызов деструкторов позволяет, то сразу получаем и защиту от "забытых" транзакций.
Не надо заучивать сложных паттернов работы с транзакциями, писать тонну одинакового кода и т.п.
На самом деле должно быть что-то вроде
Транзакция = НачатьТранзакцию();
Транзакция.Отменить();
Спасибо, познавательно!
Статья реально помогла мне бороться с ошибками, которые унаследовались из старого кода после перехода на более новый релиз платформы 1С.
Вы не умеете работать с транзакциями