Blog SAPienteSAPiente
RAP

RAP Unmanaged Scenario: como reutilizar BAPIs e lógica existente

SAPiente5 de ago. de 2026· 16 min read

No unmanaged, o RAP entrega a moldura e você entrega o miolo. Aqui você vê o espectro completo, as duas fases da transação — e uma Web API V4 real criada em cima de BAPI, de ponta a ponta.

A ponte entre o legado e o ABAP Cloud

O unmanaged scenario é o tipo de implementação do RAP em que você escreve toda a lógica transacional e de persistência do business object — o framework fornece apenas a "moldura": o contrato REST, a orquestração da transação, o OData e a EML. Ele existe por um motivo central: reaproveitar lógica que já existe (BAPIs, function modules e regras de gravação legadas) e expô-la como um serviço RAP moderno e Clean Core.

A documentação oficial não faz rodeios: no unmanaged, "não há funcionalidade padrão fornecida pelo framework — é apenas a moldura de um business object, à qual o desenvolvedor precisa aderir". Você declara as operações na behavior definition, mas implementa cada uma manualmente em classes ABAP no behavior pool — inclusive o buffer transacional, normalmente delegado às próprias function modules legadas, que mantêm o estado em memória.

Se você vai criar um app do zero, sem nada para reutilizar, o managed é mais simples e exige muito menos código. A regra prática que fecha este post já vale de abertura: comece pelo managed sempre que puder, e traga o unmanaged quando houver algo que valha a pena reutilizar.

E este post agora vai além do conceito: depois do passo a passo didático com o modelo /DMO, a gente implementa o cenário que mais aparece em projeto de verdade — expor uma BAPI standard como Web API OData V4, com criação de ordem de venda via BAPI_SALESORDER_CREATEFROMDAT2.

Antes da Comparação: o espectro completo de persistência

A maioria dos textos apresenta managed vs. unmanaged como uma escolha binária — e ela não é. Entre os dois extremos existem duas variantes intermediárias do managed que resolvem casos que muita gente ataca (sem precisar) com um unmanaged completo:

Tipos

Interaction phase (buffer, CRUD)

Save (persistência)

Quando

managed

Framework

Framework grava na persistent table

Greenfield puro: tabela própria, regras próprias

managed with additional save

Framework

Framework grava + você adiciona passos no save_modified

Gravação padrão serve, mas falta algo no commit: change documents, application log, disparar evento

managed with unmanaged save

Framework

Você grava no save_modified (sem persistent table na BDEF)

O buffer e o CRUD do framework servem, mas a gravação é sua: BAPI, múltiplas tabelas, API externa

unmanaged

Você (handler + buffer próprio/da FM)

Você (saver completo)

A lógica legada domina o ciclo inteiro — buffer, validação e gravação já existem nas FMs

Detalhes que a documentação crava sobre as variantes intermediárias:

  • Ambas exigem a reimplementação do método save_modified na saver class — e, nelas, o método save clássico não pode ser usado.

  • No unmanaged save, a BDEF não pode especificar persistent table — afinal, quem grava é você.

  • O save_modified recebe as instâncias a criar/alterar/excluir em parâmetros tipados com os BDEF derived types TYPE REQUEST FOR CHANGE e TYPE REQUEST FOR DELETE; por padrão só chegam chaves + campos alterados — a adição with full data entrega a instância completa.

  • A declaração vale para o BO inteiro (no header: managed with unmanaged save implementation in class ...) ou por entidade (define behavior for Entidade with unmanaged save) — dá para misturar sabores no mesmo BO.

A pergunta certa antes de escolher unmanaged: o que é legado de verdade — o ciclo inteiro (buffer, validações, CRUD) ou só a gravação? Se é só a gravação, um managed with unmanaged save entrega o buffer, o draft e o CRUD de graça, e você implementa um único método. O unmanaged completo é para quando a FM legada já é dona do buffer.

Managed vs. unmanaged: a diferença que importa

Aspecto

Managed

Unmanaged

Buffer transacional

O framework provê e gerencia

Você (ou a FM legada) gerencia

CREATE / UPDATE / DELETE

Gerados automaticamente

Você implementa no handler

READ

Out of the box

Você implementa (alimenta OData e ETag)

LOCK

Framework (lock master)

Você implementa (tipicamente reusa o ENQUEUE da FM)

Persistência (SAVE)

O framework grava na tabela

Você grava (saver), via FM/BAPI

Esforço de desenvolvimento

Baixo

Alto

Cenário ideal

App novo do zero (greenfield)

Reusar lógica existente (brownfield)

Em ambos os casos você ganha a mesma "casca" do RAP: serviço OData (UI ou Web API), integração com Fiori Elements e acesso via EML. A diferença está em quem implementa o miolo transacional.

Quando usar: os casos concretos

  • Expor um cadastro legado como serviço: o cadastro vive há 15 anos em FMs de create/change/save com buffer próprio e ENQUEUE próprio — o unmanaged veste o RAP por cima sem reescrever nada.

  • Documento standard sem API released: a criação passa por BAPI clássica (com wrapper para o ABAP Cloud) e a persistência é dela, não sua — o BO unmanaged orquestra a BAPI dentro do contrato RAP. É exatamente o caso da segunda metade deste post.

  • Persistência que não é uma tabela simples: regras de gravação próprias, múltiplas tabelas amarradas, número de documento por number range no commit.

  • Migração incremental de app clássico (MVC) para o ABAP Cloud: a UI vira Fiori Elements hoje; o miolo continua nas FMs testadas — e vai sendo substituído aos poucos.

  • Controle total do ciclo: lock, save, cleanup e design da API nas suas mãos.

