De nombreux processus métier nécessitent le suivi des modifications de données au fil du temps. Que ce soit pour l’historique des prix, les structures organisationnelles avec périodes de validité ou les conditions contractuelles qui changent périodiquement, les données temporelles constituent un thème central dans les applications d’entreprise. ABAP Cloud offre différentes approches pour implémenter proprement les données temporelles et le versioning.
Dans cet article, vous apprendrez les concepts derrière Temporal Data, comment créer des vues CDS temporelles, différentes stratégies d’historisation et comment implémenter des entités versionnées dans RAP.
Concepts fondamentaux de Temporal Data
Temporal Data décrit des données dont la validité est liée à des périodes temporelles. On distingue deux dimensions :
| Dimension | Description | Exemple |
|---|---|---|
| Valid Time | Quand les données sont valides (temps métier) | Le prix est valable du 01.01. au 31.03. |
| Transaction Time | Quand les données ont été saisies (temps système) | Le prix a été saisi le 15.12. |
Données bitemporelles
La combinaison des deux dimensions s’appelle données bitemporelles. Elle permet de répondre à des questions comme :
- “Quel était le prix au 15.02. selon notre état de connaissance du 01.03. ?”
- “Quand avons-nous appris que le prix a changé ?”
Pour la plupart des applications métier, Valid Time suffit. Transaction Time est surtout pertinent pour les exigences d’audit.
Patterns temporels
Il existe différentes approches pour modéliser les données temporelles :
| Pattern | Description | Cas d’usage |
|---|---|---|
| Snapshot | État actuel sans historique | Données de base simples |
| Effective Dating | Périodes de validité depuis/jusqu’à | Listes de prix, conditions |
| Event Sourcing | Toutes les modifications comme événements | Traçabilité complète |
| Slowly Changing Dimension | Versioning avec flag historique | Data Warehouse |
Conception de table pour Temporal Data
La première étape est une conception de table appropriée. Voici un exemple de table de prix temporelle :
@EndUserText.label : 'Produktpreise (zeitabhaengig)"@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENTdefine table ztproduct_price { key client : abap.clnt not null; key product_id : abap.char(10) not null; key valid_from : abap.dats not null; valid_to : abap.dats; price : abap.dec(15,2); currency : waers; created_by : abap.uname; created_at : timestampl; last_changed_by : abap.uname; last_changed_at : timestampl;}Décisions de conception importantes :
- Clé composite :
product_id+valid_fromidentifient le prix de manière unique - valid_to : Peut être initialement vide (= valable indéfiniment)
- Champs d’audit :
created_atetlast_changed_atpour Transaction Time
Variante avec numéro de version
Alternativement au datage, un numéro de version explicite peut être utilisé :
@EndUserText.label : 'Vertragsbedingungen (versioniert)"define table ztcontract_terms { key client : abap.clnt not null; key contract_id : abap.char(10) not null; key version : abap.numc(4) not null; valid_from : abap.dats; valid_to : abap.dats; is_current : abap_boolean; terms_text : abap.string(0); approved_by : abap.uname; approved_at : timestampl; created_by : abap.uname; created_at : timestampl;}Le flag is_current permet un accès rapide à la version actuelle sans comparaisons de dates.
Time-Dependent CDS Views
Les vues CDS peuvent encapsuler élégamment les données temporelles et simplifier l’accès.
Vue de base avec données temporelles
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Produktpreise - Basis"
define view entity ZI_ProductPrice as select from ztproduct_price{ key product_id as ProductId, key valid_from as ValidFrom, valid_to as ValidTo, price as Price, currency as Currency, created_by as CreatedBy, created_at as CreatedAt, last_changed_by as LastChangedBy, last_changed_at as LastChangedAt,
-- Berechnete Felder case when valid_to is initial then 'X" when valid_to >= $session.system_date then 'X" else '" end as IsCurrentlyValid,
case when valid_to is initial then abap.dats'99991231" else valid_to end as EffectiveValidTo}Vue pour les prix actuellement valables
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Produktpreise - Aktuell gueltig"
define view entity ZI_ProductPriceCurrent as select from ZI_ProductPrice{ key ProductId, ValidFrom, ValidTo, Price, Currency, LastChangedAt}where ValidFrom <= $session.system_date and ( ValidTo >= $session.system_date or ValidTo = '00000000' )La variable $session.system_date fournit la date système actuelle et permet un filtrage dynamique.
Vue avec requête à date fixe
Pour les requêtes à une date fixe spécifique, un paramètre est approprié :
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Produktpreise - Stichtagsabfrage"
define view entity ZI_ProductPriceAtDate with parameters p_key_date : abap.dats as select from ztproduct_price{ key product_id as ProductId, valid_from as ValidFrom, valid_to as ValidTo, price as Price, currency as Currency}where valid_from <= $parameters.p_key_date and ( valid_to >= $parameters.p_key_date or valid_to = '00000000' )Utilisation en ABAP :
" Preis zum 15.03.2026 abfragenSELECT * FROM zi_productpriceatdate( p_key_date = '20260315' ) WHERE ProductId = 'PROD001" INTO TABLE @DATA(lt_prices).Vue historique avec prédécesseur/successeur
Une vue étendue peut afficher le prédécesseur et le successeur d’un enregistrement :
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Produktpreis-Historie"
define view entity ZI_ProductPriceHistory as select from ztproduct_price as current_price left outer join ztproduct_price as previous_price on current_price.product_id = previous_price.product_id and previous_price.valid_to = dats_add_days( current_price.valid_from, -1, 'INITIAL' ){ key current_price.product_id as ProductId, key current_price.valid_from as ValidFrom, current_price.valid_to as ValidTo, current_price.price as CurrentPrice, current_price.currency as Currency,
previous_price.price as PreviousPrice, previous_price.valid_from as PreviousValidFrom,
-- Preisaenderung in Prozent case when previous_price.price is not initial and previous_price.price <> 0 then division( ( current_price.price - previous_price.price ) * 100, previous_price.price, 2 ) else cast( 0 as abap.dec(5,2) ) end as PriceChangePercent}Historisation avec RAP
Dans les applications RAP, l’historisation des modifications est un sujet important. Il existe plusieurs approches.
Approche 1 : Table d’historique séparée
Dans cette approche, l’état précédent est copié dans une table d’historique à chaque modification :
-- Haupttabelle (aktueller Stand)define table ztproduct { key client : abap.clnt not null; key product_id : abap.char(10) not null; name : abap.char(40); price : abap.dec(15,2); currency : waers; status : abap.char(2); last_changed_at : timestampl;}
-- History-Tabelledefine table ztproduct_history { key client : abap.clnt not null; key product_id : abap.char(10) not null; key history_timestamp : timestampl not null; name : abap.char(40); price : abap.dec(15,2); currency : waers; status : abap.char(2); changed_by : abap.uname; change_type : abap.char(1); -- C=Create, U=Update, D=Delete}L’historisation s’effectue dans une détermination :
CLASS lhc_product DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS record_history FOR DETERMINE ON SAVE IMPORTING keys FOR Product~recordHistory.ENDCLASS.
CLASS lhc_product IMPLEMENTATION. METHOD record_history. " Aktuelle Daten lesen READ ENTITIES OF zi_product IN LOCAL MODE ENTITY Product ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_products).
" History-Eintraege erstellen DATA lt_history TYPE STANDARD TABLE OF ztproduct_history.
LOOP AT lt_products INTO DATA(ls_product). APPEND VALUE #( product_id = ls_product-ProductId history_timestamp = cl_abap_context_info=>get_system_time( ) name = ls_product-Name price = ls_product-Price currency = ls_product-Currency status = ls_product-Status changed_by = cl_abap_context_info=>get_user_technical_name( ) change_type = 'U' " Update ) TO lt_history. ENDLOOP.
" In History-Tabelle schreiben INSERT ztproduct_history FROM TABLE @lt_history. ENDMETHOD.ENDCLASS.Approche 2 : Event Sourcing avec RAP Business Events
Une approche plus moderne stocke toutes les modifications comme événements :
managed implementation in class zbp_i_product unique;strict ( 2 );
define behavior for ZI_Product alias Productpersistent table ztproductlock master total etag LastChangedAtauthorization master ( instance )etag master LastChangedAt{ create; update; delete;
-- Business Events fuer Audit event ProductCreated parameter ZA_ProductEvent; event ProductUpdated parameter ZA_ProductEvent; event ProductDeleted parameter ZA_ProductEvent;
determination recordChanges on save { create; update; delete; }}Le paramètre d’événement :
@EndUserText.label: 'Product Change Event"define abstract entity ZA_ProductEvent{ ProductId : abap.char(10); ChangeType : abap.char(10); OldValues : abap.string(0); NewValues : abap.string(0); ChangedBy : abap.uname; ChangedAt : timestampl;}Déclenchement d’événement dans la détermination :
METHOD recordChanges. " Update-Events READ ENTITIES OF zi_product IN LOCAL MODE ENTITY Product ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_products).
LOOP AT lt_products INTO DATA(ls_product). DATA(lv_new_values) = /ui2/cl_json=>serialize( ls_product ).
RAISE ENTITY EVENT zi_product~ProductUpdated FROM VALUE #( ( %key = ls_product-%key ProductId = ls_product-ProductId ChangeType = 'UPDATE" NewValues = lv_new_values ChangedBy = cl_abap_context_info=>get_user_technical_name( ) ChangedAt = cl_abap_context_info=>get_system_time( ) ) ). ENDLOOP.ENDMETHOD.Approche 3 : Soft Delete avec versioning
Dans cette approche, les enregistrements ne sont jamais supprimés, mais seulement marqués comme non valides :
define table ztcontract { key client : abap.clnt not null; key contract_id : abap.char(10) not null; key version : abap.numc(4) not null; is_current : abap_boolean; is_deleted : abap_boolean; valid_from : abap.dats; valid_to : abap.dats; customer_id : abap.char(10); contract_type : abap.char(4); annual_value : abap.dec(15,2); currency : waers; created_by : abap.uname; created_at : timestampl;}La vue CDS filtre automatiquement sur les versions actuelles non supprimées :
define view entity ZI_Contract as select from ztcontract{ key contract_id as ContractId, version as Version, valid_from as ValidFrom, valid_to as ValidTo, customer_id as CustomerId, contract_type as ContractType, annual_value as AnnualValue, currency as Currency, is_current as IsCurrent, is_deleted as IsDeleted, created_at as CreatedAt}where is_current = 'X" and is_deleted = '"Entité versionnée - Exemple complet
Voici un exemple complet pour un contrat versionné avec RAP :
Modèle de données
-- Vertragsstamm (aktuelle Version)@EndUserText.label: 'Vertraege"define table ztcontract_main { key client : abap.clnt not null; key contract_id : abap.char(10) not null; customer_id : abap.char(10); contract_type : abap.char(4); current_version : abap.numc(4); status : abap.char(2); created_by : abap.uname; created_at : timestampl; last_changed_at : timestampl;}
-- Vertragsversionen@EndUserText.label: 'Vertragsversionen"define table ztcontract_version { key client : abap.clnt not null; key contract_id : abap.char(10) not null; key version : abap.numc(4) not null; valid_from : abap.dats; valid_to : abap.dats; annual_value : abap.dec(15,2); currency : waers; payment_terms : abap.char(4); notice_period : abap.numc(3); notice_unit : abap.char(1); terms_text : abap.string(0); version_reason : abap.char(60); created_by : abap.uname; created_at : timestampl;}Vue d’interface avec versions
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Vertrag mit Versionen"
define root view entity ZI_Contract as select from ztcontract_main as contract composition [0..*] of ZI_ContractVersion as _Versions{ key contract_id as ContractId, customer_id as CustomerId, contract_type as ContractType, current_version as CurrentVersion, status as Status, created_by as CreatedBy, created_at as CreatedAt, last_changed_at as LastChangedAt,
_Versions}@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Vertragsversion"
define view entity ZI_ContractVersion as select from ztcontract_version association to parent ZI_Contract as _Contract on $projection.ContractId = _Contract.ContractId{ key contract_id as ContractId, key version as Version, valid_from as ValidFrom, valid_to as ValidTo, annual_value as AnnualValue, currency as Currency, payment_terms as PaymentTerms, notice_period as NoticePeriod, notice_unit as NoticeUnit, terms_text as TermsText, version_reason as VersionReason, created_by as CreatedBy, created_at as CreatedAt,
-- Berechnetes Feld: Ist aktuelle Version? case when version = _Contract.CurrentVersion then 'X" else '" end as IsCurrentVersion,
_Contract}Behavior Definition
managed implementation in class zbp_i_contract unique;strict ( 2 );with draft;
define behavior for ZI_Contract alias Contractpersistent table ztcontract_maindraft table zdraft_contractlock master total etag LastChangedAtauthorization master ( instance )etag master LastChangedAt{ create; update; delete;
association _Versions { create; }
action createNewVersion result [1] $self;
draft action Edit; draft action Activate optimized; draft action Discard; draft action Resume; draft determine action Prepare;}
define behavior for ZI_ContractVersion alias Versionpersistent table ztcontract_versiondraft table zdraft_contr_verslock dependent by _Contractauthorization dependent by _Contractetag master CreatedAt{ update; delete;
field ( readonly ) ContractId, Version, CreatedBy, CreatedAt; field ( readonly ) IsCurrentVersion;
association _Contract;}Behavior Implementation
CLASS lhc_contract DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS createNewVersion FOR MODIFY IMPORTING keys FOR ACTION Contract~createNewVersion RESULT result.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION IMPORTING keys REQUEST requested_authorizations FOR Contract RESULT result.ENDCLASS.
CLASS lhc_contract IMPLEMENTATION. METHOD createNewVersion. " Aktuelle Vertragsdaten lesen READ ENTITIES OF zi_contract IN LOCAL MODE ENTITY Contract ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_contracts) ENTITY Contract BY \_Versions ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_versions).
LOOP AT lt_contracts ASSIGNING FIELD-SYMBOL(<ls_contract>). " Aktuelle Version ermitteln DATA(lv_current_version) = <ls_contract>-CurrentVersion. DATA(lv_new_version) = lv_current_version + 1.
" Letzte Versionsdaten als Vorlage holen READ TABLE lt_versions INTO DATA(ls_latest_version) WITH KEY ContractId = <ls_contract>-ContractId Version = lv_current_version.
IF sy-subrc = 0. " Alte Version abschliessen (ValidTo setzen) MODIFY ENTITIES OF zi_contract IN LOCAL MODE ENTITY Version UPDATE FIELDS ( ValidTo ) WITH VALUE #( ( %tky = ls_latest_version-%tky ValidTo = cl_abap_context_info=>get_system_date( ) ) ).
" Neue Version erstellen MODIFY ENTITIES OF zi_contract IN LOCAL MODE ENTITY Contract CREATE BY \_Versions FIELDS ( Version ValidFrom AnnualValue Currency PaymentTerms NoticePeriod NoticeUnit TermsText VersionReason ) WITH VALUE #( ( %tky = <ls_contract>-%tky %target = VALUE #( ( %cid = |NEW_VERSION_{ <ls_contract>-ContractId }| Version = lv_new_version ValidFrom = cl_abap_context_info=>get_system_date( ) AnnualValue = ls_latest_version-AnnualValue Currency = ls_latest_version-Currency PaymentTerms = ls_latest_version-PaymentTerms NoticePeriod = ls_latest_version-NoticePeriod NoticeUnit = ls_latest_version-NoticeUnit TermsText = ls_latest_version-TermsText VersionReason = 'Neue Version erstellt" ) ) ) ) MAPPED DATA(ls_mapped_version).
" Versionsnummer im Hauptvertrag aktualisieren MODIFY ENTITIES OF zi_contract IN LOCAL MODE ENTITY Contract UPDATE FIELDS ( CurrentVersion ) WITH VALUE #( ( %tky = <ls_contract>-%tky CurrentVersion = lv_new_version ) ). ENDIF.
" Result zurueckgeben APPEND VALUE #( %tky = <ls_contract>-%tky %param = <ls_contract> ) TO result. ENDLOOP. ENDMETHOD.
METHOD get_instance_authorizations. result = VALUE #( FOR key IN keys ( %tky = key-%tky %action-createNewVersion = COND #( WHEN 1 = 1 THEN if_abap_behv=>auth-allowed ELSE if_abap_behv=>auth-unauthorized ) ) ). ENDMETHOD.ENDCLASS.Requêtes sur les données temporelles
Version valide à une date fixe
" Vertragskonditionen zum Stichtag 01.06.2026DATA(lv_key_date) = CONV d( '20260601' ).
SELECT contract~ContractId, contract~CustomerId, version~Version, version~ValidFrom, version~ValidTo, version~AnnualValue, version~Currency FROM zi_contract AS contract INNER JOIN zi_contractversion AS version ON contract~ContractId = version~ContractId WHERE version~ValidFrom <= @lv_key_date AND ( version~ValidTo >= @lv_key_date OR version~ValidTo = '00000000' ) INTO TABLE @DATA(lt_contracts_at_date).Afficher l’historique des versions
" Alle Versionen eines VertragsSELECT ContractId, Version, ValidFrom, ValidTo, AnnualValue, VersionReason, CreatedBy, CreatedAt FROM zi_contractversion WHERE ContractId = 'C000000001" ORDER BY Version DESCENDING INTO TABLE @DATA(lt_version_history).
" Ausgabe der HistorieLOOP AT lt_version_history INTO DATA(ls_version). WRITE: / ls_version-Version, ls_version-ValidFrom, ls_version-ValidTo, ls_version-AnnualValue, ls_version-VersionReason.ENDLOOP.Déterminer les modifications entre versions
CLASS zcl_version_compare DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. TYPES: BEGIN OF ty_field_change, field_name TYPE string, old_value TYPE string, new_value TYPE string, END OF ty_field_change, tt_field_changes TYPE STANDARD TABLE OF ty_field_change WITH EMPTY KEY.
METHODS compare_versions IMPORTING iv_contract_id TYPE ztcontract_version-contract_id iv_version_old TYPE ztcontract_version-version iv_version_new TYPE ztcontract_version-version RETURNING VALUE(rt_changes) TYPE tt_field_changes.ENDCLASS.
CLASS zcl_version_compare IMPLEMENTATION. METHOD compare_versions. " Beide Versionen laden SELECT SINGLE * FROM zi_contractversion WHERE ContractId = @iv_contract_id AND Version = @iv_version_old INTO @DATA(ls_old).
SELECT SINGLE * FROM zi_contractversion WHERE ContractId = @iv_contract_id AND Version = @iv_version_new INTO @DATA(ls_new).
" Feldvergleich IF ls_old-AnnualValue <> ls_new-AnnualValue. APPEND VALUE #( field_name = 'Jahreswert" old_value = |{ ls_old-AnnualValue }| new_value = |{ ls_new-AnnualValue }| ) TO rt_changes. ENDIF.
IF ls_old-PaymentTerms <> ls_new-PaymentTerms. APPEND VALUE #( field_name = 'Zahlungsbedingungen" old_value = ls_old-PaymentTerms new_value = ls_new-PaymentTerms ) TO rt_changes. ENDIF.
IF ls_old-NoticePeriod <> ls_new-NoticePeriod. APPEND VALUE #( field_name = 'Kuendigungsfrist" old_value = |{ ls_old-NoticePeriod } { ls_old-NoticeUnit }| new_value = |{ ls_new-NoticePeriod } { ls_new-NoticeUnit }| ) TO rt_changes. ENDIF. ENDMETHOD.ENDCLASS.Intégration UI
Pour l’affichage des versions dans Fiori Elements, définissez les annotations correspondantes :
@Metadata.layer: #CORE
annotate view ZC_ContractVersion with{ @UI.facet: [ { id: 'VersionDetails', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'Versionsdetails', position: 10 } ]
@UI.lineItem: [{ position: 10, importance: #HIGH }] @UI.identification: [{ position: 10 }] Version;
@UI.lineItem: [{ position: 20, importance: #HIGH }] @UI.identification: [{ position: 20 }] ValidFrom;
@UI.lineItem: [{ position: 30, importance: #MEDIUM }] @UI.identification: [{ position: 30 }] ValidTo;
@UI.lineItem: [{ position: 40, importance: #HIGH }] @UI.identification: [{ position: 40 }] AnnualValue;
@UI.lineItem: [{ position: 50, importance: #LOW }] VersionReason;
@UI.lineItem: [{ position: 60, importance: #LOW, criticality: 'Criticality', criticalityRepresentation: #WITH_ICON }] IsCurrentVersion;}Bonnes pratiques
1. Périodes cohérentes
Assurez-vous que les périodes sont sans lacunes et sans chevauchements :
METHOD validate_no_gaps. " Alle Versionen eines Vertrags laden SELECT ValidFrom, ValidTo FROM ztcontract_version WHERE contract_id = @iv_contract_id ORDER BY ValidFrom INTO TABLE @DATA(lt_periods).
" Auf Luecken pruefen DATA(lv_expected_start) = VALUE d( ).
LOOP AT lt_periods INTO DATA(ls_period). IF sy-tabix > 1 AND ls_period-ValidFrom <> lv_expected_start. " Luecke gefunden RAISE EXCEPTION TYPE zcx_validation EXPORTING textid = zcx_validation=>gap_in_validity. ENDIF.
lv_expected_start = ls_period-ValidTo + 1. ENDLOOP.ENDMETHOD.2. Optimisation des performances
Pour de grandes quantités de données, indexez les champs de date :
-- Sekundaerindex fuer zeitbasierte Abfragen@AbapCatalog.sqlViewAppendName: 'ZIDX_PRICE_DATE"define table index ztproduct_price_idx on ztproduct_price primary index (product_id, valid_from, valid_to)3. Archivage des anciennes versions
Planifiez l’archivage des anciennes versions :
" Versionen aelter als 7 Jahre archivierenDATA(lv_archive_date) = cl_abap_context_info=>get_system_date( ) - 365 * 7.
SELECT * FROM ztcontract_version WHERE valid_to < @lv_archive_date AND valid_to <> '00000000" INTO TABLE @DATA(lt_to_archive).
" In Archivtabelle verschiebenINSERT ztcontract_vers_arch FROM TABLE @lt_to_archive.DELETE ztcontract_version FROM TABLE @lt_to_archive.Résumé
Les données temporelles et le versioning sont des concepts essentiels pour les applications métier :
- Temporal Data distingue Valid Time (validité métier) et Transaction Time (temps système)
- Time-Dependent CDS Views encapsulent la logique temporelle et permettent des requêtes à date fixe
- L’historisation peut être mise en œuvre via des tables d’historique séparées, Event Sourcing ou Soft Delete
- Les entités versionnées dans RAP utilisent des relations de composition entre les données principales et les versions
- Les bonnes pratiques incluent des périodes sans lacunes, l’optimisation des performances et l’archivage
Avec ces techniques, vous pouvez assurer la traçabilité complète de vos données métier tout en permettant des requêtes efficaces sur les états historiques.
Thèmes connexes
- CDS Views: Core Data Services verstehen - Bases du développement CDS
- Aenderungsbelege in ABAP - Journalisation classique des modifications
- RAP Basics - Fondamentaux du RESTful ABAP Programming Model
- RAP Business Events - Utiliser les événements pour Event Sourcing