Вакцинирование продом

от автора

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

Об авторе

Всем привет. Меня зовут Кирилл. За 10 лет в страховой отрасли прошел путь от IT-эникейщика до ТимЛида группы разработки страховых продуктов аквизиции. В этой статье поделюсь простыми приёмами по перешагиванию через очевидные грабли, чтобы движение через Зону Жизненного Опыта, Получаемую в Процессе было менее болезнено.

Приёмы

1. Резервная копия данных

Байка из жизни #1

Решил добавить столбец к табличке в Pervasive на проде. Размер таблицы в топ 5 базы. Запустил консольную утилиту по перестройке файла данных, подождал полчасика и закрыл – думал зависла. Файл данных таблицы, неожиданно (как же ещё) стал невалидным.

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

 

После этого случая, выработал для себя 2 железных правила:

 — Делай бекап/снапшот всякий раз перед изменением (даже для мелкого update’а);

 — Проверяй решение на другом инстансе, до деплоя на прод.

Самый простой способ помочь завтрашнему себе – сделать скрипты восстановления данных при их модифицировании (Insert/Update/Delete).

  • Для операции Delete достаточно выгрузить данные из IDE – на основании Select’а формируется датасет и просто экспортируется в файл/кэш как набор Insert-скриптов.

    Важно выгружать все данные по Foreign Keys, явным или нет, в виде отдельных Insert-инструкций. Особенно чувствительно, когда таблицы с данными каскадные либо со специфичной логикой в API/триггерах удаления. Варианты архитектуры, когда в Delete API реализован Insert/update/Post-сервисы стоит рассматривать отдельно – у данных может быть разная критичность.

Как это выглядит в PL\SQL Developer

  • Второй по сложности оператор DML – Insert. Можно просто сохранить список Primary Keys (PK). Или написать запрос, который отберёт созданные записи по специфичным маркерам. При наличии в таблице поля с автором создания (CreatedBy), датой создания, или другим особым полем, задача сильно упрощается.

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

  • Третья и самая сложная DML-операция – Update. Здесь возможны различные комбинации способов восстановления. Наиболее сложный случай в практике – восстановить данные по прошествии времени (полгода-год), когда над данными пользователь уже активно поиздевался поработал. В итоге потребовалось формализовать последовательность действий пользователя в виде скрипта. Прогнать скрипт на согласованном массиве данных — части списка для восстановления. И, естественно, проверить с двух сторон: бизнеса и DevTeam.

Подитог

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

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

Из практики

Работу со скриптами отката можно сильно облегчить через архитектуру БД — создать теневые таблицы. Решение спорное, т.к. для каждой таблицы приходится индивидуально отвечать на вопрос «что лучше?»: полные логи или малое потребление дискового пространства.

 

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

До Update

Сам Update

Update Message Set ExternalID = '123' Where ID = 1; Update Message Set ExternalID = '321' Where ID = 1; Update Message Set ExternalID = '789' Where ID = 1;

После Update

2. Feature Toggle (FT)

Байка из жизни #2

Команда целый инкремент перепиливает алгоритм расчета премии по страховому продукту. Затронуты как фронт, так и несколько систем бэка. Деплой идет тремя параллельными релизами в разное время в течение дня. День релиза определен, доработки закреплены, влиты в регрессные сборки, релиз-менеджеры финализируют тестирование и готовятся к деплою. Сутки до выкладки… и возникает вопрос, как поведет себя прод в промежутках между выкладками релизных сборок разных систем: фронт доехал, бэк в процессе, а кусок системы расчета тарифов уже вовсю работает. Страшновато. Начинаешь спешно просматривать все pull request’ы по выкладке, выясняя, что в итоге отвалится. В тот раз пронесло,благодаря требованиям к обратной совместимости (обработка null для нового входящего параметра) — неконсистентность кода прошла незаметной.

 

Итогом стало принятие на уровне команды парочки подходов:

— После выкладки крупных изменений, проверяем сами на проде, до анонсирования бизнесу;

— Декомпозиция Features на User Story таким образом, чтобы можно было их деплоить независимо (привет CI/CD);

