Предисловие

В статье предлагается решение по быстрому написанию консистентного обновления базы данных в среде SAP NetWeaver на языке программирования ABAP (хотя подход может быть применен и к другим языкам). Описанный в статье подход является результатом субъективного опыта автора.

Описание проблемы. Важность решения

Для упрощения понимания под «консистентным обновлением таблиц» в базе данных будем понимать такое обновление, при котором данные из SAP-транзакции записываются в базу либо все одновременно, либо не записываются вовсе. Можно сказать, что такие данные имеют «внутреннюю согласованность». Другими словами - это обновление данных в БД, которое отвечает требованиям ACID.

Рассмотрим пример: пусть модель данных некоторого документа состоит из заголовка (ZDOC_H) и позиции (ZDOC_I). Ситуация, когда в базу одновременно записываются данные и в ZDOC_H и ZDOC_I, является консистентным обновлением. А вот если же данные в ZDOC_H не записались, а записались только в ZDOC_I, то такое обновление не является консистентным. При этом, если по каким-то причинам одну из таблиц обновить нельзя (например, есть физическая блокировка или логический статус записи в таблице это сделать не позволяет), то и другая таблица обновляться не должна; это также является признаком консистентного обновления.

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

А как решается эта задача на уровне базы данных?

Транзакционность в SQL-базах данных

Для того, чтобы обеспечить консистентность данных (т.е. их внутреннюю согласованность между собой) есть понятие транзакционности (либо запись/изменение всех данных либо ничего). А вот для транзакционности (то есть выполнения нескольких операций БД в рамках одного шага) имеются специальные операторы. В популярных БД – это операторы START TRANSACTION и COMMIT (Рис.1). С помощью START TRANSACTION СУБД дается указание, что нужно обновить все в рамках одного шага и «шаг наполняется» нужными SQL-операциями (insert, delete, update). (пример для postgres, для ORACLE, для MySQL, для HANA). Вот этот «шаг работы базы данных» имеет название «Unit of work» или Database Unit of Work (в переводе часто используют термин Единица работы). Наполнение шага обновления диктуется исключительно логикой модели данных и поэтому используют также понятие Database Logical Unit of Work, или DB LUW (Рис.1).

Для того, чтобы обеспечить транзакционность на уровне базы данных мы открываем «логический шаг» с помощью START TRANSACTION, наполняем нужными SQL-Операциями и завершаем шаг командой COMMIT (Рис.1.). Если в течение одной из SQL-операций будет ошибка - то COMMIT не произойдет, а будет ROLLBACK, то есть отмена всех операций. Такой механизм гарантируется базой данных. Обращаю внимание, что именно таким образом, работает механизм на уровне базы, а не на уровне приложения.

Рис. 1 Объединение отдельных SQL-операций в DB LUW (транзакция на уровне БД)
Рис. 1 Объединение отдельных SQL-операций в DB LUW (транзакция на уровне БД)

Транзакция базы данных (или DB LUW) представляет собой неделимую последовательность SQL-операций. В случае ошибки хотя бы в одной из них, база данных восстанавливает состояние по всем операциям (выполняется DATABASE ROLLBACK); в случае же успешного выполнения каждой SQL-операции: выполняется DATABASE COMMIT и данные записываются в нужные поля нужных таблиц. Это подход, обеспечивающий целостность данных на уровне БД.

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

Давайте рассмотрим, как обеспечивается консистентное обновление в рамках ABAP-приложений.

Обеспечение консистентности данных в SAP NetWeaver в ABAP/4

В ABAP/4 работа непосредственно с SQL-командами базы данных не ведется. Используется ABAP SQL (open SQL). Вводится понятие SAP LUW. По факту, SAP LUW – это и есть та самая «корректная» подготовка данных внутри нашего приложения (какой бы то ни было сложности) для базы данных. При этом от SAP LUW мы получаем не только консистентность данных, но и управление транзакционностью «любой базы» (таким образом, ABAP может работать с любой базой и корректно подготавливать данные для БД, объединяя SQL-операции в транзакции) (Рис.2).

Рис. 2 Соотнесение SAP LUW и выделенного DB LUW в ABAP-программе с точки зрения SQL-операций (символ "кружочек" на схеме)
Рис. 2 Соотнесение SAP LUW и выделенного DB LUW в ABAP-программе с точки зрения SQL-операций (символ "кружочек" на схеме)