Vantagens: reuso de código testado, flexibilidade total, caminho natural de modernização brownfield — e a mesma porta de saída moderna (OData V4, Clean Core). Desvantagens: mais código para escrever e manter (handler + saver + mapeamentos), e a exigência de dominar o ciclo transacional do RAP — que é exatamente o próximo assunto.

As duas fases de uma transação RAP

Para implementar um unmanaged corretamente, é essencial entender as duas fases que todo BO RAP atravessa — e, dentro da segunda, a fronteira que separa "ainda dá para rejeitar" de "não tem mais volta".

1. Interaction phase

É a fase em que o consumidor (o app Fiori, um sistema externo via OData, ou um chamador EML) lê, filtra e modifica instâncias. As mudanças são mantidas no transactional buffer — um armazenamento temporário no sistema; elas ainda não estão no banco de dados. Aqui rodam os métodos do handler, uma classe local que herda de CL_ABAP_BEHAVIOR_HANDLER:

Método

Papel

create / update / delete

Operações de modificação (buffer!)

read

Leitura de instâncias — alimenta o OData e a verificação de ETag

lock

Bloqueio contra acesso concorrente (reuse o ENQUEUE legado)

get_instance_authorizations

Autorização por instância

get_instance_features

Feature control dinâmico (habilitar/desabilitar operações e actions)

cba_* / rba_*

Create/read by association — entidades dependentes

2. Save sequence — em duas metades

A save sequence dispara depois de pelo menos uma modificação bem-sucedida, quando o consumidor pede para salvar. É implementada na saver class, que herda de CL_ABAP_BEHAVIOR_SAVER — e a documentação a divide em duas metades com semânticas bem diferentes:

Fase

Método

Papel

Early save (ainda pode falhar)

finalize

Últimos cálculos e determinações antes de persistir — é o primeiro método da fase

check_before_save

Verificação final; se falhar, a transação inteira é rejeitada (rollback)

Late save (ponto de não retorno)

adjust_numbers

Late numbering: as chaves definitivas são atribuídas aqui

save

Grava de fato o buffer no banco — aqui entra a BAPI/FM de persistência

Limpeza

cleanup

Limpa o buffer após o save (ou após rollback)

cleanup_finalize

Limpeza final

O ponto de não retorno: passou do check_before_save, a transação não pode mais falhar de forma controlada. É por isso que os métodos da late save (como o save_modified das variantes managed) nem recebem o parâmetro failed — erro do consumidor não pode aparecer depois da early save; se a aplicação precisar abortar ali, o resultado é runtime error. Moral: toda validação que pode rejeitar a transação pertence à interaction phase ou ao check_before_save — nunca ao save.

E o erro que derruba todo mundo: você NÃO dá COMMIT WORK no save. Quem orquestra o commit é o framework RAP, alinhado à SAP LUW. Se a sua BAPI faz COMMIT internamente, isso quebra o contrato — por isso muitas BAPIs precisam de um wrapper antes de entrar no ABAP Cloud. Use BAPIs que aceitem controle externo de commit, ou embrulhe.

A arquitetura na prática

Um BO unmanaged é composto por três peças:

Peça

Conteúdo

CDS

A interface view (modelo de dados) e a projection view (camada exposta ao serviço)

Behavior Definition

Declara unmanaged implementation, lista as operações e define o mapping entre os campos do BO e a estrutura legada

Behavior Pool

A classe ABAP com o handler (interaction phase) e o saver (save sequence), ambos no CCIMP (Local Types)

Passo a passo com código: o caso didático (/DMO)

Os exemplos usam o modelo de demonstração /DMO da SAP (o mesmo do openSAP), que já traz function modules prontas para criar, alterar e salvar viagens — com buffer próprio, exatamente o cenário-alvo do unmanaged clássico. Guarde essa característica: as FMs /DMO trabalham em memória e só gravam no save. Mais adiante você vai ver que BAPIs standard se comportam diferente — e isso muda onde cada coisa é chamada.

1. A behavior definition

Declare o cenário como unmanaged (com strict ( 2 ) — obrigatório para ABAP Cloud e recomendado sempre) e use o mapping for ... control para ligar os campos do BO à estrutura legada. A control structure (x-structure) indica quais campos foram preenchidos, imitando o padrão das BAPIs:

unmanaged implementation in class zbp_i_travel_u unique;
strict ( 2 );

define behavior for ZI_Travel_U alias Travel
late numbering
lock master
authorization master ( instance )
etag master LastChangedAt
{
  create;
  update;
  delete;

  field ( readonly ) TravelId;

  mapping for /dmo/travel control /dmo/s_travel_intx
  {
    TravelId      = travel_id;
    AgencyId      = agency_id;
    CustomerId    = customer_id;
    BeginDate     = begin_date;
    EndDate       = end_date;
    Description   = description;
    Status        = status;
    LastChangedAt = lastchangedat;
  }
}

O bloco mapping substitui dezenas de MOVE-CORRESPONDING espalhados pelo código: o CORRESPONDING com as adições MAPPING FROM ENTITY / USING CONTROL faz a conversão nos dois sentidos, incluindo a x-structure. E o late numbering declara que a chave TravelId só nasce na save sequence — o consumidor cria com %cid, sem chave.

2. O handler — interaction phase

Cada operação vira um método no handler. É aqui que você chama a FM legada (que trabalha em buffer) e converte o resultado para as estruturas do RAP — incluindo o tratamento de erro via failed/reported, que a versão de tutorial sempre esquece:

