Blog SAPienteSAPiente
RAP

Quais São os Tipos de Function no RAP e Quando Usar Cada Um?

SAPiente3 de jul. de 2026· 12 min read

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

FOR MODIFY

FOR READ

Modifica estado?

Sim (é o propósito)

Nunca (runtime error se tentar)

Lock na instância

Sim, por padrão

Não — nunca trava

Output (result)

Opcional

Obrigatório

Precisa de COMMIT?

Sim (via COMMIT ENTITIES)

Não — é leitura pura

Consumo EML

MODIFY ENTITIES ... EXECUTE

READ ENTITIES ... EXECUTE

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

() obrigatório

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

  1. Se não grava, é function. Cada "action de consulta" no seu BO é um lock desnecessário e um contrato OData mentiroso.

  2. Nunca modifique dentro de uma function — o sintaxe deixa passar, o runtime não. E o erro só aparece no consumo.

  3. Output sempre via abstract entity, mesmo quando um elemento DDIC compilaria — é a recomendação explícita da documentação.

  4. Sem COMMIT no consumo: function via READ ENTITIES ... EXECUTE não persiste nada; um COMMIT ENTITIES ali é ruído que confunde o leitor do código.

  5. Padrão simular → confirmar: function para mostrar, action para efetivar. Dois artefatos, responsabilidades limpas.

  6. Parâmetro deep? OData V4. O service binding V2 rejeita operações com parâmetro deep.

  7. 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.

  8. 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

TagsADTBDEFClean CoreFiori ElementsEMLEclipseDesenvolvedor
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.