Приложение на языке ABAP/4 может быть разделено на несколько частей, которые могут обрабатываться в различных рабочих процессах на сервере приложений (Application Server). ABAP-приложение НЕ подразумевает под собой один-единственный DB LUW. В рамках ABAP-приложения может быть несколько явных DB LUW; более того, каждый рабочий процесс (work process) завершается неявным DB LUW (Рис.2).

Чтобы управлять консистентностью данных, ABAP-приложение не выполняет запись в базу данных сразу же (не передает команду БД), а откладывает их для отдельного процесса, который может быть выполнен по завершении SAP LUW (Рис.2). Завершается же SAP LUW ABAP-командой COMMIT WORK или ROLLBACK WORK. Объединение требуемых обновлений в процессе работы SAP LUW для отдельного процесса DB LUW называется bundling (группирование / «объединение в кучу»). Подробнее об этом описано в справке к SAP LUW.

Одним из способов корректно подготовить данные в рамках SAP LUW для выделенного DB LUW является использование функциональных модулей обновления (CALL FUNCTION … IN UPDATE TASK).

Использование функциональных модулей обновления в ABAP

Демо-задача: пусть имеется три таблицы базы данных (ZTC8A005_SAMPLE, ZTC8A005_HEADZTC8A005_ITEM). Таблица ZTC8A005_SAMPLE характеризует собой отдельно стоящую сущность; а вот таблицы ZTC8A005_HEAD и ZTC8A005_ITEM связаны между собой кардинальностью 1:M (попросту: заголовок-позиция). В ABAP-приложении требуется обновить и таблицу ZTC8A005_SAMPLE и таблицы ZTC8A005_HEAD, ZTC8A005_ITEM либо, если по логике приложения этого делать не нужно, не обновлять ничего (ни одну из таблиц).

Для большей определенности приведу структуру этих таблиц ниже. (Табл.1-3). Главная цель этих демонстрационных таблиц сочетать разные типы данных.

Структура таблицы ZTC8A005_SAMPLE – отдельно стоящая сущность (Таблица1)

MANDT

MANDT

ENTITY_GUID

CHAR32

Ключевое поле

ENTITY_PARAM1

CHAR10

ENTITY_PARAM2

NUMC10

ENTITY_PARAM3

SYUZEIT

ENTITY_PARAM4

SYDATUM

ENTITY_PARAM5

TIMESTAMP

ENTITY_PARAM6

INT4

Структура таблицы ZTC8A005_HEAD – заголовок сущности (Таблица2)

ID поля

Тип данных

Комментарий

MANDT

MANDT

 

HEAD_GUID

CHAR32

Ключевое поле

HEAD_PARAM1

CHAR10

 

HEAD_PARAM2

NUMC10

 

HEAD_PARAM3

SYUZEIT

 

HEAD_PARAM4

SYDATUM

 

HEAD_PARAM5

TIMESTAMP

 

HEAD_PARAM6

INT4

 

Структура таблицы ZTC8A005_ITEM – позиция сущности

ID поля

Тип данных

Комментарий

MANDT

MANDT

 

HEAD_GUID

CHAR32

Ключевое поле, внешний ключ – таблица ZTC8A005_HEAD

ITEM_GUID

CHAR32

Ключевое поле

ITEM_PARAM1

CHAR10

 

ITEM_PARAM2

NUMC10

 

ITEM_PARAM3

SYUZEIT

 

ITEM_PARAM4

SYDATUM

 

ITEM_PARAM5

TIMESTAMP

 

ITEM_PARAM6

INT4

Для обновления таблицы в режиме UPDATE TASK нужно создать функциональный модуль обновления и передать содержимое таблицы по значению (Рис.4). Например, для таблицы ZTC8A005_SAMPLE функциональный модуль обновления мог бы выглядеть так, как представлено на рисунках ниже. (Рис.3.-Рис.4.)

Рис. 3 Свойства функционального модуля обновления
Рис. 3 Свойства функционального модуля обновления
Рис. 4 Параметры функционального модуля обновления
Рис. 4 Параметры функционального модуля обновления

Обращаю внимание, что в модуль обновления параметры передаются по значению (Рис.4); поэтому для передачи табличных данных нужно создать словарный тип таблицы (Рис.5). В моем примере создан тип таблицы ZTC8A005_SAMPLE_TAB_TYPE на основе структуры ZTC8A005_SAMPLE через транзакцию SE11. (Рис.5.)

Рис. 5 Созданный тип таблицы на основе ZTC8A005_SAMPLE
Рис. 5 Созданный тип таблицы на основе ZTC8A005_SAMPLE