CLASS lhc_travel DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS create FOR MODIFY IMPORTING entities FOR CREATE Travel.
    METHODS update FOR MODIFY IMPORTING entities FOR UPDATE Travel.
    METHODS delete FOR MODIFY IMPORTING keys     FOR DELETE Travel.
    METHODS read   FOR READ   IMPORTING keys     FOR READ   Travel RESULT result.
    METHODS lock   FOR LOCK   IMPORTING keys     FOR LOCK   Travel.
ENDCLASS.

CLASS lhc_travel IMPLEMENTATION.

  METHOD create.
    LOOP AT entities INTO DATA(entity).
      DATA(ls_travel_in) = CORRESPONDING /dmo/travel( entity MAPPING FROM ENTITY ).

      CALL FUNCTION '/DMO/FLIGHT_TRAVEL_CREATE'
        EXPORTING is_travel   = CORRESPONDING /dmo/s_travel_in( ls_travel_in )
        IMPORTING es_travel   = DATA(ls_travel_out)
                  et_messages = DATA(lt_messages).

      IF lt_messages IS INITIAL.
        " sucesso: devolve %cid -> chave preliminar (definitiva vem no adjust_numbers)
        APPEND VALUE #( %cid     = entity-%cid
                        TravelId = ls_travel_out-travel_id ) TO mapped-travel.
      ELSE.
        " erro: falha estruturada, não exceção "seca"
        APPEND VALUE #( %cid = entity-%cid ) TO failed-travel.
        APPEND VALUE #( %cid = entity-%cid
                        %msg = new_message( id       = lt_messages[ 1 ]-msgid
                                            number   = lt_messages[ 1 ]-msgno
                                            severity = if_abap_behv_message=>severity-error
                                            v1       = lt_messages[ 1 ]-msgv1 ) )
               TO reported-travel.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD update.
    LOOP AT entities INTO DATA(entity).
      " USING CONTROL: preenche a x-structure a partir do %control do RAP
      DATA(ls_travel)  = CORRESPONDING /dmo/travel(        entity MAPPING FROM ENTITY ).
      DATA(ls_travelx) = CORRESPONDING /dmo/s_travel_intx( entity MAPPING FROM ENTITY USING CONTROL ).

      CALL FUNCTION '/DMO/FLIGHT_TRAVEL_UPDATE'
        EXPORTING is_travel   = CORRESPONDING /dmo/s_travel_in( ls_travel )
                  is_travelx  = ls_travelx
        IMPORTING et_messages = DATA(lt_messages).
      " ... mesmo padrão de failed/reported do create
    ENDLOOP.
  ENDMETHOD.

  METHOD read.
    LOOP AT keys INTO DATA(key).
      CALL FUNCTION '/DMO/FLIGHT_TRAVEL_READ'
        EXPORTING iv_travel_id = key-TravelId
        IMPORTING es_travel    = DATA(ls_travel)
                  et_messages  = DATA(lt_messages).

      IF lt_messages IS INITIAL.
        APPEND CORRESPONDING #( ls_travel MAPPING TO ENTITY ) TO result.
      ELSE.
        APPEND VALUE #( %tky = key-%tky ) TO failed-travel.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD lock.
    " reusa o bloqueio legado — não invente um segundo lock
    TRY.
        cl_abap_lock_object_factory=>get_instance( iv_name = '/DMO/ETRAVEL'
          )->enqueue( it_parameter = VALUE #( ( name  = 'TRAVEL_ID'
                                                value = REF #( keys[ 1 ]-TravelId ) ) ) ).
      CATCH cx_abap_foreign_lock INTO DATA(lx_lock).
        APPEND VALUE #( %tky = keys[ 1 ]-%tky ) TO failed-travel.
        APPEND VALUE #( %tky = keys[ 1 ]-%tky
                        %msg = new_message_with_text(
                                 severity = if_abap_behv_message=>severity-error
                                 text     = lx_lock->get_text( ) ) ) TO reported-travel.
    ENDTRY.
  ENDMETHOD.

ENDCLASS.

Repare no padrão: na interaction phase você trabalha em buffer (as FMs /DMO mantêm o próprio) e só persiste de verdade na fase de salvamento. E cada falha vira failed + reported — é isso que a UI Fiori transforma em mensagem para o usuário.

3. O saver — save sequence

O saver herda de cl_abap_behavior_saver e redefine só o que precisa. Para reuso de FMs com late numbering, o trio típico é adjust_numbers (chave definitiva), save (persistir) e cleanup (limpar o buffer):

CLASS lsc_zi_travel_u DEFINITION INHERITING FROM cl_abap_behavior_saver.
  PROTECTED SECTION.
    METHODS adjust_numbers REDEFINITION.
    METHODS save           REDEFINITION.
    METHODS cleanup        REDEFINITION.
ENDCLASS.

CLASS lsc_zi_travel_u IMPLEMENTATION.

  METHOD adjust_numbers.
    " late numbering: troca a chave preliminar (%pid/%pre) pela definitiva
    LOOP AT mapped-travel ASSIGNING FIELD-SYMBOL(<travel>).
      <travel>-TravelId = get_next_travel_id( ).   " number range / FM legada
    ENDLOOP.
  ENDMETHOD.

  METHOD save.
    " persiste o buffer das FMs legadas — SEM commit explícito!
    CALL FUNCTION '/DMO/FLIGHT_TRAVEL_SAVE'.
  ENDMETHOD.

  METHOD cleanup.
    " limpa o buffer das FMs após salvar ou após rollback:
    " a mesma sessão ABAP pode servir outra transação RAP — buffer sujo = inconsistência
    CALL FUNCTION '/DMO/FLIGHT_TRAVEL_INITIALIZE'.
  ENDMETHOD.

