CQRS (Command Query Responsibility Segregation) est un pattern architectural qui sépare strictement les opérations de lecture et d’écriture. Dans ABAP Cloud, CQRS permet des systèmes hautement optimisés où les chemins Query et Command peuvent être mis à l’échelle et optimisés indépendamment.
Qu’est-ce que CQRS ?
CQRS est basé sur le principe de Command Query Separation (CQS), mais l’étend au niveau architectural :
┌─────────────────────────────────────────────────────────────────────────────┐│ Architecture CQRS │├─────────────────────────────────────────────────────────────────────────────┤│ ││ ┌───────────────────┐ ││ │ Client │ ││ └─────────┬─────────┘ ││ │ ││ ┌─────────────┴─────────────┐ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ API Command │ │ API Query │ ││ │ (Écriture) │ │ (Lecture) │ ││ └────────┬────────┘ └────────┬────────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ Modèle Command │ │ Modèle Query │ ││ │ (Validation, │ Event │ (Optimisé │ ││ │ Logique │ ──────> │ pour lecture, │ ││ │ métier) │ │ Dénormalisé) │ ││ └────────┬────────┘ └────────┬────────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ Write Store │ │ Read Store │ ││ │ (Normalisé) │ │ (Dénormalisé) │ ││ └─────────────────┘ └─────────────────┘ │└─────────────────────────────────────────────────────────────────────────────┘Principes fondamentaux
| Principe | Description |
|---|---|
| Séparation | Les modèles de lecture et d’écriture sont complètement séparés |
| Optimisation | Chaque modèle est optimisé pour son objectif |
| Évolutivité | Côtés Query et Command évolutifs indépendamment |
| Flexibilité | Stockages différents possibles pour lecture et écriture |
Quand CQRS est-il pertinent ?
CQRS ne vaut pas pour chaque projet. Voici des critères de décision :
CQRS recommandé
| Scénario | Raison |
|---|---|
| Taux de lecture élevé (>90% Reads) | L’optimisation Query apporte le plus grand bénéfice |
| Modèles de lecture complexes | Views agrégées et dénormalisées pertinentes |
| Mise à l’échelle différenciée | Charges de lecture et d’écriture très différentes |
| Event Sourcing prévu | CQRS est un complément naturel |
| Logique de domaine complexe | Le côté Command peut rester focalisé |
CQRS non recommandé
| Scénario | Raison |
|---|---|
| Applications CRUD simples | L’overhead dépasse le bénéfice |
| Charge lecture/écriture équilibrée | Pas de direction d’optimisation claire |
| Exigence de cohérence forte | Eventual Consistency problématique |
| Petite équipe | Complexité supplémentaire difficile à maintenir |
CQRS dans ABAP Cloud
Dans ABAP Cloud, CQRS peut être implémenté élégamment avec RAP. Le Modèle Command correspond au BO RAP transactionnel, tandis que le Modèle Query est représenté par des CDS Views optimisées.
Vue d’ensemble de l’architecture
┌─────────────────────────────────────────────────────────────────────────────┐│ CQRS dans ABAP Cloud │├─────────────────────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ App Fiori │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ │ │ ││ │ Mutations │ Queries ││ ▼ ▼ ││ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ ││ │ Service Transactionnel │ │ Service Analytique │ ││ │ (API Command) │ │ (API Query) │ ││ │ │ │ │ ││ │ ┌───────────────────────┐ │ │ ┌────────────────────────────┐ │ ││ │ │ ZC_Order (Projection) │ │ │ │ ZC_OrderAnalytics (Read) │ │ ││ │ │ - create, update │ │ │ │ - Agrégations │ │ ││ │ │ - delete, actions │ │ │ │ - Joins, Calculs │ │ ││ │ └───────────────────────┘ │ │ └────────────────────────────┘ │ ││ │ │ │ │ │ │ ││ │ ▼ │ │ │ │ ││ │ ┌───────────────────────┐ │ │ │ │ ││ │ │ ZI_Order (Interface) │ │ │ │ │ ││ │ │ + Behavior Definition │ │ │ │ │ ││ │ └───────────────────────┘ │ │ │ │ ││ │ │ │ │ │ │ ││ └─────────────┼───────────────┘ └───────────────┼──────────────────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ Tables de base de données │ ││ │ (Normalisées, Single Source of Truth) │ ││ └─────────────────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────────────────┘Modèle Query (Optimisé pour la lecture)
Le modèle Query dans ABAP Cloud se compose de CDS Views optimisées pour des accès en lecture rapides.
Base : Vue de lecture dénormalisée
@EndUserText.label: 'Commandes - Modèle Query"@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #CHECK@Metadata.ignorePropagatedAnnotations: true@ObjectModel.usageType: { serviceQuality: #A, sizeCategory: #L, dataClass: #TRANSACTIONAL}define view entity ZI_OrderQuery as select from zorder as Order
-- Dénormalisation : Données client directement intégrées inner join zcustomer as Customer on Order.customer_id = Customer.customer_id
-- Dénormalisation : Master data matériel left outer join zmaterial as Material on Order.material_id = Material.material_id
{ key Order.order_id as OrderId, Order.order_date as OrderDate, Order.status as Status,
-- Données client (dénormalisées) Customer.customer_id as CustomerId, Customer.customer_name as CustomerName, Customer.city as CustomerCity, Customer.country as CustomerCountry, Customer.credit_limit as CustomerCreditLimit,
-- Données matériel (dénormalisées) Material.material_id as MaterialId, Material.material_text as MaterialText, Material.material_group as MaterialGroup,
-- Champs calculés Order.quantity as Quantity, Order.unit_price as UnitPrice, Order.quantity * Order.unit_price as TotalAmount,
-- Texte de statut calculé case Order.status when 'N' then 'Nouveau" when 'P' then 'En traitement" when 'C' then 'Terminé" when 'X' then 'Annulé" else 'Inconnu" end as StatusText,
-- Classification temporelle case when Order.order_date >= $session.system_date then 'Aujourd hui" when Order.order_date >= $session.system_date - 7 then 'Cette semaine" when Order.order_date >= $session.system_date - 30 then 'Ce mois" else 'Plus ancien" end as TimeCategory}Vue d’analyse agrégée
@EndUserText.label: 'Commandes - Analyse agrégée"@Analytics.dataCategory: #CUBEdefine view entity ZI_OrderAnalytics as select from ZI_OrderQuery{ -- Dimensions @AnalyticsDetails.query.axis: #ROWS CustomerCountry,
@AnalyticsDetails.query.axis: #ROWS MaterialGroup,
@AnalyticsDetails.query.axis: #ROWS TimeCategory,
@AnalyticsDetails.query.axis: #ROWS Status,
-- Mesures @Aggregation.default: #SUM TotalAmount,
@Aggregation.default: #SUM Quantity,
@Aggregation.default: #COUNT @EndUserText.label: 'Nombre de commandes" cast(1 as abap.int4) as OrderCount,
@Aggregation.default: #AVG @EndUserText.label: 'Valeur moyenne de commande" TotalAmount as AvgOrderValue}Définition du service Query
@EndUserText.label: 'Service Query Commandes"@ObjectModel.query.implementedBy: 'ABAP:ZCL_ORDER_QUERY_PROVIDER"define custom entity ZCE_OrderQuery{ key OrderId : abap.char(10); OrderDate : abap.dats; CustomerName : abap.char(80); CustomerCountry: land1; MaterialText : abap.char(40); TotalAmount : abap.dec(15,2); StatusText : abap.char(20);
-- Champs Query étendus CustomerRanking: abap.int4; DaysOpen : abap.int4;}Implémentation du Query Provider
CLASS zcl_order_query_provider DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_rap_query_provider.
ENDCLASS.
CLASS zcl_order_query_provider IMPLEMENTATION.
METHOD if_rap_query_provider~select. " Pagination et tri depuis la requête DATA(lv_top) = io_request->get_paging( )->get_page_size( ). DATA(lv_skip) = io_request->get_paging( )->get_offset( ). DATA(lt_sort) = io_request->get_sort_elements( ). DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
" Construire une requête optimisée SELECT order~order_id AS orderid, order~order_date AS orderdate, cust~customer_name AS customername, cust~country AS customercountry, mat~material_text AS materialtext, order~quantity * order~unit_price AS totalamount, CASE order~status WHEN 'N' THEN 'Nouveau" WHEN 'P' THEN 'En traitement" WHEN 'C' THEN 'Terminé" ELSE 'Inconnu" END AS statustext, -- Classement basé sur le chiffre d'affaires DENSE_RANK( ) OVER( ORDER BY order~quantity * order~unit_price DESC ) AS customerranking, DATS_DAYS_BETWEEN( order~order_date, @sy-datum ) AS daysopen FROM zorder AS order INNER JOIN zcustomer AS cust ON order~customer_id = cust~customer_id LEFT OUTER JOIN zmaterial AS mat ON order~material_id = mat~material_id INTO TABLE @DATA(lt_result) UP TO @lv_top ROWS OFFSET @lv_skip.
" Retourner le résultat io_response->set_data( lt_result ). io_response->set_total_number_of_records( lines( lt_result ) ). ENDMETHOD.
ENDCLASS.Modèle Command (Optimisé pour l’écriture)
Le modèle Command se concentre sur la logique métier, les validations et l’intégrité transactionnelle.
Vue Interface (Normalisée)
@EndUserText.label: 'Commande - Modèle Command"@AccessControl.authorizationCheck: #CHECKdefine view entity ZI_Order as select from zorder{ key order_id as OrderId, customer_id as CustomerId, material_id as MaterialId, order_date as OrderDate, quantity as Quantity, unit_price as UnitPrice, status as Status, created_by as CreatedBy, created_at as CreatedAt, changed_by as ChangedBy, changed_at as ChangedAt,
-- Associations (pas de dénormalisation) _Customer, _Material, _Items}Behavior Definition (Commands)
managed implementation in class zbp_i_order unique;strict ( 2 );with draft;
define behavior for ZI_Order alias Orderpersistent table zorderdraft table zorder_dlock master total etag ChangedAtauthorization master ( instance )etag master ChangedAt{ // Commands standard create; update; delete;
// Contrôle des champs field ( readonly ) OrderId, CreatedBy, CreatedAt, ChangedBy, ChangedAt; field ( mandatory ) CustomerId, MaterialId, Quantity;
// Actions métier (Commands) action confirm result [1] $self; action cancel result [1] $self; action reopen result [1] $self;
// Factory Action factory action copyOrder [1];
// Validations validation validateCustomer on save { create; update; field CustomerId; } validation validateQuantity on save { create; update; field Quantity; } validation validateStatus on save { field Status; }
// Determinations determination calculatePrice on modify { field Quantity, MaterialId; } determination setInitialStatus on modify { create; }
// Events pour la synchronisation Query event orderCreated; event orderConfirmed; event orderCancelled; event orderChanged;
draft action Edit; draft action Activate; draft action Discard; draft action Resume; draft determine action Prepare;}Implémentation Behavior
CLASS zbp_i_order DEFINITION LOCAL.ENDCLASS.
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION. METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION IMPORTING keys REQUEST requested_authorizations FOR Order RESULT result.
METHODS validateCustomer FOR VALIDATE ON SAVE IMPORTING keys FOR Order~validateCustomer.
METHODS validateQuantity FOR VALIDATE ON SAVE IMPORTING keys FOR Order~validateQuantity.
METHODS confirm FOR MODIFY IMPORTING keys FOR ACTION Order~confirm RESULT result.
METHODS cancel FOR MODIFY IMPORTING keys FOR ACTION Order~cancel RESULT result.
METHODS calculatePrice FOR DETERMINE ON MODIFY IMPORTING keys FOR Order~calculatePrice.
METHODS setInitialStatus FOR DETERMINE ON MODIFY IMPORTING keys FOR Order~setInitialStatus.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_instance_authorizations. ENDMETHOD.
METHOD validateCustomer. " Lire les commandes concernées READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order FIELDS ( CustomerId ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
" Vérifier les clients SELECT customer_id, credit_limit, blocked FROM zcustomer FOR ALL ENTRIES IN @lt_orders WHERE customer_id = @lt_orders-CustomerId INTO TABLE @DATA(lt_customers).
LOOP AT lt_orders INTO DATA(ls_order). READ TABLE lt_customers INTO DATA(ls_customer) WITH KEY customer_id = ls_order-CustomerId.
IF sy-subrc <> 0. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |Le client { ls_order-CustomerId } n existe pas| ) ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. CONTINUE. ENDIF.
IF ls_customer-blocked = abap_true. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |Le client { ls_order-CustomerId } est bloqué| ) ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. ENDIF. ENDLOOP. ENDMETHOD.
METHOD validateQuantity. READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order FIELDS ( Quantity ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order) WHERE Quantity <= 0. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |La quantité doit être supérieure à 0| ) %element-Quantity = if_abap_behv=>mk-on ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. ENDLOOP. ENDMETHOD.
METHOD confirm. " Lire les commandes READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order). " Vérifier si la confirmation est possible IF ls_order-Status <> 'N'. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-error text = |Seules les nouvelles commandes peuvent être confirmées| ) ) TO reported-order. APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order. CONTINUE. ENDIF.
" Modifier le statut MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( Status ) WITH VALUE #( ( %tky = ls_order-%tky Status = 'C' " Confirmé ) ) REPORTED DATA(lt_update_reported) FAILED DATA(lt_update_failed).
" Déclencher l'événement pour la synchronisation Query RAISE ENTITY EVENT zi_order~orderConfirmed FROM VALUE #( ( OrderId = ls_order-OrderId ) ). ENDLOOP.
" Retourner le résultat READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result ( %tky = order-%tky %param = order ) ). ENDMETHOD.
METHOD cancel. READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order). IF ls_order-Status = 'X'. APPEND VALUE #( %tky = ls_order-%tky %msg = new_message_with_text( severity = if_abap_behv_message=>severity-warning text = |Commande déjà annulée| ) ) TO reported-order. CONTINUE. ENDIF.
MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( Status ) WITH VALUE #( ( %tky = ls_order-%tky Status = 'X" ) ) REPORTED DATA(lt_update_reported).
RAISE ENTITY EVENT zi_order~orderCancelled FROM VALUE #( ( OrderId = ls_order-OrderId ) ). ENDLOOP.
READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result ( %tky = order-%tky %param = order ) ). ENDMETHOD.
METHOD calculatePrice. READ ENTITIES OF zi_order IN LOCAL MODE ENTITY Order FIELDS ( MaterialId Quantity ) WITH CORRESPONDING #( keys ) RESULT DATA(lt_orders).
" Récupérer les prix du master data matériel SELECT material_id, standard_price FROM zmaterial FOR ALL ENTRIES IN @lt_orders WHERE material_id = @lt_orders-MaterialId INTO TABLE @DATA(lt_prices).
LOOP AT lt_orders INTO DATA(ls_order). READ TABLE lt_prices INTO DATA(ls_price) WITH KEY material_id = ls_order-MaterialId.
IF sy-subrc = 0. MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( UnitPrice ) WITH VALUE #( ( %tky = ls_order-%tky UnitPrice = ls_price-standard_price ) ). ENDIF. ENDLOOP. ENDMETHOD.
METHOD setInitialStatus. MODIFY ENTITIES OF zi_order IN LOCAL MODE ENTITY Order UPDATE FIELDS ( Status ) WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = 'N' " Nouveau ) ). ENDMETHOD.
ENDCLASS.Synchronisation basée sur les événements
Dans une architecture CQRS complète, les modèles Query et Command sont synchronisés par des événements.
Event Handler pour la mise à jour Query
CLASS zcl_order_event_handler DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_rap_event_handler.
ENDCLASS.
CLASS zcl_order_event_handler IMPLEMENTATION.
METHOD if_rap_event_handler~handle. " Dispatch basé sur le type d'événement CASE io_event->get_event( ). WHEN 'ORDERCREATED'. handle_order_created( io_event ). WHEN 'ORDERCONFIRMED'. handle_order_confirmed( io_event ). WHEN 'ORDERCANCELLED'. handle_order_cancelled( io_event ). WHEN 'ORDERCHANGED'. handle_order_changed( io_event ). ENDCASE. ENDMETHOD.
METHOD handle_order_created. " Mettre à jour le cache Query ou synchroniser le Read-Store DATA(lt_event_data) = io_event->get_converted_event_data( ).
" Pour un Read-Store séparé : Écrire les données dénormalisées " Pour une solution cache : Invalider le cache
LOOP AT lt_event_data INTO DATA(ls_event). " Récupérer les données complètes de la commande SELECT SINGLE * FROM zi_orderquery WHERE orderid = @ls_event-orderid INTO @DATA(ls_order_query).
" Écrire dans le Read-Store (si table séparée) " MODIFY zorder_read FROM ls_order_query.
" Ou : Invalider le cache " zcl_order_cache=>invalidate( ls_event-orderid ). ENDLOOP. ENDMETHOD.
METHOD handle_order_confirmed. " Mettre à jour le statut dans le Read-Store ENDMETHOD.
METHOD handle_order_cancelled. " Mettre à jour le statut dans le Read-Store ENDMETHOD.
METHOD handle_order_changed. " Mettre à jour les champs modifiés dans le Read-Store ENDMETHOD.
ENDCLASS.Intégration avec RAP
La séparation CQRS dans RAP s’effectue via des services séparés pour la lecture et l’écriture.
Service Transactionnel (Commands)
@EndUserText.label: 'Service de gestion des commandes"define service ZUI_ORDER_O4 { expose ZC_Order as Order; expose ZC_OrderItem as OrderItem;}Service Analytics (Queries)
@EndUserText.label: 'Service Analytics Commandes"define service ZUI_ORDER_ANALYTICS_O4 { expose ZC_OrderAnalytics as OrderAnalytics; expose ZC_OrderQuery as OrderQuery;}Séparation des Service Bindings
Dans les Service Bindings, l’objectif est explicitement défini :
- Service Transactionnel : OData V4 avec CRUD complet
- Service Analytics : OData V4 Analytics (lecture seule)
Bonnes pratiques
1. Responsabilités claires
" Command Handler - focalisé sur la logique métierCLASS zcl_order_command_handler DEFINITION. PUBLIC SECTION. METHODS create_order IMPORTING is_order TYPE zif_order_types=>ts_create_command RAISING zcx_order_validation.
METHODS confirm_order IMPORTING iv_order_id TYPE zorder-order_id RAISING zcx_order_state.ENDCLASS.
" Query Handler - focalisé sur la récupération de donnéesCLASS zcl_order_query_handler DEFINITION. PUBLIC SECTION. METHODS get_orders_by_customer IMPORTING iv_customer_id TYPE zcustomer-customer_id RETURNING VALUE(rt_orders) TYPE zif_order_types=>tt_order_summary.
METHODS get_order_statistics IMPORTING iv_date_from TYPE datum iv_date_to TYPE datum RETURNING VALUE(rs_stats) TYPE zif_order_types=>ts_statistics.ENDCLASS.2. Prendre en compte l’Eventual Consistency
" Query avec indication de cohérenceMETHOD get_order_status. " Note : Les données peuvent être retardées jusqu'à X secondes SELECT SINGLE status, last_sync_timestamp FROM zorder_read WHERE order_id = @iv_order_id INTO @DATA(ls_result).
" Pour les requêtes critiques : Lire directement depuis le Command-Store IF iv_require_consistent = abap_true. SELECT SINGLE status FROM zorder WHERE order_id = @iv_order_id INTO @DATA(lv_status). ls_result-status = lv_status. ENDIF.
rs_result = ls_result.ENDMETHOD.3. Modèles de données séparés
| Aspect | Modèle Command | Modèle Query |
|---|---|---|
| Normalisation | 3NF, normalisé | Dénormalisé |
| Optimisation | Performance écriture | Performance lecture |
| Index | Optimisés écriture | Optimisés lecture |
| Validation | Complète | Aucune |
| Logique métier | Oui | Non |
4. Commands idempotents
METHOD execute_command. " Vérifier si le command a déjà été exécuté SELECT SINGLE @abap_true FROM zcommand_log WHERE command_id = @is_command-command_id INTO @DATA(lv_exists).
IF lv_exists = abap_true. " Idempotent : Déjà exécuté, pas d'erreur RETURN. ENDIF.
" Exécuter le command " ...
" Logger INSERT zcommand_log FROM @( VALUE #( command_id = is_command-command_id executed_at = utclong_current( ) executed_by = sy-uname ) ).ENDMETHOD.Résumé
| Aspect | Description |
|---|---|
| Modèle Query | CDS Views, dénormalisé, optimisé lecture |
| Modèle Command | RAP BO avec Behavior Definition, normalisé |
| Synchronisation | RAP Business Events ou jobs en arrière-plan |
| Cohérence | Accepter l’Eventual Consistency |
| Mise à l’échelle | Services séparés pour lecture/écriture |
CQRS dans ABAP Cloud permet des systèmes hautement optimisés pour des exigences complexes. La séparation des modèles de lecture et d’écriture offre de la flexibilité pour l’optimisation et la mise à l’échelle. Cependant, le pattern nécessite une complexité supplémentaire et ne devrait être utilisé qu’en cas de bénéfice clair.
Sujets connexes
- Design Patterns pour RAP - Autres patterns architecturaux
- Architecture Event-Driven - Communication asynchrone
- Fondamentaux RAP - RESTful ABAP Programming
- Custom Analytical Queries - CDS Views analytiques
- RAP Custom Entities - Query Providers flexibles