— Обязательно наличие FT (сейчас наличие FT является НФТ для большей части доработок со стороны Компании)

Байка из жизни #3

В другой раз выкатывали изменения UI внутреннего страхового продукта. Допилили, залили, включились в релиз, и на регрессном стеде смежная команда находит баг (да-да, это случается, и это нормально) в одной из проверок UI. Анализ дефекта показывает, что не учли крупный бизнес-кейс – его доработка требует качественного тестирования и спокойной головы при написании кода. Хорошо, что доработку проверки зафичетоглили под отдельным кодом FT, т.е. она может быть отключена независимо. Подключаем бизнес и принимаем решение, что проверка некритична и можем запускаться без неё. FT отключается, задача тех долга на исправление закидывается в бэклог ближайшего спринта, дефект закрывается и релиз выходит без сбоев. Танцуют Все!

 

Что вынес для себя:

— Несколько кодов FT для одной стори хоть и усложняет обслуживание техдолга, но экономит нервы и время при ловле багов4

— Если UI поддерживает FT – используй.

Практика FT чуть сложнее резервной копии данных. Для нее необходим некий механизм логических переключателей – будь то таблица в БД, конфиг-файлы или настройки метаданных. Плюс треубется внутреннее согласие с ростом техдолга и необходимостью его обслуживания.

Сразу уточню, описание дано для Release Toggles. Их срок жизни – не дольше 2-3 релизных циклов (2 статья из Уголовного кодекса разработчика достаточно ярко подсвечивают столь жесткое требование). Необходимы для уменьшения количества внепланов/критических релизов. Польза в том, чтобы даже при полном отказе прода, его можно было легко (в идеале мгновенно, в процессе деплоя) реанимировать, отключив новый код.

Долгоживущие FT A/B тестирования, несут в себе другую ценность и на спокойствие разработчика при выкладке кода особо не влияют.

Базовые принципы:

  • Выключенный FT код не меняет, то есть курсор выполнения проходит ровно все те же блоки в том же порядке, что и без FT;

  • FT не используются повторно. На проде точно, а другие инстансы – на совести разработчика;

4 минута выступления Doug Sven — как не надо делать

Knightmare: A DevOps Предостерегающая история

Feature Toggles: The Good, The Bad, and The Ugly with Andy Davies
Feature Toggles: The Good, The Bad, and The Ugly with Andy Davies

4 минута видео

  • FT обязательно выпиливать после подтверждения отсутствия критических отказов. Мелкий косяк через 3-4 месяца активного (!) использования кода – явление настолько тривиальное, что ради него откатывать все решение нерационально.

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

3.Тесты сходимости/модульные тесты

Байка из жизни #4

Перепала как-то задача переписать запрос Oracle онлайн-канала – периодически оптимизатор выбирает план с full scan по датасету в 3-4m записей. Это был не первый подход к снаряду. Очевидные вещи в виде разбиения на запросы с union уже были применены. Нужен индекс. Так как таблица большая и с активными I/O операциями, DBA просят обоснования, что создание индекаса действительно поможет.

Просят – пожалуйста. Сделано 2 версии запроса, обернуты в FT и влиты в тестовый стенд. Дальше, через RunStats анализ используемых ресурсов, потом прогон на совпадение результатов по нескольким сотням примеров. Сошлись и это хорошо.

Пример сравнения производительности
Пример сравнения производительности

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

Самый приятный бонус данного подхода – один раз настраиваешь тесты (при разработке), и потом не паришься, когда нужно допилить функционал (протестировать руками даже 20 кейсов 3-4 раза подряд желания вызывает мало)

За этот подход спасибо коллегам. Крайне полезен при рефакторинге легаси. Суть – взять данные в базе (список договоров) и прогнать по максимуму через обе реализации метода, старую и новую. Так как enterprise-база кишит тестовыми примерами разной степени кривизны, при выборке в несколько тысяч-десятков тысяч примеров, шансов пропустить критический косяк очень мало. Добавив еще и сравнением производительности, получается хорошая серебряная пуля при разговоре с архитекторами.

Практика прекрасно сочетается с FT — в рамках одного запуска кода легко сравнить старое и новое решения.