ENDCLASS.

4. Testar via EML — antes de qualquer serviço

Não espere o binding para descobrir se o BO funciona. Um classrun com EML exercita o ciclo completo (interaction phase + save sequence) em segundos:

METHOD if_oo_adt_classrun~main.
  MODIFY ENTITIES OF ZI_Travel_U
    ENTITY Travel
      CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate Description )
      WITH VALUE #( ( %cid        = 'create1'
                      AgencyId    = '070001'
                      CustomerId  = '000001'
                      BeginDate   = cl_abap_context_info=>get_system_date( )
                      EndDate     = cl_abap_context_info=>get_system_date( ) + 10
                      Description = 'Teste unmanaged via EML' ) )
    MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).

  COMMIT ENTITIES.   " aqui dispara a save sequence inteira

  IF sy-subrc = 0.
    out->write( |Criado: { mapped-travel[ 1 ]-TravelId }| ).
  ENDIF.
ENDMETHOD.

Na prática: BAPI_SALESORDER_CREATEFROMDAT2 como Web API OData V4

Agora o cenário que aparece em quase todo projeto: um sistema externo (e-commerce, portal, middleware) precisa criar ordens de venda no S/4HANA. A criação de ordem passa pela BAPI_SALESORDER_CREATEFROMDAT2, e você quer expor isso como uma Web API OData V4 limpa — em vez de abrir a BAPI inteira via RFC/SOAP, com seus 40 parâmetros e 90% de campos que o consumidor não precisa conhecer.

Esse é o padrão que a própria SAP chama de RAP facade: você modela um BO enxuto, com só os campos que fazem sentido para o consumidor (princípio need-to-know), e a BAPI vira detalhe de implementação escondido atrás do contrato OData. O consumidor faz um POST com JSON simples e recebe de volta o número da ordem criada.

A natureza da BAPI muda o jogo

Aqui mora a diferença crucial em relação ao exemplo /DMO — e o motivo de muita implementação dar dump. As FMs /DMO têm buffer próprio: você chama create na interaction phase, os dados ficam em memória, e o save persiste. BAPIs transacionais standard não funcionam assim. Elas são "one-shot": uma chamada valida os dados e registra a gravação em update task, tudo de uma vez. E o RAP, para respeitar a SAP LUW, só permite registrar update task na late save phase — chamar a BAPI de verdade num handler da interaction phase gera runtime error.

A boa notícia: a maioria das BAPIs de POST tem um "VALIDATE" — no caso da BAPI_SALESORDER_CREATEFROMDAT2, o parâmetro TESTRUN, que executa todas as validações sem registrar gravação nenhuma. É ele que permite encaixar a BAPI no ciclo RAP sem violar o ponto de não retorno:

Chamada

Fase RAP

Por quê

BAPI com TESTRUN = 'X'

check_before_save (early save)

Valida tudo enquanto a transação ainda pode falhar de forma controlada — erro vira mensagem para o consumidor, não dump

BAPI de verdade (registra update task)

adjust_numbers (late save)

Update task só é permitida na late save; e como o número da ordem nasce na BAPI, ele precisa ser capturado aqui — é o adjust_numbers que devolve a chave definitiva ao framework

BAPI_TRANSACTION_COMMIT

Nunca

O framework RAP dá o commit no fim da LUW — commit explícito é dump na certa

Repare no detalhe do adjust_numbers: no exemplo /DMO, o número vinha de um number range e o save chamava a FM de gravação. Com BAPI standard, é a própria BAPI que gera o número — então a chamada real acontece no adjust_numbers (que faz parte da late save e aceita update task), e o método save fica praticamente vazio: a gravação já está registrada, e o commit do framework a dispara.

1. O modelo de dados: uma facade enxuta

A interface view lê das CDS standard de ordem de venda — assim o READ (e o GET do OData) sai de graça do banco, sem BAPI de leitura. Só os campos que o consumidor precisa:

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order - Facade Unmanaged'
define root view entity ZI_SalesOrder_U
  as select from I_SalesDocument
  composition [1..*] of ZI_SalesOrderItem_U as _Item
{
  key SalesDocument           as SalesOrder,
      SalesDocumentType       as SalesOrderType,
      SalesOrganization,
      DistributionChannel,
      OrganizationDivision    as Division,
      SoldToParty,
      PurchaseOrderByCustomer as CustomerReference,
      TotalNetAmount,
      TransactionCurrency,
      _Item
}
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Item - Facade Unmanaged'
define view entity ZI_SalesOrderItem_U
  as select from I_SalesDocumentItem
  association to parent ZI_SalesOrder_U as _SalesOrder
    on $projection.SalesOrder = _SalesOrder.SalesOrder
{
  key SalesDocument     as SalesOrder,
  key SalesDocumentItem as SalesOrderItem,
      Material,
      OrderQuantity,
      OrderQuantityUnit,
      NetAmount,
      _SalesOrder
}

Dependendo da release, ajuste os nomes de campo das views standard — o ponto é o padrão: leitura via CDS, escrita via BAPI.

2. A behavior definition: create-only, late numbering

Uma decisão de design que economiza metade do código: a facade expõe criação e leitura. Alterar ordem de venda é outra BAPI, outro fluxo de validação, outro post. Sem update/delete, você não precisa de lock nem de ETag — e o BDEF fica assim:

unmanaged implementation in class zbp_i_salesorder_u unique;
strict ( 2 );

