Change Documents no RAP
"Quem mudou esse campo, e quando?"
Cedo ou tarde alguém te faz essa pergunta. Auditoria, um cliente desconfiado, uma área de compliance. E você precisa do valor antigo, do valor novo, do usuário e da hora exata. No SAP isso tem nome: change documents.
Não é a única forma de guardar histórico. Dá pra usar Application Log, dá pra gravar uma tabela própria. Mas o change document é o padrão que o SAP entende, que tem CDS e telas standard pra ler, e que em vários setores é exigência de compliance, não escolha.
Prefere log de processo? A alternativa via Application Log usa a família cl_bali_* (criar o log com cl_bali_log, montar a mensagem com cl_bali_message_setter, persistir e amarrar ao processo). Cobri ela a fundo no post dedicado ao Application Log no ABAP Cloud — e ela também aparece em ação no post de Application Jobs.
O que me fez gostar dele no RAP foi não escrever log nenhum. No clássico você criava o objeto na SCDO, gerava uma função e chamava CHANGEDOCUMENT_OPEN, ..._SINGLE_CASE campo a campo, ..._CLOSE. Espalhado pela aplicação inteira. No RAP você adiciona umas palavras-chave no behavior definition, cria o objeto com um quick fix, e o framework grava em CDHDR/CDPOS a cada save.
Esse post é a implementação completa, do jeito que eu faria num app de Pedido de Venda (Sales Order, cabeçalho + itens): BDEF, criação do objeto, leitura por código e exibição do histórico no Fiori (nos dois cenários: sistema novo e sistema antigo).
O essencial
Trilha de auditoria campo a campo (valor antigo, novo, usuário, data) gravada em
CDHDR+CDPOS.No RAP é declarativo: nenhuma linha de log na classe de comportamento.
Pré-requisitos:
strict ( 2 ),persistent table(só cenário managed) e campos marcados como relevantes.Três peças: as palavras-chave no BDEF, o Change Document Object (quick fix no ADT) e a marcação dos campos.
Exibição no Fiori: via
I_ChangeDocument_2(sistema novo) ou CDS próprio sobreCDHDR/CDPOS(sistema antigo).
Como o change document chegou até o RAP?
O recurso vem evoluindo há tempos. Começou com a abordagem clássica de GUI usando function modules. Depois ganhou a opção de criar o Change Document Object no ADT, como qualquer CDS. Agora, como isso também é um tipo de comportamento das entidades, basta adicionar palavras-chave na behavior definition e criar o objeto por quick fix. Bem mais curto.
Passo 1: declarar os change documents na behavior definition
Aqui mora quase tudo. Abra a behavior definition (no exemplo, ZI_SALES_ORDER) e adicione as palavras-chave de change document. Note a changedocuments master na raiz, a changedocuments dependent by _SalesOrder no item, e o controle por operação dentro de cada bloco.
managed implementation in class ZBP_I_SALES_ORDER unique;
strict ( 2 );
with draft;
define behavior for ZI_SALES_ORDER
persistent table zso_header
draft table zso_header_d
lock master total etag LastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
changedocuments master ( ZCHD_SO ) <---
{
changedocuments( create : key, update : data, delete : none ); <---
create ( precheck );
update;
delete ( features : instance );
field ( readonly : update, mandatory : create ) SalesOrderId;
// ... actions, validations e draft actions do app ...
association _Item { create; with draft; }
mapping for zso_header { /* ... */ }
}
define behavior for ZI_SALES_ORDER_ITEM
persistent table zso_item
draft table zso_item_d
lock dependent by _SalesOrder
authorization dependent by _SalesOrder
changedocuments dependent by _SalesOrder <---
{
changedocuments( create : data, delete : data ); <---
update;
delete;
field ( readonly : update, mandatory : create ) ItemId;
field ( readonly ) SalesOrderId, Description;
association _SalesOrder { with draft; }
mapping for zso_item { /* ... */ }
}Vou destrinchar cada pedaço, porque é aqui que dá nó na cabeça.
changedocuments master ( ZCHD_SO )
Declara a entidade de cabeçalho como a master dos change documents. Ela é dona do change document object ZCHD_SO, que é a chave sob a qual todos os registros caem na CDHDR/CDPOS. Um RAP BO tem no máximo um change documents master. O normal é ser a raiz, como aqui.
changedocuments dependent by _SalesOrder
Declara o item como dependente. Ele não tem objeto próprio: usa o da master. O by _SalesOrder diz qual associação seguir pra chegar até ela. Master e dependentes precisam formar uma árvore na hierarquia de composição, o que no nosso caso já acontece.
O trio create / update / delete
É onde você decide o que logar em cada operação. São três valores possíveis:
Valor | O que registra |
|---|---|
| Nada. A operação não gera change document. |
| Só a chave do registro (que ele foi criado ou excluído), sem detalhar campo. |
| Todos os campos relevantes que mudaram, com valor antigo e novo. |
No exemplo, o cabeçalho usa create : key (na criação os campos ainda estão esparsos, não vale logar tudo), update : data (o mais importante pra auditoria) e delete : none. Já o item usa create : data e delete : data, porque a linha é o dado de verdade: saber exatamente o que tinha num item quando foi criado ou removido é o que interessa numa auditoria do pedido.
E os opcionais asynchronous e utc?
Dois acréscimos que não usei acima, mas que valem conhecer:
asynchronous— pra cenário de alto volume, quando gravar muito change document começa a pesar no tempo de resposta. O dado ainda é determinado de forma síncrona, mas a gravação naCDHDR/CDPOSvai pro framework de processamento em background.utc— por padrão o timestamp sai no fuso do sistema. Comutcele é gravado em UTC, o que ajuda quando o sistema atende fusos diferentes e você quer carimbos consistentes.
Quais os pré-requisitos pra isso funcionar?
Três coisas precisam estar no lugar, senão o ativador reclama ou o log simplesmente não aparece.
1. Strict mode 2. O change document nativo exige strict ( 2 ) na behavior definition. Sem isso, as palavras-chave nem existem.
2. Persistent table. Precisa de uma persistent table declarada. Isso tem uma consequência direta: não dá pra usar change document em cenário unmanaged (pelo menos por enquanto). É recurso de BO managed.
3. Campos marcados como relevantes. O framework não loga campo à toa. Pra um campo entrar no change document, ele precisa ou referenciar um data element com o flag de change document ligado no DDIC, ou usar um CDS simple type com a annotation @AbapCatalog.typeSpec.changeDocumentRelevant: true.
No jeito ABAP Cloud, você cria o simple type marcado:
@EndUserText.label: 'Status do Pedido'
@AbapCatalog.typeSpec.changeDocumentRelevant: true
define type ZE_SO_STATUS : abap.char(1).E faz o cast do campo na CDS:
define root view entity ZI_SALES_ORDER
as select from zso_header
{
...
// campo marcado para change documents
cast( status as ZE_SO_STATUS ) as Status,
...
}Campos que usam data elements standard (como Currency ou SalesOrg) já costumam vir marcados, por isso são rastreados de fábrica. Quem não está marcado simplesmente não aparece no histórico, mesmo que mude.
Não marque tudo. Cada campo relevante vira linha na CDPOS. Marcar o objeto inteiro incha a tabela e enche o histórico de ruído. Marque o que alguém vai querer auditar de verdade (valores, status, datas de negócio) e deixe os campos técnicos de fora.
Passo 2: criar o Change Document Object
Com o BDEF salvo, o ADT mostra um quick fix ao lado da linha changedocuments master ( ZCHD_SO ). Clique nele e informe o nome do objeto. A convenção é ZCHD_XXX_SO, com XXX sendo as iniciais do seu nome.