Примеры кода
declare   vTime1  number := 0;   vTime2  number := 0;    procedure prc(pID in number, pRowID IN ROWID)   is         function find(pSeconds out number) return number     is       vResult NUMBER;     begin       pSeconds := dbms_utility.get_time/100;       -- Тестируемый метод       vResult  := TestPack.TestMethod;       pSeconds := (dbms_utility.get_time/100 - pSeconds);       return vResult;     end;          procedure TestCase(pCase in varchar2)     is       vResult1 number;       vResult2 number;       vSeconds1 number;       vSeconds2 number;       vDiff VARCHAR2(100);     begin       SAVEPOINT SP;       ftoggle.Disable('FT_12345');       vResult1 := find(vSeconds1);       vTime1 := vTime1 + vSeconds1;       ftoggle.Enable('FT_12345');       vResult2 := find(vSeconds2);       vTime2 := vTime2 + vSeconds2;       RollBack To SP;       if nvl(vResult1, -1) <> nvl(vResult2, -1) then         vDiff := ';[' || pCase || '] ' || pAgrISN || ': ' || vResult1 || '<>' || vResult2;       end if;       Update TMP_Table t          Set T.ERR = T.ERR||NVL2(T.ERR||vDiff, ';', Null)||vDiff,              T.STATUS     = Decode(pCase, '1', Nvl(T.STATUS,0)     + vSeconds1*1000000 + vSeconds2, T.STATUS))       Where T.RowID = pRowID;     end;        begin     GetTestData(pID);     TestCase('1');     TestCase('2');     TestCase('3');   end;  begin   for vCur in (select to_number(t.fio) As ID, T.RowID from TMP_Table t Where T.Status Is Null)   loop     prc(vCur.ID, vCur.RowID);     Update TMP_Table t Set T.ERR = 'Compare' Where T.RowID = vCur.Rowid And T.ERR Is Null;   end loop; end;

Логичное развитие тестов сходимости – модульные тесты (Unit tests, UT). Когда проверки присутствуют в базе одновременно с продовым кодом (за теорией – к Roy Osherove).

Польза UT не однозначна на старте проекта. Однако при развитии решения, благодаря UT стоимость рефакторинга снижается (см. техдолг). Наиболее очевидный аспект – меньше времени тратится на ретест при изменении кода.

Вопросы, как писать UT на Oracle достойны отдельной статьи. Здесь же приведу пару примеров реализации в формате тестопригодного дизайна на Oracle.

Пример UT#1

Проверяемый метод

  -- тип для расчета периодов   TYPE TPERIOD IS RECORD (     pBeg DATE,     pEnd DATE   );   TYPE TPERIODTABLE IS TABLE OF TPERIOD;    /**    * ==========================================================================<br/>   * Расчет периодов многолетнего страхования <br/>   * ==========================================================================<br/>   * @param pBegin - начало периода   * @param pEnd   - окончание периода   * @return       - таблица с периодами   */       FUNCTION CalcPeriods(pBegin IN DATE, pEnd IN DATE) RETURN TPERIODTABLE   AS     vResult TPERIODTABLE := TPERIODTABLE();     vPeriod TPERIOD;     vBegin  DATE   := TRUNC(pBegin);     vEnd    DATE   := TRUNC(pEnd);     vMonths NUMBER := GREATEST(months_between(vEnd, vBegin), 1);     -- Необходимо корректировать 1 день в феврале високосного года     NeedCorrectLeapYear BOOLEAN := (TO_CHAR(vBegin,'dd.mm') = '28.02') AND (TO_CHAR(vBegin,'mm') = TO_CHAR(vBegin + 1,'mm'));   BEGIN     --Договор может закончиться в том же месяце, но через несколько дней. Добавим месяц.     IF TRUNC(vMonths) < vMonths THEN       vMonths := TRUNC(vMonths)+1;     END IF;     FOR cMonth IN 1..vMonths     LOOP       IF MOD(cMonth - 1, 12) = 0 THEN         vPeriod.pbeg := add_months(vBegin, cMonth - 1);         vPeriod.pend := add_months(vPeriod.pbeg, 12) - 1;         IF vPeriod.pend > pend THEN           vPeriod.pend := pend;         END IF;         IF (NeedCorrectLeapYear AND (TO_CHAR(vPeriod.pend,'dd.mm') = '28.02'))         THEN           vPeriod.pend := vPeriod.pend - 1;         END IF;         vResult.extend;         vResult(vResult.last) := vPeriod;       END IF;     END LOOP;      RETURN vResult;   END CalcPeriods;