define behavior for ZI_SalesOrder_U alias SalesOrder
late numbering
authorization master ( global )
{
  create;
  field ( readonly ) SalesOrder;

  association _Item { create; }
}

define behavior for ZI_SalesOrderItem_U alias SalesOrderItem
late numbering
{
  field ( readonly ) SalesOrder, SalesOrderItem;

  association _SalesOrder;
}

late numbering nas duas entidades: nem o número da ordem nem o número do item existem antes da BAPI rodar. O consumidor cria com %cid, e as chaves definitivas nascem no adjust_numbers.

3. O handler: buffer próprio, sem tocar na BAPI

Como a BAPI não tem buffer (é one-shot), quem bufferiza é você. Na interaction phase, o create só coleta o payload e emite uma chave preliminar (%pid) — nada de BAPI aqui:

CLASS lcl_buffer DEFINITION.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_item,
             pid_root TYPE abp_behv_pid,
             pid      TYPE abp_behv_pid,
             material TYPE matnr,
             quantity TYPE kwmeng,
             uom      TYPE vrkme,
           END OF ty_item,
           BEGIN OF ty_order,
             pid          TYPE abp_behv_pid,
             order_type   TYPE auart,
             sales_org    TYPE vkorg,
             distr_chan   TYPE vtweg,
             division     TYPE spart,
             sold_to      TYPE kunnr,
             customer_ref TYPE bstkd,
           END OF ty_order.
    CLASS-DATA: gt_orders TYPE TABLE OF ty_order WITH KEY pid,
                gt_items  TYPE TABLE OF ty_item  WITH KEY pid.
ENDCLASS.

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS create FOR MODIFY
      IMPORTING entities FOR CREATE SalesOrder.
    METHODS cba_item FOR MODIFY
      IMPORTING entities_cba FOR CREATE SalesOrder\_Item.
    METHODS read FOR READ
      IMPORTING keys FOR READ SalesOrder RESULT result.
    METHODS rba_item FOR READ
      IMPORTING keys_rba FOR READ SalesOrder\_Item FULL result_requested
      RESULT result LINK association_links.
    METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION
      IMPORTING REQUEST requested_authorizations FOR SalesOrder RESULT result.
ENDCLASS.

CLASS lhc_salesorder IMPLEMENTATION.

  METHOD create.
    LOOP AT entities INTO DATA(entity).
      " chave preliminar: o framework troca pelo VBELN no adjust_numbers
      DATA(lv_pid) = CONV abp_behv_pid( cl_system_uuid=>create_uuid_x16_static( ) ).

      APPEND VALUE #( pid          = lv_pid
                      order_type   = entity-SalesOrderType
                      sales_org    = entity-SalesOrganization
                      distr_chan   = entity-DistributionChannel
                      division     = entity-Division
                      sold_to      = entity-SoldToParty
                      customer_ref = entity-CustomerReference ) TO lcl_buffer=>gt_orders.

      APPEND VALUE #( %cid = entity-%cid
                      %pid = lv_pid ) TO mapped-salesorder.
    ENDLOOP.
  ENDMETHOD.

  METHOD cba_item.
    LOOP AT entities_cba INTO DATA(cba).
      " %cid_ref aponta para o create do pai na mesma transação
      READ TABLE mapped-salesorder INTO DATA(ls_parent)
        WITH KEY %cid = cba-%cid_ref.

      LOOP AT cba-%target INTO DATA(item).
        DATA(lv_pid_item) = CONV abp_behv_pid( cl_system_uuid=>create_uuid_x16_static( ) ).

        APPEND VALUE #( pid_root = ls_parent-%pid
                        pid      = lv_pid_item
                        material = item-Material
                        quantity = item-OrderQuantity
                        uom      = item-OrderQuantityUnit ) TO lcl_buffer=>gt_items.

        APPEND VALUE #( %cid = item-%cid
                        %pid = lv_pid_item ) TO mapped-salesorderitem.
      ENDLOOP.
    ENDLOOP.
  ENDMETHOD.

  METHOD read.
    " ordens já persistidas: leitura direta da CDS — é isso que responde o GET
    " e a releitura que o framework faz após o save (resposta do POST)
    SELECT FROM ZI_SalesOrder_U
      FIELDS SalesOrder, SalesOrderType, SalesOrganization, DistributionChannel,
             Division, SoldToParty, CustomerReference, TotalNetAmount, TransactionCurrency
      FOR ALL ENTRIES IN @keys
      WHERE SalesOrder = @keys-SalesOrder
      INTO CORRESPONDING FIELDS OF TABLE @result.
  ENDMETHOD.

  METHOD rba_item.
    " mesmo padrão do read, via ZI_SalesOrderItem_U + association_links
  ENDMETHOD.

  METHOD get_global_authorizations.
    " AUTHORITY-CHECK do objeto V_VBAK_AAT (ou o padrão do seu projeto)
  ENDMETHOD.

ENDCLASS.

4. O saver: TESTRUN na early save, BAPI real no adjust_numbers

Dois detalhes fazem toda a diferença aqui. Primeiro: o saver herda de cl_abap_behavior_saver_failed — a variante que adiciona o parâmetro failed ao adjust_numbers, como rede de segurança caso a BAPI falhe mesmo depois do testrun. Segundo: a montagem do payload da BAPI vira um método privado reutilizado nas duas chamadas — garantindo que o que foi validado é exatamente o que será gravado:

