Quais São os Tipos de Function no RAP e Quando Usar Cada Um?
A metade esquecida das operações não-standard
Todo mundo fala de actions. Mas o RAP tem uma segunda operação não-standard, gêmea da action e sistematicamente ignorada: a function. E ignorá-la tem um custo concreto — projeto afora se vê action sendo usada para consultar coisa: "simular o imposto", "verificar o limite de crédito", "calcular a disponibilidade". Tudo isso trava instância sem necessidade, suja o contrato OData com operação de escrita que não escreve e, no fundo, é function vestida de action.
A definição oficial é cirúrgica: uma RAP function é uma operação implementada pelo desenvolvedor que retorna informação sem nenhum efeito colateral. Ela calcula e lê — sem emitir locks e sem modificar dados. A implementação vive num handler FOR READ do behavior pool (a action vive no FOR MODIFY — essa é a divisa entre os dois mundos).
O contrato é fiscalizado em runtime, não em sintaxe. Se você tentar modificar dados dentro da implementação de uma function, não há erro de sintaxe nenhum — o código ativa normalmente. O estouro vem depois: runtime error quando um consumidor acessa a function. É o tipo de bug que passa no desenvolvimento e explode no teste integrado.
A anatomia da declaração
[internal] [static] [repeatable] function NomeDaFunction
[external 'NomeExterno']
[parameter EntidadeDeEntrada]
result [cardinalidade] { $self | entity Entidade | EntidadeAbstrata };Quase idêntica à action — com uma diferença que muda tudo:
Aspecto | Action | Function |
|---|---|---|
Handler |
|
|
Modifica estado? | Sim (é o propósito) | Nunca (runtime error se tentar) |
Lock na instância | Sim, por padrão | Não — nunca trava |
Output ( | Opcional | Obrigatório |
Precisa de COMMIT? | Sim (via COMMIT ENTITIES) | Não — é leitura pura |
Consumo EML |
|
|
O result obrigatório faz sentido: uma function que não devolve nada não serve para nada — ela não pode ter deixado rastro nenhum no sistema. E as opções de tipo e cardinalidade são as mesmas da action: $self, outra entidade ou uma abstract entity, com [1], [0..1], [1..*] ou [0..*].
1. Instance function: perguntar algo a uma instância
O default: a function é vinculada a instâncias e responde algo sobre elas.
Casos de uso:
Simular imposto/preço: "quanto ficaria este pedido com o imposto recalculado?" — devolve o resultado sem gravar nada.
Verificar elegibilidade: "este contrato pode ser renovado?" — devolve um veredito estruturado (pode/não pode + motivos) sem tocar no contrato.
Consolidar detalhes calculados: devolver totais, saldos e projeções de uma instância que não existem persistidos.
@EndUserText.label: 'Resultado da simulação'
define abstract entity ZD_Tax_Simulation
{
NetAmount : abap.dec(15,2);
TaxAmount : abap.dec(15,2);
GrossAmount : abap.dec(15,2);
}define behavior for ZR_SalesOrder alias Order
{
...
function simulateTax result [1] ZD_Tax_Simulation;
}" handler FOR READ — repare: nada de FOR MODIFY aqui
METHODS simulateTax FOR READ
IMPORTING keys FOR FUNCTION Order~simulateTax RESULT result.
METHOD simulateTax.
READ ENTITIES OF ZR_SalesOrder IN LOCAL MODE
ENTITY Order FIELDS ( NetAmount TaxCode ) WITH CORRESPONDING #( keys )
RESULT DATA(orders).
result = VALUE #( FOR order IN orders
( %tky = order-%tky
%param = VALUE #(
NetAmount = order-NetAmount
TaxAmount = calc_tax( amount = order-NetAmount tax = order-TaxCode )
GrossAmount = order-NetAmount
+ calc_tax( amount = order-NetAmount tax = order-TaxCode ) ) ) ).
ENDMETHOD.E o consumo — repare que é READ ENTITIES, e sem COMMIT:
READ ENTITIES OF ZR_SalesOrder
ENTITY Order
EXECUTE simulateTax FROM VALUE #( ( OrderUUID = lv_uuid ) )
RESULT DATA(simulation)
FAILED DATA(failed) REPORTED DATA(reported).
" pronto. sem COMMIT ENTITIES — não há o que persistir.
DATA(gross) = simulation[ 1 ]-%param-GrossAmount.Function só se chama por EML? Não — ela também vai para o OData
Dúvida frequente: "function é só consumo interno via READ ENTITIES ... EXECUTE?" Não. Exatamente como a action, a function é exposta no serviço OData — basta um use function na projection BDEF. A diferença de fundo está no verbo HTTP, e ela decorre da natureza dos dois artefatos:
Action | Function | |
|---|---|---|
Efeito colateral | Sim → POST | Não → GET |
Parâmetros | No corpo (body JSON) | Inline na URL, entre parênteses |
Parênteses sem parâmetro | — |
|
Composição ($filter/$select no retorno) | Não | Sim (é leitura) |
Na projection, expor é uma linha:
define behavior for ZC_SalesOrder alias Order
{
use function simulateTax;
use function calculateOpenTotal;
}E a chamada HTTP num serviço OData V4 do RAP usa o nome qualificado da operação (o alias de schema SAP__self num Web API) anexado à URL da entidade — GET, com os parâmetros da function inline:
" static function (sem instância) — parênteses vazios obrigatórios:
GET .../Order/SAP__self.calculateOpenTotal()
" instance function com parâmetro — chave da instância + parâmetro da function:
GET .../Order(OrderUUID=...)/SAP__self.simulateTax(TaxCode='ICMS')Contraste direto com o consumo via EML acima: mesma operação, dois consumidores. Internamente, READ ENTITIES ... EXECUTE; externamente, um GET. O post de Web API detalha o SAP__self e a construção completa da URL.
E o botão no Fiori? Function também pode virar botão (o Fiori Elements chama function import via OData) — mas com uma ressalva que decide a escolha: function não suporta side effects nem state messages. Ou seja, ela não dispara o refresh automático de campos na tela que o result [1] $self de uma action provoca. Botão que precisa refletir uma mudança na UI → action (porque houve mudança de estado); botão que só exibe um valor calculado → function serve, tratando o retorno manualmente.
2. Static function: perguntar algo ao conjunto
Com static, a function se desvincula de instâncias e responde sobre a entidade como um todo. O exemplo da documentação oficial é exatamente esse: uma calculateTotal que soma os preços de todas as instâncias com status 'O' e devolve o total.
Casos de uso:
KPIs para dashboard/cockpit: total em aberto, contagem por status, valor médio — números do conjunto, calculados na hora.
Verificações de contexto: "existe algum documento pendente que bloqueia o fechamento do período?"
Consulta parametrizada sem instância: "qual a disponibilidade do material X no centro Y?" — a resposta não pertence a nenhuma instância do BO.
static function calculateOpenTotal result [1] ZD_Total_Result;METHOD calculateOpenTotal.
SELECT FROM zsalesorder
FIELDS SUM( net_amount ) AS total, COUNT(*) AS open_count
WHERE overall_status = 'O'
INTO @DATA(ls_totals).
" static: uma linha de resultado, endereçada por %cid
result = VALUE #( ( %cid = keys[ 1 ]-%cid
%param = VALUE #( TotalAmount = ls_totals-total
OpenCount = ls_totals-open_count ) ) ).
ENDMETHOD.Detalhe da documentação: o output da function pode até ser tipado com um elemento DDIC simples — o exemplo oficial faz isso —, mas a nota logo em seguida é clara: a recomendação é usar apenas CDS abstract entities como parâmetros BDEF. Contrato tipado, versionável, com labels para a UI.
A chamada não recebe chave de instância — usa %cid no EML, e parênteses vazios no GET:
" via EML — %cid em vez de chave, sem COMMIT:
READ ENTITIES OF ZR_SalesOrder
ENTITY Order
EXECUTE calculateOpenTotal FROM VALUE #( ( %cid = 'c1' ) )
RESULT DATA(totals) FAILED DATA(failed) REPORTED DATA(reported).
DATA(open) = totals[ 1 ]-%param-OpenCount.
" via OData Web API — GET, parênteses obrigatórios mesmo sem parâmetro:
" GET .../Order/SAP__self.calculateOpenTotal()3. Function com parâmetro de entrada
Mesma mecânica da action: abstract entity após parameter, valores acessíveis via %param. O exemplo oficial é uma calculateDiscount que recebe o percentual e devolve o preço reduzido — sem gravar o desconto, só respondendo "ficaria assim":
function calculateDiscount parameter ZD_Discount_Pct result [1] ZD_Price_Result;A chamada passa os valores no %param (EML) ou inline na URL (GET):
" via EML — o parâmetro vai no %param da linha:
READ ENTITIES OF ZR_SalesOrder
ENTITY Order
EXECUTE calculateDiscount
FROM VALUE #( ( OrderUUID = lv_uuid
%param = VALUE #( discount_percent = 15 ) ) )
RESULT DATA(prices) FAILED DATA(failed) REPORTED DATA(reported).
DATA(net) = prices[ 1 ]-%param-NetAfterDiscount.
" via OData Web API — parâmetro inline na URL:
" GET .../Order(OrderUUID=...)/SAP__self.calculateDiscount(discount_percent=15)Caso de uso matador: o padrão simular → confirmar. A function calculateDiscount mostra o resultado ao usuário; se ele confirmar, aí sim a action applyDiscount grava. Duas operações, dois contratos, cada uma no handler certo — e a simulação nunca trava a instância de ninguém.
Parâmetro flat vs. deep (e a pegadinha do OData V2)
Parâmetros de entrada podem ser flat (estrutura plana via abstract entity — o caso comum) ou deep (hierarquia de abstract entities amarrada por um abstract behavior definition com with hierarchy, usando a palavra-chave deep). Deep vale para cenários em que a pergunta em si é estruturada — "simule este documento com estes itens hipotéticos".
Pegadinha de exposição: OData V2 só aceita operações não-standard com parâmetros flat. Se você incluir uma function (ou action) com parâmetro deep num serviço V2, o service binding acusa erro. Parâmetro deep pede OData V4 — mais um item para a conta na hora de escolher a versão do serviço.
4. Repeatable function: a mesma pergunta, duas vezes na mesma requisição
Igualzinho às actions: por padrão, uma function não pode ser executada mais de uma vez na mesma instância dentro da mesma requisição EML/OData — a tentativa gera violação de contrato (com os contract checks ligados). A adição repeatable libera:
repeatable function simulatePrice parameter ZD_Scenario result [1] ZD_Price_Result;Caso de uso: simulação comparativa num único batch — o consumidor manda a mesma instância com três cenários de desconto diferentes e recebe as três projeções de uma vez:
READ ENTITIES OF ZR_SalesOrder
ENTITY Order
EXECUTE simulatePrice FROM VALUE #(
( OrderUUID = lv_uuid %param-discount_percent = 5 )
( OrderUUID = lv_uuid %param-discount_percent = 10 ) " mesma instância
( OrderUUID = lv_uuid %param-discount_percent = 15 ) ) " de novo!
RESULT DATA(scenarios).5. Internal function: cálculo que só o BO conhece
A adição internal restringe o acesso à implementação do próprio BO — a function não aparece no serviço nem pode ser executada por consumidores externos.
Casos de uso: a mesma lógica de cálculo consumida por uma action, uma determination e uma validation (um único ponto de verdade para "calcular o total"); regras de elegibilidade internas que não são contrato público. É o par de leitura da internal action:
internal function checkEligibility result [1] ZD_Eligibility;
" chamada de dentro do behavior pool (via EML IN LOCAL MODE):
READ ENTITIES OF ZR_Contract IN LOCAL MODE
ENTITY Contract
EXECUTE checkEligibility FROM CORRESPONDING #( keys )
RESULT DATA(eligibility).
" de FORA do BO não existe chamada: não há GET no OData,
" e um READ ENTITIES externo nem compila — é internal.6. Key function: buscar por chave alternativa
O tipo mais recente e mais desconhecido. Uma key function recebe os valores de uma chave alternativa (definida como pure key na BDEF) e devolve as instâncias correspondentes — o "buscar pelo número do documento quando a chave técnica é UUID":
define behavior for ZR_Contract alias Contract
{
...
// chave alternativa sobre o número externo do contrato
key MyAltKey : alternative ( ContractNumber );
// key function referenciando a chave alternativa
key function GetByContractNumber for MyAltKey;
}E aqui vem o presente do framework:Em BO managed, a key function não precisa de implementação nenhuma — o RAP BO provider resolve sozinho a busca pela chave alternativa. Só no unmanaged você implementa manualmente, no handler FOR READ ... FUNCTION. Se especificar output explícito, a sintaxe é result selective [cardinalidade] $self.
Casos de uso: APIs em que o consumidor externo só conhece o identificador de negócio (número do pedido, código do material, chave legada) e não o UUID interno; e — destaque da própria SAP — cenários de associação cross-BO, onde um BO precisa localizar instâncias de outro pela chave semântica. Na projection, reusa-se com use function.
E a chamada é o ponto alto: o consumidor passa a chave alternativa, não o UUID — é o "buscar pelo número do contrato" que a API precisa:
" via EML — informa o ContractNumber (chave de negócio), recebe as instâncias:
READ ENTITIES OF ZR_Contract
ENTITY Contract
EXECUTE GetByContractNumber
FROM VALUE #( ( %key-ContractNumber = '4500001234' ) )
RESULT DATA(found) FAILED DATA(failed) REPORTED DATA(reported).
" 'found' traz a instância completa — inclusive o UUID técnico, agora conhecido:
DATA(uuid) = found[ 1 ]-ContractUUID.
" via OData Web API — GET com a chave alternativa inline:
" GET .../Contract/SAP__self.GetByContractNumber(ContractNumber='4500001234')7. Default function: o preenchedor de diálogos
A default function é uma function com emprego fixo: acoplada a uma action (ou factory/save action), ela fornece os valores default dos parâmetros no diálogo do Fiori Elements — o popup já abre preenchido com sugestões inteligentes:
action createFollowUp parameter ZD_FollowUp_Param result [1] $self
{ default function getFollowUpDefaults; }" implementação: FOR READ, devolvendo os defaults em %param
METHOD getFollowUpDefaults.
READ ENTITIES OF ZR_Contract IN LOCAL MODE
ENTITY Contract FIELDS ( PartnerID Buyer ) WITH CORRESPONDING #( keys )
RESULT DATA(contracts).
result = VALUE #( FOR contract IN contracts
( %tky = contract-%tky
%param = VALUE #(
Description = |Follow-up de { contract-ContractID }|
Buyer = COND #( WHEN contract-Buyer IS NOT INITIAL
THEN contract-Buyer
ELSE cl_abap_context_info=>get_user_technical_name( ) )
ValidFrom = cl_abap_context_info=>get_system_date( ) ) ) ).
ENDMETHOD.Casos de uso: sugerir o motivo mais frequente num popup de rejeição, derivar datas de validade e responsável a partir do contexto, pré-preencher o diálogo do "criar a partir de" com dados da origem. É lógica de negócio de verdade — condições, consultas, derivações — a serviço de UX: menos digitação, menos erro, mais consistência. O tema oficial na documentação se chama Operation Defaulting.
Esta é a exceção que não se chama explicitamente: você não escreve um EXECUTE getFollowUpDefaults nem um GET para ela. Quem a invoca é o framework, automaticamente, no momento em que o Fiori Elements vai abrir o diálogo da action createFollowUp — o retorno preenche os campos do popup. A default function é acoplada à action pela BDEF e disparada pelo ciclo da UI; o "consumo" dela é o usuário clicando no botão que abre o diálogo.
A tabela de decisão: function ou action? E qual function?
Requisito | Use |
|---|---|
Calcular/simular/consultar sobre instâncias, sem gravar | Instance function |
Número/veredito sobre o conjunto (KPI, verificação de contexto) | Static function |
Simulação parametrizada ("e se fosse X%?") | Function com parameter |
Vários cenários da mesma instância num só batch | Repeatable function |
Cálculo reutilizado internamente, invisível para fora | Internal function |
Localizar instância por chave de negócio/legada | Key function (managed: de graça!) |
Pré-preencher parâmetros de action no diálogo Fiori | Default function |
Simular e depois gravar | Function (simula) + action (grava) — o padrão simular → confirmar |
Qualquer coisa que altere estado | Action — não é function! |
Boas práticas consolidadas
Se não grava, é function. Cada "action de consulta" no seu BO é um lock desnecessário e um contrato OData mentiroso.
Nunca modifique dentro de uma function — o sintaxe deixa passar, o runtime não. E o erro só aparece no consumo.
Output sempre via abstract entity, mesmo quando um elemento DDIC compilaria — é a recomendação explícita da documentação.
Sem COMMIT no consumo: function via
READ ENTITIES ... EXECUTEnão persiste nada; umCOMMIT ENTITIESali é ruído que confunde o leitor do código.Padrão simular → confirmar: function para mostrar, action para efetivar. Dois artefatos, responsabilidades limpas.
Parâmetro deep? OData V4. O service binding V2 rejeita operações com parâmetro deep.
Chave alternativa em managed = key function de graça. Antes de escrever um SELECT de conversão de chave, veja se uma pure key + key function não resolve sem uma linha de implementação.
Default function em toda action com parâmetro voltada a usuário — popup vazio é oportunidade de UX desperdiçada.
Perguntas frequentes
Function precisa de COMMIT ENTITIES?
Não. Function é executada via READ ENTITIES ... EXECUTE e não altera nada — não há o que commitar. Se o seu código tem COMMIT depois de uma function, ou o COMMIT é inútil, ou a "function" está modificando dados (e vai estourar em runtime).
O que acontece se eu modificar dados dentro de uma function?
Nada em tempo de ativação — não há erro de sintaxe. Mas ocorre runtime error quando um consumidor acessa a function. O contrato "sem efeitos colaterais" é fiscalizado na execução.
Por que o result é obrigatório em function e opcional em action?
Porque a action pode se justificar só pelo efeito (mudou o status, pronto); a function não tem efeito nenhum — se ela também não devolvesse nada, seria uma operação vazia. Por isso o output parameter é mandatório.
Function aparece como botão no Fiori Elements?
Pode, sim — a function é exposta no OData e o Fiori Elements consegue renderizar um controle que a chama (via function import). O que decide a escolha não é "dá ou não dá botão", e sim o comportamento: function não suporta side effects nem state messages, então ela não dispara o refresh automático de campos na tela após o clique — isso é privilégio da action com result [1] $self. Regra prática: se o botão precisa que a UI reflita uma mudança, é action (houve mudança de estado); se só exibe um valor calculado num popup, function serve. E, para um valor apenas exibido inline, avalie antes um campo virtual/calculado na CDS, que dispensa a operação.
Key function funciona em BO unmanaged?
Funciona, mas aí a implementação é sua, no handler FOR READ ... FUNCTION. O "de graça" (implementação pelo próprio RAP BO provider) é exclusividade do managed.
Posso usar $self como parâmetro de entrada de uma function?
Só em static functions (ou actions) — e em BDEF strict mode nem isso. Em instance function, compila sem warning e gera runtime error no consumo. Não confunda com o $self do result, que é o uso comum e seguro.
Conclusão
Function é a resposta do RAP para tudo que é pergunta: simulação, cálculo, verificação, busca por chave de negócio, defaults de diálogo — sem lock, sem commit, sem efeito colateral, com o contrato fiscalizado pelo próprio framework. O critério de escolha cabe numa frase: se altera estado, action; se informa, function — e dentro das functions, a tabela de decisão acima resolve o resto. Da próxima vez que aparecer uma "action de simulação" no code review, você já sabe o que apontar.
Referências oficiais
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.