Что такое транзакция в 1с. Одноразовые клиенты

2017-10-31

Как создать вариант для транзакции с помощью SHD0 ?

Пояснение к вопросу

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

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

  • Разделить доступ к полям одного инфо-типа для двух групп пользователей, работающих с ним, при условии, что уровень полномочий на этот инфо-тип у них одинаковый. Иными словами, для одних пользователей сделать доступными одни поля, для других - другие.

Из исходных данных: транзакция, с которой работают пользователи - PA30 . Инфо-тип в данном случае не имеет значения. Конкретно в своем примере, я буду использовать инфо-тип кадрового администрирования.

В качестве примера хорошо подойдет инфо-тип 0002 - "Personal Data"

Задачу по так называемому разграничению полномочий сформулируем следующим образом:

  • Поле Mar.Status инфо-типа 0002 "Personal Data" необходимо сделать недоступным для редактирования (то есть только на чтение);
  • Поле Valid From Date of Current Marital Status инфо-типа 0002 "Personal Data" необходимо скрыть.

Решение вопроса

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

  1. Создать отдельную транзакцию для группы пользователей, которым необходимо изменить уровень доступа к полям инфо-типа;
  2. Создать вариант экрана, в котором выполнить настройку видимости полей, с последующим присвоением варианта экрана созданному варианту транзакции (получилось слишком кучеряво, но по факту ничего сложного).
  3. Создать вариант для этой транзакции, присвоив ему вариант экрана, в котором настроена соответствующая видимость полей;
  4. Создать пользовательскую роль, в которую включить транзакцию, и присвоить ее пользователям;
  5. Выполнить тестирование.

1. Создание новой транзакции

Так как в качестве основной транзакции выступает PA30 , возьмем ее за основу, скопировав в новую. Выполнить это можно через транзакцию SE93

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

Транзакция успешно запускается. Переходим к следующему пункту.

2. Создание варианта для новой транзакции

Создаем вариант для новой транзакции, в котором определяем уровень доступа к полям инфо-типа. Напомню, мне необходимо изменить уровень доступа полей "Mar.Status " и "Valid From Date of Current Marital Status» . Выполнять эти действия необходимо в транзакции SHD0 .