CLASS lsc_zi_salesorder_u DEFINITION INHERITING FROM cl_abap_behavior_saver_failed.
  PROTECTED SECTION.
    METHODS check_before_save REDEFINITION.
    METHODS adjust_numbers    REDEFINITION.
    METHODS save              REDEFINITION.
    METHODS cleanup           REDEFINITION.
  PRIVATE SECTION.
    METHODS call_bapi
      IMPORTING is_order        TYPE lcl_buffer=>ty_order
                iv_testrun      TYPE abap_boolean
      EXPORTING ev_salesorder   TYPE vbeln_va
                et_return       TYPE bapiret2_t.
ENDCLASS.

CLASS lsc_zi_salesorder_u IMPLEMENTATION.

  METHOD call_bapi.
    DATA(ls_header) = VALUE bapisdhd1( doc_type   = is_order-order_type
                                       sales_org  = is_order-sales_org
                                       distr_chan = is_order-distr_chan
                                       division   = is_order-division
                                       purch_no_c = is_order-customer_ref ).

    DATA(lt_partners) = VALUE bapiparnr_t( ( partn_role = 'AG'
                                             partn_numb = is_order-sold_to ) ).

    DATA(lt_items)     = VALUE bapisditm_t( ).
    DATA(lt_schedules) = VALUE bapischdl_t( ).
    DATA(lv_posnr)     = VALUE posnr( ).

    LOOP AT lcl_buffer=>gt_items INTO DATA(ls_item)
      WHERE pid_root = is_order-pid.
      lv_posnr += 10.
      APPEND VALUE #( itm_number = lv_posnr
                      material   = ls_item-material
                      target_qty = ls_item-quantity ) TO lt_items.
      " o clássico: quantidade vai (também) na schedule line, senão a ordem nasce zerada
      APPEND VALUE #( itm_number = lv_posnr
                      req_qty    = ls_item-quantity ) TO lt_schedules.
    ENDLOOP.

    CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
      EXPORTING
        order_header_in = ls_header
        testrun         = iv_testrun
      IMPORTING
        salesdocument   = ev_salesorder
      TABLES
        order_items_in     = lt_items
        order_partners     = lt_partners
        order_schedules_in = lt_schedules
        return             = et_return.
  ENDMETHOD.

  METHOD check_before_save.
    " EARLY SAVE: valida com TESTRUN — aqui ainda dá para rejeitar com mensagem limpa
    LOOP AT lcl_buffer=>gt_orders INTO DATA(ls_order).
      call_bapi( EXPORTING is_order   = ls_order
                           iv_testrun = abap_true
                 IMPORTING et_return  = DATA(lt_return) ).

      LOOP AT lt_return INTO DATA(ls_ret) WHERE type CA 'EAX'.
        APPEND VALUE #( %pid = ls_order-pid ) TO failed-salesorder.
        APPEND VALUE #( %pid = ls_order-pid
                        %msg = new_message( id       = ls_ret-id
                                            number   = ls_ret-number
                                            severity = if_abap_behv_message=>severity-error
                                            v1 = ls_ret-message_v1  v2 = ls_ret-message_v2
                                            v3 = ls_ret-message_v3  v4 = ls_ret-message_v4 ) )
               TO reported-salesorder.
      ENDLOOP.
    ENDLOOP.
  ENDMETHOD.

  METHOD adjust_numbers.
    " LATE SAVE: chamada real — a BAPI registra a update task e devolve o VBELN.
    " É aqui (e só aqui) que a chave definitiva pode nascer.
    LOOP AT lcl_buffer=>gt_orders INTO DATA(ls_order).
      call_bapi( EXPORTING is_order      = ls_order
                           iv_testrun    = abap_false
                 IMPORTING ev_salesorder = DATA(lv_vbeln)
                           et_return     = DATA(lt_return) ).

      IF lv_vbeln IS NOT INITIAL.
        " troca a chave preliminar (%pid) pela definitiva — o framework
        " usa isso para montar a resposta do POST com o número real
        APPEND VALUE #( %pid       = ls_order-pid
                        SalesOrder = lv_vbeln ) TO mapped-salesorder.

        DATA(lv_posnr) = VALUE posnr( ).
        LOOP AT lcl_buffer=>gt_items INTO DATA(ls_item)
          WHERE pid_root = ls_order-pid.
          lv_posnr += 10.
          APPEND VALUE #( %pid           = ls_item-pid
                          SalesOrder     = lv_vbeln
                          SalesOrderItem = lv_posnr ) TO mapped-salesorderitem.
        ENDLOOP.
      ELSE.
        " rede de segurança: só existe porque herdamos de cl_abap_behavior_saver_failed
        APPEND VALUE #( %pid = ls_order-pid ) TO failed-salesorder.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD save.
    " nada a fazer: a update task já foi registrada pela BAPI no adjust_numbers;
    " o COMMIT do framework RAP dispara a gravação. E NUNCA chame
    " BAPI_TRANSACTION_COMMIT aqui — é dump garantido.
  ENDMETHOD.

  METHOD cleanup.
    CLEAR: lcl_buffer=>gt_orders, lcl_buffer=>gt_items.
  ENDMETHOD.

ENDCLASS.

Por que validar duas vezes? Porque cada chamada faz um trabalho diferente. O TESTRUN no check_before_save protege o consumidor: erro de negócio (cliente bloqueado, material inexistente, org de vendas inválida) vira HTTP 4xx com mensagem estruturada. A chamada real no adjust_numbers assume que já está tudo validado — se ainda assim falhar (condição de corrida, por exemplo), o failed do saver_failed evita o dump, mas a transação inteira é abortada. É o comportamento correto: depois da early save, não existe "meio erro".

5. Expor como Web API OData V4