Тесты

  /**   * ========================================================================<br/>   * Получить параметры вызова метода CalcPeriods по-умолчанию<br/>   * ========================================================================<br/>   * @param pDateBeg   - Дата начала периода   * @param pAddMonths - Длительность периода в месяцах   * @return           - Запись типа TPERIOD с данными периода   * ! Важно - Процедура использует Oracle Add_months, поведение которой для 29.02 специфично   */   FUNCTION GetDefaultValues (pDateBeg   IN DATE    DEFAULT TRUNC(SYSDATE),                              pAddMonths IN INTEGER DEFAULT 12) RETURN TPERIOD   IS     vResult TPERIOD;   BEGIN     vResult.pBeg := pDateBeg;     vResult.pEnd := Add_months(vResult.pBeg, pAddMonths) - 1;     RETURN vResult;   END GetDefaultValues;    /**   * ========================================================================<br/>   * Сформировать CLOB по периоду<br/>   * ========================================================================<br/>   * @param pPeriod - Тип с информацией по периоду   * @return        - Структурированный CLOB с данными   */   FUNCTION getPeriodClob(pPeriod IN TPERIOD     ) RETURN CLOB   IS   BEGIN     RETURN TO_CLOB('<PERIOD>'||                    '<DATEBEGIN>'||TO_CHAR(pPeriod.pbeg, 'DD.MM.YYYY')||'</DATEBEGIN>'||                    '<DATEEND>'||TO_CHAR(pPeriod.pend, 'DD.MM.YYYY')||'</DATEEND>'||                    '</PERIOD>');   END getPeriodClob;    /**   * ========================================================================<br/>   * Сформировать CLOB по периоду (reload)<br/>   * ========================================================================<br/>   * @param pBeg - Дата начала периода   * @param pBeg - Дата окончания периода   * @return     - Структурированный CLOB с данными   */   FUNCTION getPeriodClob(pBeg IN DATE, pEnd IN DATE) RETURN CLOB   IS     vPeriod TPERIOD;   BEGIN     vPeriod.pBeg := pBeg;     vPeriod.pEnd := pEnd;     RETURN getPeriodClob(vPeriod);   END getPeriodClob;    /**   * ========================================================================<br/>   * Тест разбиения периода на 1 год<br/>   * ========================================================================<br/>   */   PROCEDURE PER_1YEAR   IS     vDefPeriod TPERIOD;     vPeriods   TPERIODTABLE;     vRes       XMLType;     vResOK     XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('01.01.2020', 'DD.MM.YYYY'));     vResOK     := XMLTYPE('<PERIODS>'||getPeriodClob(pPeriod => vDefPeriod)||'</PERIODS>');     vPeriods   := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend,);     vRes       := getPeriodsXML(pPeriods => vPeriods);          UNITTEST.CheckTestResult('PER_1YEAR', vRes, vResOK);   END PER_1YEAR;    /**   * ========================================================================<br/>   * Тест разбиения периода на 2 года<br/>   * ========================================================================<br/>   */   PROCEDURE PER_2YEAR   IS     vDefPeriod TPERIOD;     vPeriods   TPERIODTABLE;     vOkPeriods TPERIODTABLE;     vRes       XMLType;     vResOK     XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('01.01.2020', 'DD.MM.YYYY'), pAddMonths => 24);     -- Определить даты интервалов     Select T.dBeg, T.dEnd Bulk Collect Into vOkPeriods     From (       Select vDefPeriod.pbeg As dBeg, TO_DATE('31.12.2020', 'DD.MM.YYYY') As dEnd From Dual       Union All       Select TO_DATE('01.01.2021', 'DD.MM.YYYY'), vDefPeriod.pEnd From Dual       ) T;     vResOK   := getPeriodsXML(pPeriods => vOkPeriods);     vPeriods := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend);     vRes     := getPeriodsXML(pPeriods => vPeriods);     UNITTEST.CheckTestResult('PER_2YEAR', vRes, vResOK);   END PER_2YEAR;    /**   * ========================================================================<br/>   * Тест разбиения периода на 400 дней<br/>   * ========================================================================<br/>   */   PROCEDURE PER_400DAYS   IS     vDefPeriod TPERIOD;     vPeriods   TPERIODTABLE;     vOkPeriods TPERIODTABLE;     vRes       XMLType;     vResOK     XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('17.01.2020', 'DD.MM.YYYY'), pAddMonths => 12);     vDefPeriod.pend := vDefPeriod.pbeg + 400;     -- Определить даты интервалов     Select T.dBeg, T.dEnd Bulk Collect Into vOkPeriods     From (       Select vDefPeriod.pbeg As dBeg, TO_DATE('16.01.2021', 'DD.MM.YYYY') As dEnd From Dual       Union All       Select TO_DATE('17.01.2021', 'DD.MM.YYYY'), vDefPeriod.pEnd From Dual       ) T;     vResOK   := getPeriodsXML(pPeriods => vOkPeriods);     vPeriods := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend);     vRes     := getPeriodsXML(pPeriods => vPeriods);     UNITTEST.CheckTestResult('PER_400DAYS', vRes, vResOK);   END PER_400DAYS;    /**   * ========================================================================<br/>   * Тест разбиения периода на 4 года(40 месяцев)<br/>   * ========================================================================<br/>   */   PROCEDURE PER_40MONTHS   IS     vDefPeriod TPERIOD;     vPeriods   TPERIODTABLE;     vOkPeriods TPERIODTABLE;     vRes       XMLType;     vResOK     XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('01.01.2020', 'DD.MM.YYYY'), pAddMonths => 40);      -- Определить даты интервалов     Select T.dBeg, T.dEnd Bulk Collect Into vOkPeriods     From (       Select vDefPeriod.pbeg As dBeg, TO_DATE('31.12.2020', 'DD.MM.YYYY') As dEnd From Dual       Union All       Select TO_DATE('01.01.2021', 'DD.MM.YYYY'), TO_DATE('31.12.2021', 'DD.MM.YYYY') From Dual       Union All       Select TO_DATE('01.01.2022', 'DD.MM.YYYY'), TO_DATE('31.12.2022', 'DD.MM.YYYY') From Dual       Union All       Select TO_DATE('01.01.2023', 'DD.MM.YYYY'), vDefPeriod.pEnd From Dual       ) T;     vResOK := getPeriodsXML(pPeriods => vOkPeriods);     vPeriods := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend);     vRes     := getPeriodsXML(pPeriods => vPeriods);     UNITTEST.CheckTestResult('PER_40MONTHS', vRes, vResOK);   END PER_40MONTHS;    /**   * ========================================================================<br/>   * Тест разбиения периода на 3 года c 27.02 пред високосного года<br/>   * ========================================================================<br/>   */   PROCEDURE PER_3YEAR_2702   IS     vDefPeriod TPERIOD;     vPeriods TPERIODTABLE;     vOkPeriods TPERIODTABLE;          vRes   XMLType;     vResOK XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('27.02.2023', 'DD.MM.YYYY'), pAddMonths => 36);      -- Определить даты интервалов     Select T.dBeg, T.dEnd Bulk Collect Into vOkPeriods     From (       Select vDefPeriod.pbeg As dBeg, TO_DATE('26.02.2024', 'DD.MM.YYYY') As dEnd From Dual       Union All       Select TO_DATE('27.02.2024', 'DD.MM.YYYY'), TO_DATE('26.02.2025', 'DD.MM.YYYY') From Dual       Union All       Select TO_DATE('27.02.2025', 'DD.MM.YYYY'), vDefPeriod.pEnd From Dual       ) T;     vResOK   := getPeriodsXML(pPeriods => vOkPeriods);     vPeriods := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend);     vRes     := getPeriodsXML(pPeriods => vPeriods);     UNITTEST.CheckTestResult('PER_3YEAR_2702', vRes, vResOK);   END PER_3YEAR_2702;    /**   * ========================================================================<br/>   * Тест разбиения периода на 3 года c 28.02 пред високосного года<br/>   * ========================================================================<br/>   */   PROCEDURE PER_3YEAR_2802   IS     vDefPeriod TPERIOD;     vPeriods   TPERIODTABLE;     vOkPeriods TPERIODTABLE;     vRes       XMLType;     vResOK     XMLType;   BEGIN     vDefPeriod := GetDefaultValues(pDateBeg => TO_DATE('28.02.2023', 'DD.MM.YYYY'), pAddMonths => 36);     -- Определить даты интервалов     Select T.dBeg, T.dEnd Bulk Collect Into vOkPeriods     From (       Select vDefPeriod.pbeg As dBeg, TO_DATE('28.02.2024', 'DD.MM.YYYY') As dEnd From Dual       Union All       Select TO_DATE('29.02.2024', 'DD.MM.YYYY'), TO_DATE('27.02.2025', 'DD.MM.YYYY') From Dual       Union All       Select TO_DATE('28.02.2025', 'DD.MM.YYYY'), vDefPeriod.pEnd From Dual       ) T;     vResOK   := getPeriodsXML(pPeriods => vOkPeriods);     vPeriods := CalcPeriods  (pBegin => vDefPeriod.pbeg, pEnd => vDefPeriod.pend);     vRes     := getPeriodsXML(pPeriods => vPeriods);     UNITTEST.CheckTestResult('PER_3YEAR_2802', vRes, vResOK);   END PER_3YEAR_2802;