Внутреннее наполнение функционального модуля обновления представлено ниже (Листинг1).

FUNCTION z_c8a_005_demo_upd_sample.
*"----------------------------------------------------------------------
*"*"Функциональный модуль обновления:
*"
*"*"Локальный интерфейс:
*"  IMPORTING
*"     VALUE(IV_UPDKZ) TYPE  UPDKZ DEFAULT 'M'
*"     VALUE(IT_SAMPLE) TYPE  ZTC8A005_SAMPLE_TAB_TYPE
*"----------------------------------------------------------------------
  DATA lc_modify_tab TYPE updkz VALUE 'M'.
  DATA lc_upd_tab TYPE updkz VALUE 'U'.
  DATA lc_del_tab TYPE updkz VALUE 'D'.

  IF it_sample IS INITIAL.
    EXIT.
  ENDIF.

  CASE iv_updkz.
    WHEN lc_modify_tab.
      MODIFY ztc8a005_sample FROM TABLE it_sample.
    WHEN lc_upd_tab.
      UPDATE ztc8a005_sample FROM TABLE it_sample.
    WHEN lc_del_tab.
      DELETE ztc8a005_sample FROM TABLE it_sample.
    WHEN OTHERS.
  ENDCASE.
ENDFUNCTION.

Таким образом, мы подготовили функциональный модуль обновления, но для использования в основном ABAP-приложении, нам нужно корректно вызвать этот функциональный модуль. Пример использования этого функционального модуля приведен в код-листинге ниже (Код-Листинг2.). Отличие от обычного вызова функционального модуля в ключевом слове «IN UPDATE TASK», а также необходимость COMMIT или ROLLBACK в последующей части программы.

    DATA lt_sample_tab TYPE STANDARD TABLE OF ztc8a005_sample.

    lt_sample_tab = VALUE #(
    ( entity_guid = 'FUNC_GUID_MOD' entity_param1 = 'CHAR10' entity_param2 = '0504030201' )
    ( entity_guid = 'FUNC_GUID2_MOD' entity_param1 = '2CHAR10' entity_param2 = '0102030405' )
    ( entity_guid = 'FUNC_GUID2_DEL' entity_param1 = '2CHAR10' entity_param2 = '777909034' ) ).


    CALL FUNCTION 'Z_C8A_005_DEMO_UPD_SAMPLE'
      IN UPDATE TASK
      EXPORTING
        it_sample = lt_sample_tab.


      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
        EXPORTING
          wait = abap_true.

Аналогичным образом можно создать функциональные модули обновления для таблиц ZTC8A005_HEAD и ZTC8A005_ITEM. Демонстрационный код-листинг доступен в отдельном пакете. Описание как и откуда скачать – находится здесь.

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

Теперь к проблеме «стабильность программы и скорость разработки».

Получается, что нужно создать несколько объектов, чтобы корректно обновить базу? Но ведь это замедляет создание приложений? Можно ли не создавать доп.объектов? Можно! :-)

Использование функционала Any Tab Update Task для обновления БД в ABAP-приложениях

С помощью утилиты «Any Tab Update Task» можно не создавать дополнительные функциональные модули и словарные типы таблиц. Детальное описание самой утилиты, а также сам функционал и демо-примеры, находятся здесь и на github.

Преобразуем код из код-листинг 2 с использованием утилиты «AnyTabUpdateTask» (код-листинг 3).

    DATA lc_db_tab_sample TYPE tabname VALUE 'ZTC8A005_SAMPLE'.
    DATA lt_sample_tab TYPE STANDARD TABLE OF ztc8a005_sample.

    lt_sample_tab = VALUE #(
    ( entity_guid = 'FUNC_GUID_MOD' entity_param1 = 'CHAR10' entity_param2 = '0504030201' )
    ( entity_guid = 'FUNC_GUID2_MOD' entity_param1 = '2CHAR10' entity_param2 = '0102030405' )
    ( entity_guid = 'FUNC_GUID2_DEL' entity_param1 = '2CHAR10' entity_param2 = '777909034' ) ).

    NEW zcl_c8a005_save2db(
      )->save2db( iv_tabname = lc_db_tab_sample
                  it_tab_content = lt_sample_tab )->do_commit_if_any( ).

Как видно, сам объем строк стал немного меньше; при этом специальных дополнительных объектов не создавалось. За счет этого повышается скорость разработки без потери стабильности обновления данных.

Теперь вернемся к случаю, когда таблиц несколько (в том числе пустые) с разными типами данных. (Код-Листинг 4)