Com o BO testado via EML (mesmo padrão do classrun da seção anterior — crie a ordem com CREATE + CREATE BY \_Item e dê COMMIT ENTITIES), a exposição segue a trilha padrão do RAP:

  • Projection views ZC_SalesOrder_U e ZC_SalesOrderItem_U + projection BDEF com use create; e use association _Item { create; } — sem update, sem delete, espelhando a facade.

  • Service definition expondo as duas projections (dica: use provider contract entity_ids quando disponível na sua release, e nomeie pensando no consumidor — SalesOrder, não ZC_SALESORDER_U).

  • Service binding do tipo OData V4 - Web API — e não "UI". A diferença não é cosmética: o binding Web API não gera anotações de UI no metadata e é o contrato correto para consumo sistema-a-sistema.

  • Publicação e acesso: no ABAP Cloud/BTP, ative via ADT e crie o Communication Scenario + Arrangement com usuário de comunicação; no on-premise/Private Cloud (tier clássico), publique o service group na /IWFND/V4_ADMIN e providencie o usuário de serviço com as autorizações SD adequadas.

O consumidor cria a ordem com um único POST — deep insert V4, itens aninhados na navegação:

POST /sap/opu/odata4/sap/zapi_salesorder/srvd_a2x/sap/zapi_salesorder/0001/SalesOrder
Content-Type: application/json

{
  "SalesOrderType": "OR",
  "SalesOrganization": "1710",
  "DistributionChannel": "10",
  "Division": "00",
  "SoldToParty": "0010100001",
  "CustomerReference": "PEDIDO-ECOM-4711",
  "_Item": [
    { "Material": "TG11", "OrderQuantity": "10", "OrderQuantityUnit": "PC" },
    { "Material": "TG12", "OrderQuantity": "5",  "OrderQuantityUnit": "PC" }
  ]
}

E a resposta fecha o ciclo com elegância: como o adjust_numbers devolveu a chave definitiva em mapped, o framework responde o 201 Created já com o número real da ordem — releitura feita pelo seu método read, direto da CDS, após o commit:

HTTP/1.1 201 Created

{
  "SalesOrder": "0000012345",
  "SalesOrderType": "OR",
  "SalesOrganization": "1710",
  "SoldToParty": "0010100001",
  "CustomerReference": "PEDIDO-ECOM-4711",
  "TotalNetAmount": "1500.00",
  "TransactionCurrency": "BRL"
}

Do lado de lá, ninguém sabe que existe uma BAPI de 1996 fazendo o trabalho pesado. Contrato limpo, payload mínimo, mensagens de erro estruturadas — e zero modificação no standard. Clean Core na prática.

Boas práticas e erros comuns

  • Nunca dê COMMIT WORK no saver: o RAP controla a transação. BAPI com commit interno = wrapper antes de entrar. E BAPI_TRANSACTION_COMMIT dentro do BO = dump.

  • Validação que rejeita = early save (ou interaction phase). Depois do check_before_save não há falha controlada — é o ponto de não retorno.

  • BAPI transacional só na late save: update task é proibida na interaction phase. O VALIDATE (TESTRUN) pode rodar antes; a chamada real, não.

  • Número que nasce na BAPI = chamada no adjust_numbers: é o único ponto da late save em que você ainda consegue devolver a chave definitiva ao framework (via mapped) — e é isso que faz o POST responder com o número real.

  • Use cl_abap_behavior_saver_failed com BAPIs: a variante habilita failed no adjust_numbers e evita que um erro tardio da BAPI vire short dump.

  • Use o mapping ... control: CORRESPONDING ... MAPPING FROM ENTITY USING CONTROL elimina a conversão manual e mantém a compatibilidade com as x-structures das BAPIs.

  • Separe interação de salvamento: nos handlers, buffer; no save, persistência. FM que grava direto no handler quebra o modelo.

  • Reaproveite o lock existente: se a FM já bloqueia via ENQUEUE, use o mesmo objeto de bloqueio no método lock — dois locks para o mesmo dado é receita de deadlock. E se a facade é create-only, você nem precisa de lock.

  • Trate erros em failed/reported: devolva falhas estruturadas, em vez de levantar exceções "secas" que viram short dump na cara do consumidor.

  • Sempre strict ( 2 ): os checks adicionais do modo estrito são obrigatórios no ABAP Cloud e pegam erro de contrato em tempo de ativação.

  • Limpe o buffer no cleanup religiosamente: a mesma sessão pode atender outra transação RAP — buffer sujo de uma transação anterior é o bug intermitente mais difícil de reproduzir que existe.

  • Antes de partir para o unmanaged completo, confira o espectro: se só a gravação é legada, managed with unmanaged save resolve com uma fração do código — inclusive no cenário de BAPI (a própria SAP simplificou o guia de facade para essa variante).

Perguntas frequentes

Qual a diferença essencial entre managed e unmanaged?

No managed, o framework implementa o buffer, as operações e a persistência por você. No unmanaged, você implementa tudo isso no behavior pool (handler + saver), normalmente reusando BAPIs/FMs — que costumam trazer o próprio buffer.

E o "managed with unmanaged save"? Não é a mesma coisa?

Não — e a diferença economiza semanas. Nele, a interaction phase inteira (buffer, CRUD, draft) continua com o framework; só a gravação é sua, no save_modified, recebendo as instâncias em TYPE REQUEST FOR CHANGE/DELETE. É o sabor certo quando a tabela/BAPI de destino é legada mas o ciclo transacional pode ser novo. O unmanaged completo só se justifica quando o buffer também é legado — ou quando você quer controle total do ciclo, como na facade deste post.

