RAP Unmanaged Scenario: como reutilizar BAPIs e lógica existente
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 | Gravação padrão serve, mas falta algo no commit: change documents, application log, disparar evento |
managed with unmanaged save | Framework | Você grava no | 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_modifiedna saver class — e, nelas, o métodosaveclássico não pode ser usado.No unmanaged save, a BDEF não pode especificar
persistent table— afinal, quem grava é você.O
save_modifiedrecebe as instâncias a criar/alterar/excluir em parâmetros tipados com os BDEF derived typesTYPE REQUEST FOR CHANGEeTYPE REQUEST FOR DELETE; por padrão só chegam chaves + campos alterados — a adiçãowith full dataentrega 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 |
|---|---|
| Operações de modificação (buffer!) |
| Leitura de instâncias — alimenta o OData e a verificação de ETag |
| Bloqueio contra acesso concorrente (reuse o ENQUEUE legado) |
| Autorização por instância |
| Feature control dinâmico (habilitar/desabilitar operações e actions) |
| 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) |
| Últimos cálculos e determinações antes de persistir — é o primeiro método da fase |
| Verificação final; se falhar, a transação inteira é rejeitada (rollback) | |
Late save (ponto de não retorno) |
| Late numbering: as chaves definitivas são atribuídas aqui |
| Grava de fato o buffer no banco — aqui entra a BAPI/FM de persistência | |
Limpeza |
| Limpa o buffer após o save (ou após rollback) |
| 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 osave_modifieddas variantes managed) nem recebem o parâmetrofailed— 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 WORKno 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 |
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 |
| 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) |
| 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 |
| 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_saveprotege o consumidor: erro de negócio (cliente bloqueado, material inexistente, org de vendas inválida) vira HTTP 4xx com mensagem estruturada. A chamada real noadjust_numbersassume que já está tudo validado — se ainda assim falhar (condição de corrida, por exemplo), ofaileddo 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_UeZC_SalesOrderItem_U+ projection BDEF comuse create;euse association _Item { create; }— sem update, sem delete, espelhando a facade.Service definition expondo as duas projections (dica: use
provider contract entity_idsquando disponível na sua release, e nomeie pensando no consumidor —SalesOrder, nãoZC_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_ADMINe 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_COMMITdentro do BO = dump.Validação que rejeita = early save (ou interaction phase). Depois do
check_before_savenã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
failednoadjust_numberse evita que um erro tardio da BAPI vire short dump.Use o mapping ... control:
CORRESPONDING ... MAPPING FROM ENTITY USING CONTROLelimina 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
Comentários 0
Ainda sem comentários
Seja o primeiro a comentar este artigo.
Entre na conversa
Faça login para deixar seu comentário neste artigo.