📸 PRINT: o quick fix (Ctrl+1) sobre a linha changedocuments master e o diálogo pedindo o nome do objeto.
Depois de criar, o objeto abre na tela de edição. Clique em Add e informe a tabela do cabeçalho, zso_header. Deixe os checkboxes desmarcados por ora.
O que cada checkbox faz?
Já que você está nessa tela, vale entender os campos. São opções clássicas de change document object:
Log Multiple Changes — marca se o dado alterado vem numa tabela interna (multiple case) em vez de uma work area (single case). No RAP o framework cuida disso.
Log Field Values for Deletions — gera entrada separada por campo na exclusão. Desmarcado, escreve uma única entrada dizendo que o registro foi apagado, sem detalhe de campo.
Log Initial Field Values for Deletions — registra o campo mesmo que estivesse vazio na hora da exclusão. Cuidado: gera muito change document. Só ligue se precisar mesmo.
Log Field Values for Creations — a mesma ideia, na inserção: entrada por campo em vez de uma só.
Log Initial Field Values for Creations — registra valores iniciais na inserção. Mesma ressalva de volume.
Reference Table — se os campos de moeda/unidade estão numa tabela de referência, você informa o nome dela aqui. No nosso caso não precisa, a moeda é definida na própria tabela.
O detalhe: no RAP esses checkboxes são irrelevantes. O que importa de verdade é só o nome do change document object, que vira campo-chave no resultado. Todo o comportamento de log é controlado pela sintaxe do BDEF (aquele create / update / delete), não pelos checkboxes da tela.
Como ler os change documents por código?
Feito o passo acima, o dado já é gravado em CDHDR e CDPOS a cada save. Ler é a parte que tem pegadinha.
Em BTP trial ou S/4HANA Public Edition, você lê pela classe cl_chdo_read_tools=>changedocument_read:
TRY.
cl_chdo_read_tools=>changedocument_read(
EXPORTING
i_objectclass = 'ZCHD_SO'
i_date_of_change = sy-datum
IMPORTING
et_cdredadd_tab = FINAL(result) ).
out->write( result ).
CATCH cx_chdo_read_error.
" tratar exceção
ENDTRY.Só que numa trial isso estoura por falta de autorização. Quando você cria e ativa o change document object, o SAP gera uma classe com o método WRITE. Essa mesma classe implementa a interface IF_CHDO_ENHANCEMENTS, e o changedocument_read consulta o método if_chdo_enhancements~authority_check dela pra decidir se você pode ler.
A saída na trial é implementar esse método pra liberar a leitura. Na classe gerada (ZCL_ZCHD_SO_CHDO), redefina o authority_check. O WRITE continua o gerado, que por baixo dos panos chama o changedocument_open/single_case/close:
CLASS zcl_zchd_so_chdo IMPLEMENTATION.
METHOD if_chdo_enhancements~authority_check.
rv_is_authorized = abap_true.
ENDMETHOD.
METHOD write.
" método gerado automaticamente — por baixo dos panos:
cl_chdo_write_tools=>changedocument_open( ... ).
cl_chdo_write_tools=>changedocument_single_case( ... ).
cl_chdo_write_tools=>changedocument_close( IMPORTING changenumber = changenumber ).
ENDMETHOD.
ENDCLASS.O rv_is_authorized = abap_true é gambiarra de trial. Ele só existe porque a trial não provisiona os objetos de autorização. Num S/4HANA produtivo as autorizações vêm pelas roles, e o método faz a verificação de verdade. Não suba isso fixo em produção.
Com isso você consegue ver todas as modificações assim como na CDHDR.
Como mostrar o histórico no Fiori?
Gravar e ler por código é metade. A outra metade é o usuário ver as alterações numa aba, sem abrir a CDHDR na mão. Aqui o caminho depende da sua release.
Em BTP trial ainda não rola. Não há CDS view liberada pra isso na trial no momento. Está planejado pra release 2608, tanto no SAP BTP ABAP Environment quanto no S/4HANA Cloud Public Edition (dá pra acompanhar no roadmap da SAP). Nos cenários abaixo, é sistema com a view disponível ou CDS próprio.
Em S/4HANA novo, com I_ChangeDocument_2 disponível
Aqui é tranquilo: você reaproveita a CDS standard I_ChangeDocument_2. Três passos.
1. Na interface view ZI_SALES_ORDER, crie a associação casando a classe do objeto e a chave da instância:
define root view entity ZI_SALES_ORDER
as select from zso_header
association of one to many I_ChangeDocument_2 as _ChangeDocs
on _ChangeDocs.ChangeDocObjectClass = 'ZCHD_SO'
and _ChangeDocs.ChangeDocObject = $projection.SalesOrderId
{
...
_ChangeDocs
}2. Exponha e redirecione na projection view ZC_SALES_ORDER:
define root view entity ZC_SALES_ORDER
provider contract transactional_query
as projection on ZI_SALES_ORDER
{
...
_ChangeDocs : redirected to C_ChangeDocument_2
}3. Na metadata extension, adicione o facet de histórico (do tipo #LINEITEM_REFERENCE apontando pra _ChangeDocs):
@UI.facet: [ { id: 'SalesOrderHead', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
label: 'Cabeçalho', position: 10 },
{ id: 'SalesOrderItem', purpose: #STANDARD, type: #LINEITEM_REFERENCE,
label: 'Itens', position: 20, targetElement: '_Item' },
{ id: 'ChangeHistory', purpose: #STANDARD, type: #LINEITEM_REFERENCE,
label: 'Histórico de Alterações', position: 30, targetElement: '_ChangeDocs' } ]Pronto. Em sistema com a I_ChangeDocument_2 liberada, é só isso.
Em S/4HANA antigo, sem I_ChangeDocument_2
Quando a view standard não existe, você monta a sua CDS direto sobre as tabelas CDHDR/CDPOS. Dá mais trabalho, mas é totalmente factível.
1. Crie a interface view ZI_SO_CHANGEDOC selecionando de cdpos com left join em cdhdr:
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Change Documents do Pedido'
@Metadata.ignorePropagatedAnnotations: true
@Metadata.allowExtensions: true
@ObjectModel.usageType: { serviceQuality: #X, sizeCategory: #XXL, dataClass: #MIXED }
@VDM.viewType: #EXTENSION
define root view entity ZI_SO_CHANGEDOC
as select from cdpos
left outer join cdhdr
on cdhdr.objectclas = cdpos.objectclas
and cdhdr.objectid = cdpos.objectid
and cdhdr.changenr = cdpos.changenr
association [0..1] to I_ChangeDocLongTableKey as _ChangeDocLongTableKey
on $projection.ChangeDocShortTableKey = _ChangeDocLongTableKey.ChangeDocKeyGuid
{
key cdpos.objectclas as ChangeDocObjectClass,
key cdpos.objectid as ChangeDocObject,
key cdpos.changenr as ChangeDocument,
key cdpos.tabname as ChangeDocDatabaseTable,
key cdpos.tabkey as ChangeDocShortTableKey,
key cdpos.fname as ChangeDocDatabaseTableField,
key cdpos.chngind as ChangeDocItemChangeType,
case when _ChangeDocLongTableKey.ChangeDocLongTableKey is not initial
then _ChangeDocLongTableKey.ChangeDocLongTableKey
else cdpos.tabkey
end as ChangeDocTableKey,
cdhdr.username as CreatedByUser,
@Semantics.systemDate.createdAt: true
cdhdr.udate as CreationDate,
@Semantics.systemTime.createdAt: true
cdhdr.utime as CreationTime,
cast( dats_tims_to_tstmp( cdhdr.udate, cdhdr.utime,
abap_system_timezone( $session.client, 'NULL' ),
$session.client, 'NULL' ) as cdcreated ) as CreationDateTime,
cdhdr.tcode as ChangeTransactionCode,
cdhdr.change_ind as ChangeDocChangeType,
@Semantics.language: true
cdhdr.langu as ChangeDocLanguage,
cast( cdpos.value_old as cdfldvalo preserving type ) as ChangeDocPreviousFieldValue,
cast( cdpos.value_new as cdfldvaln preserving type ) as ChangeDocNewFieldValue,
_ChangeDocLongTableKey
}A associação a I_ChangeDocLongTableKey resolve o caso de chave de tabela maior que 70 caracteres (lê de CDPOS_UID e cai de volta na chave curta da CDPOS quando não precisa).
2. Crie a projection view ZC_SO_CHANGEDOC sobre essa interface, reexpondo as chaves e os campos (ChangeDocDatabaseTableField, ChangeDocPreviousFieldValue, ChangeDocNewFieldValue, CreationDateTime, CreatedByUser, ChangeDocItemChangeType).
3. Na metadata extension da projection, defina as colunas da tabela de histórico:
@Metadata.layer: #CUSTOMER
@UI.headerInfo: { typeName: 'Change Document', typeNamePlural: 'Change Documents' }
annotate view ZC_SO_CHANGEDOC with
{
@UI.facet: [{ id: 'ChangeHistory', purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE, label: 'Histórico de Alterações', position: 10 }]
@UI.lineItem: [{ position: 10, label: 'Campo' }]
ChangeDocDatabaseTableField;
@UI.lineItem: [{ position: 20, label: 'Valor Antigo' }]
ChangeDocPreviousFieldValue;
@UI.lineItem: [{ position: 30, label: 'Valor Novo' }]
ChangeDocNewFieldValue;
@UI.lineItem: [{ position: 40, label: 'Alterado Em' }]
CreationDateTime;
@UI.lineItem: [{ position: 50, label: 'Alterado Por' }]
CreatedByUser;
@UI.lineItem: [{ position: 60, label: 'Tipo' }]
ChangeDocItemChangeType;
}4. Associe a interface do change document à raiz ZI_SALES_ORDER (mesma condição: ChangeDocObjectClass = 'ZCHD_SO' e ChangeDocObject = $projection.SalesOrderId).
5. Exponha e redirecione na consumption ZC_SALES_ORDER:
_Item : redirected to composition child ZC_SALES_ORDER_ITEM,
_ChangeDocs : redirected to ZC_SO_CHANGEDOC6. Acrescente o facet de Histórico de Alterações na metadata extension do cabeçalho (igual ao do cenário com a view standard: #LINEITEM_REFERENCE em _ChangeDocs, position 30).
7. Exponha a projection do change document no service definition:
define service ZUI_SALES_ORDER {
expose ZC_SALES_ORDER as SalesOrder;
expose ZC_SALES_ORDER_ITEM as SalesOrderItem;
expose ZC_SO_CHANGEDOC as SalesOrderChangeDocument;
}É isso. Agora o app mostra uma aba Histórico de Alterações que se atualiza a cada alteração no cabeçalho ou nos itens. Os itens entram porque declaramos changedocuments dependent by _SalesOrder no BDEF — tudo cai sob o mesmo objeto ZCHD_SO.
O que significam as letras U, I, J, D, E na coluna Tipo?
Na aba de histórico você vai ver letras na coluna de tipo. Cada uma diz o tipo da alteração, e elas se conectam direto com o que você configurou no create/update/delete do BDEF:
Letra | Significado |
|---|---|
| Update. Um campo mudou de valor. É o mais comum: grava valor antigo e novo. |
| Insert. Registro novo, uma única entrada pra ele todo (é o que sai com |
| Insert detalhado. Inserção com log campo a campo. É o que o item gera, porque usamos |
| Delete. Exclusão com uma única entrada, sem detalhe de campo. |
| Delete detalhado. Exclusão com log campo a campo (item com |
Resumindo: I e D são os marcadores simples de inserção/exclusão; J e E são as versões ricas, com detalhe de campo; e U é o cavalo de batalha de toda alteração.
Clássico x RAP: o que sai do seu código
Etapa | Clássico | RAP nativo |
|---|---|---|
Definir o objeto | Transação | Quick fix no ADT a partir do BDEF |
Marcar campos | Flag no data element | Flag no data element ou |
Gravar o log |
|
|
Onde grava |
|
|
Exibir | Tela própria / FM de leitura |
|
Perguntas frequentes
Change document funciona com draft?
Sim. O log é gravado no save efetivo, quando o draft é ativado e os dados vão pra tabela persistente. Alterações no rascunho não geram change document, o que é o comportamento certo: você audita só o que foi de fato salvo.
Dá pra usar em cenário unmanaged?
Não, pelo menos por enquanto. O change document nativo exige uma persistent table declarada, e isso restringe ao cenário managed. Em unmanaged você cairia de volta na abordagem clássica com os function modules.
Posso ter mais de um change document object no mesmo BO?
Não. Um RAP BO tem no máximo um change documents master. As entidades filhas se penduram nele com changedocuments dependent by, apontando pra associação que leva à raiz. Tudo cai sob a mesma chave de objeto.
Meu campo mudou e não apareceu no histórico. Por quê?
Quase sempre é falta de marcação. Confira se o data element tem o flag de change document, ou se o tipo CDS tem @AbapCatalog.typeSpec.changeDocumentRelevant: true e está aplicado via cast na CDS. Sem isso, o framework ignora a mudança de propósito.
Consigo ver o histórico no Fiori em BTP trial?
Ainda não. Não existe CDS view liberada pra isso na trial no momento. Está planejado pra release 2608 (BTP ABAP Environment e S/4HANA Cloud Public Edition). Até lá, em trial você lê por código com cl_chdo_read_tools=>changedocument_read.
Conclusão
Change document no RAP entrega muito por pouco código. As palavras-chave no BDEF, o objeto criado por quick fix, os campos marcados, e você tem uma trilha de auditoria campo a campo no formato que o SAP entende, sem manter nenhuma linha de log.
A parte que mais varia é a exibição: em sistema novo é a I_ChangeDocument_2 em três passos; em sistema antigo é a CDS própria sobre CDHDR/CDPOS. Vale conferir a release antes de prometer a aba de histórico. Feito isso, da próxima vez que perguntarem "quem mudou esse valor e quando?", a resposta está a uma aba de distância no app.
Referências
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.