Onde eu chamo a BAPI: no save ou no adjust_numbers?

Depende de quem gera a chave. Se o número vem de number range seu, o adjust_numbers resolve a numeração e o save chama a gravação. Se o número nasce dentro da BAPI (caso das ordens de venda), a chamada real vai para o adjust_numbers — único ponto da late save que ainda devolve chaves ao framework — e o save fica vazio, porque a update task já foi registrada e o commit do framework a dispara.

O consumidor da Web API recebe o número gerado no POST?

Sim, desde que o adjust_numbers preencha o mapped com a chave definitiva. O framework troca a chave preliminar (%pid) pelo número real, faz a releitura via seu método read após o commit e responde o 201 já com o VBELN. Se o mapped ficar vazio, o consumidor recebe a criação sem saber qual documento nasceu — e aí a API perde metade da utilidade.

Por que chamar a BAPI com TESTRUN no check_before_save se ela vai rodar de novo depois?

Porque são responsabilidades diferentes. O TESTRUN roda na early save, onde a transação ainda pode ser rejeitada com mensagem estruturada — é a sua chance de devolver um 4xx limpo. A chamada real roda na late save, onde erro de consumidor não pode mais existir; o failed do cl_abap_behavior_saver_failed ali é rede de segurança contra condições de corrida, não fluxo normal de validação.

Posso converter um BO unmanaged em managed depois?

Sim, mas é uma reescrita: o managed espera persistência gerenciada pelo framework. O caminho natural é migrar quando a lógica legada for finalmente substituída por tabela e regras próprias do RAP — e as variantes intermediárias (additional/unmanaged save) servem de degrau.

Preciso implementar todos os métodos da save sequence?

Não. Você só redefine o que precisa. Para reuso de FMs, normalmente bastam save e cleanup; adjust_numbers entra com late numbering, e finalize/check_before_save conforme houver determinações e validações finais.

Por que minha validação no save gera runtime error em vez de mensagem?

Porque a late save phase não aceita falha controlada — o parâmetro failed nem existe ali (a menos que você herde de cl_abap_behavior_saver_failed, e mesmo assim o resultado é abortar a transação, não devolver o controle ao usuário). Validação que pode rejeitar a transação pertence à interaction phase ou, no limite, ao check_before_save (early save).

Dá para usar unmanaged no ABAP Cloud (S/4HANA Cloud)?

Sim, desde que tudo que você chama seja liberado para ABAP Cloud. BAPIs clássicas não liberadas precisam de um wrapper liberado antes de entrar no behavior pool. No S/4HANA Private Cloud (tier clássico) e on-premise, a chamada direta da BAPI funciona — mas o wrapper continua sendo boa prática de Clean Core.

Preciso de draft no unmanaged?

Draft é opcional e exige implementação adicional (tabela de draft e mapeamento). Para Web APIs — como a facade deste post — você dispensa sem dó: sistema externo não "rascunha" ordem de venda. Se draft for requisito forte, é mais um ponto a favor de avaliar o managed with unmanaged save, onde o draft vem do framework.

O unmanaged serve para Web API também?

Perfeitamente — a "casca" é a mesma. A projection + service definition + binding Web API expõem o BO unmanaged como endpoint OData V4 igual a um managed; o consumidor não vê diferença nenhuma. A segunda metade deste post é exatamente isso, de ponta a ponta.

Conclusão

O unmanaged scenario é a ponte entre o mundo clássico e o ABAP Cloud: ele deixa você expor lógica que já existe — BAPIs, FMs e tabelas com regras próprias — através de um serviço RAP moderno e Clean Core. O custo é escrever mais código (handler + saver) e dominar as duas fases da transação, com o ponto de não retorno da save sequence sempre no radar; o benefício é controle total e reaproveitamento do que já está testado.

E a facade de ordem de venda mostra o padrão completo em campo: BAPI validada com TESTRUN na early save, chamada real no adjust_numbers capturando o número gerado, save vazio, commit por conta do framework — e um POST OData V4 que responde 201 com o VBELN real. A régua de decisão ficou mais fina do que "managed ou unmanaged": percorra o espectro — managed, additional save, unmanaged save, unmanaged — e pare no primeiro sabor que resolve. Comece pelo managed sempre que puder; traga o unmanaged quando houver algo que valha a pena reutilizar.

Referências oficiais

  • Business Object Implementation Types — SAP Help

  • Save Options (managed, additional, unmanaged save) — SAP Help

  • Using BAPIs in RAP — SAP Community (blog oficial do time RAP)

  • Exposing BAPI as OData API using RAP Facade — SAP Community

  • RAP - SavingOptions — ABAP Keyword Documentation

  • save_modified, RAP Saver Method — ABAP Keyword Documentation

  • finalize, RAP Saver Method — ABAP Keyword Documentation

  • RAP - late numbering — ABAP Keyword Documentation

  • ABAP Cheat Sheet — EML / ABAP for RAP — SAP-samples

  • openSAP — Building Apps with RAP — SAP-samples

TagsADTBDEFBAPIFiori ElementsEclipseEMLDesenvolvedorClean CoreBO Interface
Avalie este conteúdo
para avaliar
SAPiente
@sapiente

Blog sobre SAP, ABAP, BTP, Fiori e tudo que envolve o ecossistema SAP.

Ver perfil →
FacebookInstagramYouTubeTwitter

Comentários 0

Entre na conversa

Faça login para deixar seu comentário neste artigo.

Ainda sem comentários

Seja o primeiro a comentar este artigo.