Пример UT#2

Проверяемый метод

  /**   * ========================================================================<br/>   * Сохранение внешних ссылок на объекты <br/>   * ========================================================================<br/>   * @param pList      - CLOB с данными в формате JSON со списком GUID   * @return           - Результат обработки (1 - нет ошибок, -1 - ошибка)   */   FUNCTION SaveExternalLink(pJSON IN CLOB) RETURN NUMBER   IS     vResult NUMBER(1) := 1;   BEGIN     IF pJSON IS NULL THEN        RETURN vResult;     END IF;     BEGIN       Insert Into Message M         (ClassName, InsNumber, ExternalID)       Select Distinct J.ClassName, J.InsNumber, J.ExternalID       From JSON_Table(pJSON, '$.objects[*]'                         Columns(ClassName    Varchar2(32)   path '$.name',                                 InsNumber    Number         path '$.insNumber',                                 ExternalID   Varchar2(4000) path '$.externalID')) J       Where J.ClassName Is Not Null          And J.InsNumber Is Not Null          And J.GUID      Is Not Null         And Not Exists           (Select Null From Message Me            Where ME.ExternalID = J.ExternalID Or              (J.ClassName Is Not Null And                ME.ClassName = J.ClassName And               ME.InsNumber = J.InsNumber)           );     EXCEPTION       WHEN OTHERS THEN         vResult := -1;     END;     RETURN vResult;   END SaveExternalLink;