DATA lc_db_tab_sample TYPE tabname VALUE 'ZTC8A005_SAMPLE'.
DATA lt_sample_tab TYPE STANDARD TABLE OF ztc8a005_sample.
DATA lt_sample_empty_tab TYPE STANDARD TABLE OF ztc8a005_sample.
DATA lt_head_tab TYPE STANDARD TABLE OF ztc8a005_head.
DATA lt_item_tab TYPE STANDARD TABLE OF ztc8a005_item.
DATA lv_ts TYPE timestamp.
DATA lo_saver_anytab TYPE REF TO zcl_c8a005_save2db.

GET TIME STAMP FIELD lv_ts.

lt_sample_tab = VALUE #(
    ( entity_guid = 'ANY_GUID_MOD' entity_param1 = 'CHAR10' entity_param2 = '0504030201'
        entity_param3 = sy-uzeit entity_param4 = sy-datum entity_param5 = lv_ts )
    ( entity_guid = 'ANY_GUID2_MOD' entity_param1 = '2CHAR10' entity_param2 = '0102030405'
      entity_param3 = sy-uzeit entity_param4 = sy-datum entity_param5 = lv_ts )
    ( entity_guid = 'ANY_GUID2_DEL' entity_param1 = '2CHAR10' entity_param2 = '777909034'
      entity_param3 = sy-uzeit entity_param4 = sy-datum entity_param5 = lv_ts )
    ).

lt_head_tab = VALUE #(
    ( head_guid = 'ANY_GUID_UPD' head_param1 = 'ANY_GUID_ADD' head_param2 = '9988776655'
        head_param3 = sy-uzeit head_param4 = sy-datum head_param5 = lv_ts )
    ( head_guid = 'ANY_GUID2_UPD' head_param1 = 'ANY_GUID2_ADD' head_param2 = '9988776655'
        head_param3 = sy-uzeit head_param4 = sy-datum head_param5 = lv_ts )
    ( head_guid = 'ANY_GUID_DEL' head_param1 = 'ANY_GUID_ADD' head_param2 = '9988774444'
        head_param3 = sy-uzeit head_param4 = sy-datum head_param5 = lv_ts )
    ( head_guid = 'ANY_GUID2_DEL' head_param1 = 'ANY_GUID2_ADD' head_param2 = '9988774444'
        head_param3 = sy-uzeit head_param4 = sy-datum head_param5 = lv_ts )
 ).

lt_item_tab = VALUE #(
    ( head_guid = 'ANY_GUID_UPD' item_guid = 'ANY_ITEM_GUID_ADD' item_param1 = '2CHAR10' item_param2 = '9988776655'
        item_param3 = sy-uzeit item_param4 = sy-datum item_param5 = lv_ts )
    ( head_guid = 'ANY_GUID2_UPD' item_guid = 'ANY_ITEM_GUID2_ADD' item_param1 = '2CHAR10'
        item_param3 = sy-uzeit item_param4 = sy-datum item_param5 = lv_ts )
    ( head_guid = 'ANY_GUID_DEL' item_guid = 'ANY_ITEM_GUID_ADD' item_param2 = '9988776655'
        item_param3 = sy-uzeit item_param4 = sy-datum item_param5 = lv_ts )
    ( head_guid = 'ANY_GUID2_DEL' item_guid = 'ANY_ITEM_GUID2_ADD' item_param1 = '2CHAR10'
        item_param3 = sy-uzeit item_param4 = sy-datum item_param5 = lv_ts )
).


CREATE OBJECT lo_saver_anytab.
lo_saver_anytab->save2db( EXPORTING iv_tabname     = lc_db_tab_sample
                                    it_tab_content = lt_sample_tab ).

lo_saver_anytab->save2db( EXPORTING iv_tabname     = 'ZTC8A005_HEAD'
                                    it_tab_content = lt_head_tab ).

lo_saver_anytab->save2db( EXPORTING iv_tabname     = 'ZTC8A005_ITEM'
                                    it_tab_content = lt_item_tab ).

CLEAR lt_sample_empty_tab.
lo_saver_anytab->save2db( EXPORTING iv_tabname     = lc_db_tab_sample
                                    it_tab_content = lt_sample_empty_tab ).


" обновление всех таблиц будет одномоментно после commit
" а по пустой таблицы ничего происходить не будет (не будет поставлен Update Task)
lo_saver_anytab->do_commit_if_any( ).

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

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

Буду рад обратной связи и комментариям.

Комментарии (0)