В поле Transaction Code укажите наименование новой транзакции (см. пункт #1. Создание новой транзакции)

2.1 Создание варианта для экрана

На закладке "Screen Variants" необходимо создать вариант экрана, используемый для инфо-типа 0002. В этом варианте следует применить нужные правила отображения для полей. Все что нужно знать - это соответствующий пул модулей и номер экрана (для этого, на экране редактирования инфо-типа выберите в меню System -> Status )

В следующем видеофрагменте указана последовательность действий, которую необходимо выполнить для создания варианта экрана

Вариант экрана создан. Никаких изменений пока не наблюдается.

2.2 Присвоение варианта экрана варианту транзакции

Выполните присвоение созданного варианта экрана варианту транзакции, после чего выполните активацию созданного варианта транзакции

2.3 Тестирование

Сценарий тестирования очень прост:

  • Запустите транзакцию PA30 , выберите табельный номер, откройте на редактирование инфо-тип 0002 и проверьте, что вышеуказанные поля отображаются без изменений
  • Запустите транзакцию ZPA30 , и выполните точно такую же последовательность действий

Разница очевидна, а значит и поставленная задача успешно выполнена.

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

Если мы обратимся к технической документации или к диску ИТС, то увидим, что фирма 1С рекомендует следующий способ организации транзакции в попытке

Попытка //1. Начало транзакции. НачатьТранзакцию() ; //2. Блок операций, выполняющихся в транзакции. //3. Если все операции успешны, фиксируем транзакцию. ЗафиксироватьТранзакцию() ; Исключение //4. Если при выполнении кода возникли ошибки, отменяем транзакцию. ОтменитьТранзакцию() ; //5. При необходимости запись в журнал регистрации. //6. При необходимости вывод сообщения пользователю. КонецПопытки ;

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

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

&НаСервереБезКонтекста НачатьТранзакцию() ; //записываем новый товар Товар = Справочники. Товары. СоздатьЭлемент() ; Товар. Наименование = "Дырокол" ; Товар. Записать() ; //записываем цену НаборЗаписей = РегистрыСведений. Цена. СоздатьНаборЗаписей() ; НоваяЗапись = НаборЗаписей. Добавить() ; НоваяЗапись. Период = ТекущаяДата() ; НоваяЗапись. Товар = Товар. Ссылка; НоваяЗапись. Сумма = 100 ; НаборЗаписей. Записать() ; ЗафиксироватьТранзакцию() ; КонецПроцедуры

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

&НаСервереБезКонтекста Процедура ВыполнитьТранзакциюНаСервере() //создаем новый товар Товар = Справочники. Товары. СоздатьЭлемент() ; Товар. Наименование = "Дырокол" ; //Создаем запись с ценой НаборЗаписей = РегистрыСведений. Цена. СоздатьНаборЗаписей() ; НоваяЗапись = НаборЗаписей. Добавить() ; НоваяЗапись. Период = ТекущаяДата() ; НоваяЗапись. Сумма = 100 ; //Выполняем транзакцию в попытке Попытка НачатьТранзакцию() ; Товар. Записать() ; НоваяЗапись. Товар = Товар. Ссылка; НаборЗаписей. Записать() ; ЗафиксироватьТранзакцию() ; Исключение ОтменитьТранзакцию() ; Сообщение = Новый СообщениеПользователю; Сообщение. Текст = ; Сообщение. Сообщить() ; ЗаписьЖурналаРегистрации("Произошла ошибка при записи товара и его цены" ) ; КонецПопытки ; КонецПроцедуры

Как НЕ НАДО делать

У тех кто только начинает работать с транзакциями зачастую возникает желание сделать вот таким образом

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

Или в цикле

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

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

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

В результате выполнения этой процедуры увидим в окне сообщений следующее:

{ВнешняяОбработка.ТранзакцииВПопытке.Форма.Форма.Форма(20)}: Ошибка записи товара. {ВнешняяОбработка.ТранзакцииВПопытке.Форма.Форма.Форма(40)}: Ошибка при вызове метода контекста (Записать): В данной транзакции уже происходили ошибки!

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

Возможные варианты

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

&НаСервереБезКонтекста Процедура ВыполнитьТранзакциюНаСервере() // Начинаем транзакцию Отказ = Ложь ; НачатьТранзакцию() ; // Пытаемся записать товар Попытка Товар = Справочники. Товары. СоздатьЭлемент() ; Товар. Наименование = "Дырокол" ; Товар. Записать() ; Исключение Отказ = Истина ; Сообщение = Новый СообщениеПользователю; Сообщение. Текст = "Ошибка при записи товара" ; Сообщение. Сообщить() ; КонецПопытки ; // Пытаемся записать цену Попытка НаборЗаписей = РегистрыСведений. Цена. СоздатьНаборЗаписей() ; НоваяЗапись = НаборЗаписей. Добавить() ; НоваяЗапись. Период = ТекущаяДата() ; НоваяЗапись. Товар = Товар. Ссылка; НоваяЗапись. Сумма = 100 ; НаборЗаписей. Записать() ; Исключение Отказ = Истина ; Сообщение = Новый СообщениеПользователю; Сообщение. Текст = "Ошибка при записи цены" ; Сообщение. Сообщить() ; КонецПопытки ; // Фиксируем или отменяем транзакцию Если НЕ Отказ Тогда ЗафиксироватьТранзакцию() ; Иначе ОтменитьТранзакцию() ; КонецЕсли ; КонецПроцедуры

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

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

Переходим от одноразовых покупок к постоянным клиентам

Ну что ж, у вас получилось сконвертировать клиента. Это круто, а что дальше? По данным статистики от 30% до 80% покупателей в ecommerce индустрии совершают заказ лишь раз за весь свой жизненный цикл. В сфере игр второй заказ совершает 60% клиентов. Как нам получить постоянных клиентов при столь неутешительных численных данных?

Этот вопрос волнует маркетологов по всему миру. Они трудятся, прилагая все свои усилия на перевод клиентов из одноразовых в постоянных. Будь то первичные заказы в ритейле или депозиты в онлайн-играх. Почему второй заказ столь важен? Если клиент совершает свой второй заказ или вносит второй депозит, вероятность совершения третьего заказа возрастает в десятки раз по сравнению с клиентами, совершившими только один заказ.

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


Вероятность совершения следующей транзакции в зависимости от числа текущих транзакций

Многие компании объединяют одноразовых клиентов в одну группу и используют разные методики и посылы для стимулирования второй покупки. Звучит как неплохой план, не так ли? Так или иначе, все клиенты в группе - одноразовые покупатели. Но мы собрались здесь для обсуждения иного подхода. Этот второй метод заключается в разделении группы одноразовых клиентов на разные сегменты в зависимости от характеристик их первой транзакции. Нырнем глубже и рассмотрим предпосылки такой сегментации.

День недели

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

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


Вероятность совершения транзакции второй транзакции в зависимости от дня недели первой транзакции в ставках на спорт

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


Вероятность совершения транзакции второй транзакции в зависимости от дня недели первой транзакции в ритейле

Мы можем увидеть аналогичную ситуацию. Покупатели совершают вторую покупку в тот же день недели, что и первую. Важно отметить, что дисперсия в сфере ритейла была выше, чем в сфере ставок на спорт. Но сама зависимость проявлялась для каждой компании. Самый популярный день для покупок - понедельник, самый непопулярный - воскресенье. Если рассмотреть зависимости между заказами только для одного бренда, получим вот что.


Вероятность совершения транзакции второй транзакции в зависимости от дня недели первой транзакции в ритейле для одного бренда

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

Время суток

Как вы могли догадаться, мы протестировали аналогичные гипотезы для времени суток. Есть ли корреляция между временем суток первого заказа и второго, если второй заказ совершили как минимум через семь дней позже? Мы поделили день на 4 периода: ночь, утро, вечер и полдень - и проверили распределение вторых заказов для каждого временного периода для 6 брендов.


Вероятность второго заказа в зависимости от времени суток первого заказа

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

Стоимость товаров в заказе

Как маркетологи, мы стремимся увеличить число товаров в заказе. Допродажи в мире маркетинга - способ жизни, а если нет - то следовало бы. Но должны ли мы всегда пытаться допродать товар? Является ли такое решение лучшим для всех наших клиентов? В проведенном анализе мы исследовали, увеличивается ли стоимость товаров во втором заказе по сравнению с первым.

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


Вероятность стоимости второго заказа в зависимости от стоимости первого заказа

Большинство клиентов, чьи заказы были совершены в низком ценовом диапазоне, остались в том же диапазоне и во втором заказе.

Заключение

Наш анализ выше показывает, что мы можем почерпнуть из первых заказов. Главное, что нам нужно запомнить, что не следует помещать всех одноразовых клиентов в одну группу. Стоит сегментировать клиентов в зависимости от их дня недели и времени покупки, стоимости заказа.
Использование этих методов и шагов поможет вам лучше понять, как увеличить LTV и получить больше лояльных клиентов.

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

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

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


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


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

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

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


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

Транзакция — логически связанная, неделимая последовательность действий. Транзакция может быть либо выполнена целиком, либо вообще не выполнена. Для фиксации транзакции в СУБД используется метод COMMIT.

Типичный пример транзакции — перевод денежных средств с одного счета на другой:

  1. начать транзакцию;
  2. прочесть количество денежных средств на счету номер 123;
  3. уменьшить баланс счета 123 на 100 рублей;
  4. сохранить баланс счёта номер 123;
  5. прочесть количество денежных средств на счету номер 321;
  6. увеличить баланс на 100 рублей;
  7. записать новое количество денежных средств на счете 321;
  8. зафиксировать транзакцию.

Получите 267 видеоуроков по 1С бесплатно:

Как мы видим, если транзакция выполнена не полностью, то она не имеет смысла.

Ключевые требования (ACID) к транзакционной СУБД

Одним из наиболее распространённых наборов требований к транзакциям и транзакционным СУБД является набор ACID (Atomicity, Consistency, Isolation, Durability). Это те свойства, которыми должна обладать любая транзакция:

  • Атомарность (Atomicity) — никакая транзакция не должна быть зафиксирована частично;
  • Согласованность (Consistency) — система находится в согласованном состоянии до начала транзакции и должна остаться в согласованном состоянии после завершения транзакции;
  • Изолированность (Isolation) — во время выполнения транзакции параллельные транзакции не должны оказывать влияние на её результат;
  • Надежность (Durability) — в случая сбоя изменения, сделанные успешно завершённой транзакцией, должны остаться сохранёнными после возвращения системы в работу.

Транзакции в 1С

Транзакции в 1С 8.3 и 8.2 создаются как автоматически, так и описываются разработчиками.

С помощью метода ТранзакцияАктивна() можно узнать, активна ли транзакция.

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