Тесты

  /**   * ========================================================================<br/>   * Сформировать CLOB по объекту <br/>   * ========================================================================<br/>   * @param pClassName  - Класс объекта   * @param pInsNumber  - Номер заявки   * @param pExternalID - Значение внешней ссылки   * @return            - Структурированный CLOB с данными   */   FUNCTION getObjectClob(pClassName IN VARCHAR2,                           pInsNumber IN VARCHAR2,                           pExternalID      IN VARCHAR2) RETURN CLOB   IS   BEGIN     RETURN TO_CLOB('<OBJECT>'||                    '<CLASSNAME>'||pClassName||'</CLASSNAME>'||                    '<INSNUMBER>'||pInsNumber||'</INSNUMBER>'||                    '<EXTERNALID>'||pExternalID||'</EXTERNALID>'||                    '</OBJECT>');   END getObjectClob;    /**   * ========================================================================<br/>   * Сформировать XML со списком внеших ссылок по номеру заявки <br/>   * ========================================================================<br/>   * @param pInsNumber - Список номеров заявок   * @return           - XML с данными   */   FUNCTION getObjectsXMLByInsNum(pInsNumber IN TNUM) RETURN XMLType   IS     vResult CLOB;   BEGIN     IF pInsNumber.Count > 0 THEN       FOR vCur IN (Select M.ClassName, M.InsNumber, M.ExternalID                    From Message M, Table(pInsNumber) R                    Where M.InsNumber = R.column_value                    Order By ClassName, InsNumber)       LOOP         vResult := vResult || getObjectClob(vCur.ClassName, vCur.InsNumber, vCur.ExternalID);       END LOOP;     END IF;      RETURN XMLTYPE('<OBJECTS>'||vResult||'</OBJECTS>');   END getObjectsXMLByInsNum;     /**   * ========================================================================<br/>   * Сформировать XML со списком внеших ссылок по номеру заявки <br/>   * ========================================================================<br/>   * @param pXML - XML с данными   * @return     - CLOB формата JSON с данными   */   FUNCTION ConvertXMLToJSONClob(pXML IN XMLTYPE) RETURN CLOB   IS     vResult CLOB;   BEGIN     IF pXML IS NULL THEN       RETURN vResult;     END IF;     Select JSON_Object(Key 'objects' Value        JSON_ArrayAgg(                JSON_Object(Key 'className' Value X.ClassName,                            Key 'insNumber' Value X.InsNumber,                            Key 'externalID' Value X.ExternalID)                Order by X.InsNumber, X.ClassName                Returning VarChar2(32000)))       Into vResult       From XMLTable('/OBJECTS/OBJECT' Passing pXML Columns                       ClassName    Path 'CLASSNAME',                       InsNumber    Path 'INSNUMBER',                       ExternalID   Path 'EXTERNALID')X;     RETURN vResult;   END ConvertXMLToJSONClob;    /**   * ========================================================================<br/>   * Параметрическое сохранение списка ExternalID на основании XML (OK) <br/>   * ========================================================================<br/>   * @param pTarget      - CLOB с данными для проверки   * @param pList        - CLOB с данными для загрузки   * @param pInsNumber   - Номер проверяемой заявки   * @param pTestCaption - Название теста   */     PROCEDURE SaveExternalID_ByXML(pTarget       IN CLOB,                                  pList         IN CLOB,                                  pInsNumber    IN TNUM,                                  pTestCaption  IN VARCHAR2)   IS     vRes         XMLType;     vResOK       XMLType := XMLTYPE('<OBJECTS>'||pTarget||'</OBJECTS>');     vList        CLOB    := ConvertXMLToJSONClob(XMLTYPE('<OBJECTS>'||pList||'</OBJECTS>'));     vResult      NUMBER;   BEGIN     SAVEPOINT SP;     BEGIN       vResult:= SaveExternalLink(pJSON => vList);       vRes   := getObjectsXMLByInsNum(pInsNumber => pInsNumber);     EXCEPTION       WHEN OTHERS THEN         vRes := XMLType('<ERR>' || RegExp_replace(SQLERRM, '(.)([\(|\[].*)', '\1') || '</ERR>');     END;     ROLLBACK TO SP;     UNITTEST.CheckTestResult(pTestCaption, vRes, vResOK);   END SaveExternalID_ByXML;    /**   * ========================================================================<br/>   * Параметрическое сохранение списка ExternalID на основании XML (OK) <br/>   * ========================================================================<br/>   * @param pList        - CLOB с данными для загрузки   * @param pErrCode     - Код ожидаемой ошибки   * @param pTestCaption - Название теста   */     PROCEDURE SaveExternalID_ByXML_Raise(pList        IN CLOB,                                        pErrCode     IN VARCHAR2,                                        pTestCaption IN VARCHAR2     )   IS     vRes         XMLType := XMLTYPE('<OBJECTS>NOT OK</OBJECTS>');     vResOK       XMLType := XMLTYPE('<OBJECTS>Correct error</OBJECTS>');     vList        CLOB    := ConvertXMLToJSONClob(XMLTYPE('<OBJECTS>'||pList||'</OBJECTS>'));     vResult      NUMBER;   BEGIN     SAVEPOINT SP;     BEGIN       vResult:= SaveExternalLink(pJSON => vList);     EXCEPTION       WHEN OTHERS THEN         IF INSTR(SQLERRM, pErrCode) > 0 THEN           vRes := vResOK;         ELSE           vRes := XMLType('<ERR>' || RegExp_replace(SQLERRM, '(.)([\(|\[].*)', '\1') || '</ERR>');         END IF;     END;     ROLLBACK TO SP;     UNITTEST.CheckTestResult(pTestCaption, vRes, vResOK);   END SaveExternalID_ByXML_Raise;    /**   * ========================================================================<br/>   * Сохранение NULL-значения (OK) <br/>   * ========================================================================<br/>   */     PROCEDURE SaveExternalID_NullList_OK   IS     vInsNumber NUMBER;   BEGIN     SaveExternalID_ByXML(pTarget      => NULL,                          pList        => NULL,                          pInsNumber   => TNum(vInsNumber),                          pTestCaption => 'Сохранение NULL-значения (OK)');   END SaveExternalID_NullList_OK;    /**   * ========================================================================<br/>   * Сохранение пустого списка ExternalID (OK) <br/>   * ========================================================================<br/>   */     PROCEDURE SaveExternalID_EmptyList_OK   IS     vInsNumber NUMBER;   BEGIN     SaveExternalID_ByXML(pTarget      => NULL,                          pList        => NULL,                          pInsNumber   => TNum(vInsNumber),                          pTestCaption => 'Сохранение пустого списка (OK)');   END SaveExternalID_EmptyList_OK;    /**   * ========================================================================<br/>   * Сохранение списка ExternalID из 1 элемента по заявке (тип не заполнен) (OK)  <br/>   * ========================================================================<br/>   */     PROCEDURE SaveExternalID_1Elem_EmptyClass_OK   IS     vInsNumber NUMBER;     vList      CLOB   := getObjectClob(NULL, 12345, 'e65f9824-90f2-7da9-99f0-a23418a3e980');   BEGIN     SaveExternalID_ByXML(pTarget      => NULL,                          pList        => vList,                          pInsNumber   => TNum(vInsNumber),                           pTestCaption => 'Сохранение списка ExternalID из 1 элемента по заявке (тип не заполнен)(OK)');   END SaveExternalID_1Elem_EmptyClass_OK;    /**   * ========================================================================<br/>   * Сохранение списка ExternalID из 1 элемента по заявке (некорректный тип) (ERR)  <br/>   * ========================================================================<br/>   */     PROCEDURE SaveExternalID_1Elem_IncClass_ERR   IS     vInsNumber NUMBER := 12345;     vList      CLOB   := getObjectClob('TESTERR', vInsNumber, 'e65f9824-90f2-7da9-99f0-a23418a3e980');   BEGIN     SaveExternalID_ByXML_Raise(pList        => vList,                                pErrCode     => 'ERROR_CODE_1',                                 pTestCaption => 'Сохранение списка ExternalID из 1 элемента по заявке (некорректный тип)(ERR)');   END SaveExternalID_1Elem_IncClass_ERR;    /**   * ========================================================================<br/>   * Сохранение списка ExternalID из 1 элемента с некорректным номером заявки (OK)<br/>   * ========================================================================<br/>   */     PROCEDURE SaveExternalID_1Elem_IncIN_OK   IS     vInsNumber NUMBER;     vList      CLOB   := getObjectClob('TYPE_1', '12345_SS', 'e65f9824-90f2-7da9-99f0-a23418a3e980');   BEGIN     SaveExternalID_ByXML(pTarget      => NULL,                          pList        => vList,                          pInsNumber   => TNum(vInsNumber),                           pTestCaption => 'Сохранение списка ExternalID из 1 элемента с некорректным номером заявки (OK)');   END SaveExternalID_1Elem_IncIN_OK;

Резюме

Как видно, предлагаемые практики не требуют больших усилий в части использования, но очень эффективны. Критерий их применения – «как быстро смогу починить упавший из-за моих доработок прод». Буду признателен в комментариях за описания других техник, минимизирующих ошибку разработчика.


ссылка на оригинал статьи https://habr.com/ru/post/676710/


Комментарии

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

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