Les Custom Entities avec agrégation permettent des fonctions de reporting performantes dans RAP. Alors que les CDS Views conviennent pour des agrégations simples, les Custom Queries avec code ABAP offrent un contrôle total sur les opérations GROUP BY complexes, les filtres dynamiques et les KPI calculés.
Quand utiliser les Custom Queries pour l’agrégation ?
Les Custom Queries sont le bon choix lorsque :
| Besoin | Agrégation CDS | Custom Query |
|---|---|---|
| GROUP BY simple | ✅ Recommandé | Possible |
| Plusieurs niveaux d’agrégation | ⚠️ Limité | ✅ Recommandé |
| Groupement dynamique | ❌ Impossible | ✅ Recommandé |
| Calculs complexes (YoY, %) | ⚠️ Complexe | ✅ Recommandé |
| Sources de données externes | ❌ Impossible | ✅ Recommandé |
| Combinaison de plusieurs sources | ❌ Impossible | ✅ Recommandé |
Structure de base d’une Custom Entity avec agrégation
La Custom Entity définit les champs agrégés :
@EndUserText.label: 'Umsatzstatistik"@ObjectModel.query.implementedBy: 'ABAP:ZCL_SALES_STAT_QUERY"define custom entity ZI_SalesStatistics{ key CustomerId : abap.char(10); key SalesMonth : abap.char(7); " YYYY-MM Format CustomerName : abap.char(80); TotalRevenue : abap.dec(15,2); OrderCount : abap.int4; AvgOrderValue : abap.dec(15,2); MinOrderValue : abap.dec(15,2); MaxOrderValue : abap.dec(15,2); Currency : abap.cuky;}Implémentation de Query avec agrégation
La classe Query effectue l’agrégation via ABAP SQL :
CLASS zcl_sales_stat_query DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_rap_query_provider.
PRIVATE SECTION. TYPES: BEGIN OF ty_statistics, CustomerId TYPE kunnr, SalesMonth TYPE char7, CustomerName TYPE name1, TotalRevenue TYPE wertv8, OrderCount TYPE int4, AvgOrderValue TYPE wertv8, MinOrderValue TYPE wertv8, MaxOrderValue TYPE wertv8, Currency TYPE waers, END OF ty_statistics, tt_statistics TYPE STANDARD TABLE OF ty_statistics WITH EMPTY KEY.
METHODS aggregate_sales_data IMPORTING it_filter TYPE if_rap_query_filter=>tt_name_range_pairs RETURNING VALUE(rt_data) TYPE tt_statistics.
METHODS apply_sorting IMPORTING it_sort TYPE abp_behv_sort_tab CHANGING ct_data TYPE tt_statistics.
METHODS apply_paging IMPORTING io_paging TYPE REF TO if_rap_query_paging CHANGING ct_data TYPE tt_statistics.ENDCLASS.
CLASS zcl_sales_stat_query IMPLEMENTATION. METHOD if_rap_query_provider~select. IF io_request->is_data_requested( ). " Filter abrufen DATA(lt_filter) = VALUE if_rap_query_filter=>tt_name_range_pairs( ). TRY. lt_filter = io_request->get_filter( )->get_as_ranges( ). CATCH cx_rap_query_filter_no_range. ENDTRY.
" Aggregation durchführen DATA(lt_statistics) = aggregate_sales_data( lt_filter ).
" Sortierung anwenden apply_sorting( EXPORTING it_sort = io_request->get_sort_elements( ) CHANGING ct_data = lt_statistics ).
" Gesamtanzahl vor Paging speichern DATA(lv_total_count) = lines( lt_statistics ).
" Paging anwenden apply_paging( EXPORTING io_paging = io_request->get_paging( ) CHANGING ct_data = lt_statistics ).
io_response->set_data( lt_statistics ). ENDIF.
IF io_request->is_total_numb_of_rec_requested( ). io_response->set_total_number_of_records( lv_total_count ). ENDIF. ENDMETHOD.
METHOD aggregate_sales_data. " Aggregation mit GROUP BY SELECT vbak~kunnr AS CustomerId, LEFT( CAST( vbak~erdat AS char ), 7 ) AS SalesMonth, kna1~name1 AS CustomerName, SUM( vbap~netwr ) AS TotalRevenue, COUNT( DISTINCT vbak~vbeln ) AS OrderCount, AVG( vbap~netwr ) AS AvgOrderValue, MIN( vbap~netwr ) AS MinOrderValue, MAX( vbap~netwr ) AS MaxOrderValue, vbak~waerk AS Currency FROM i_salesorder AS vbak INNER JOIN i_salesorderitem AS vbap ON vbap~salesorder = vbak~salesorder INNER JOIN i_customer AS kna1 ON kna1~customer = vbak~soldtoparty WHERE vbak~salesordertype = 'TA" GROUP BY vbak~kunnr, LEFT( CAST( vbak~erdat AS char ), 7 ), kna1~name1, vbak~waerk INTO TABLE @rt_data.
" Filter nachträglich anwenden LOOP AT it_filter INTO DATA(ls_filter). CASE ls_filter-name. WHEN 'CUSTOMERID'. DELETE rt_data WHERE CustomerId NOT IN ls_filter-range. WHEN 'SALESMONTH'. DELETE rt_data WHERE SalesMonth NOT IN ls_filter-range. ENDCASE. ENDLOOP. ENDMETHOD.
METHOD apply_sorting. " Dynamische Sortierung aufbauen DATA(lv_sort_string) = ``.
LOOP AT it_sort INTO DATA(ls_sort). IF lv_sort_string IS NOT INITIAL. lv_sort_string = lv_sort_string && ` `. ENDIF.
lv_sort_string = lv_sort_string && ls_sort-element_name.
IF ls_sort-descending = abap_true. lv_sort_string = lv_sort_string && ` DESCENDING`. ELSE. lv_sort_string = lv_sort_string && ` ASCENDING`. ENDIF. ENDLOOP.
IF lv_sort_string IS NOT INITIAL. SORT ct_data BY (lv_sort_string). ELSE. " Default-Sortierung: Umsatz absteigend SORT ct_data BY TotalRevenue DESCENDING. ENDIF. ENDMETHOD.
METHOD apply_paging. DATA(lv_offset) = io_paging->get_offset( ). DATA(lv_page_size) = io_paging->get_page_size( ).
IF lv_page_size <= 0. RETURN. " Kein Paging gewünscht ENDIF.
" Maximale Seitengröße begrenzen IF lv_page_size > 500. lv_page_size = 500. ENDIF.
DATA(lv_from) = lv_offset + 1. DATA(lv_to) = lv_offset + lv_page_size.
" Nur gewünschten Bereich behalten DATA lt_paged LIKE ct_data. LOOP AT ct_data INTO DATA(ls_row) FROM lv_from TO lv_to. APPEND ls_row TO lt_paged. ENDLOOP.
ct_data = lt_paged. ENDMETHOD.ENDCLASS.Exemple pratique : Statistiques de ventes par client et mois
Un exemple complet pour une application de reporting de ventes :
1. Définition de Custom Entity
@EndUserText.label: 'Kundenumsatz-Statistik"@ObjectModel.query.implementedBy: 'ABAP:ZCL_REVENUE_REPORT_QUERY"define custom entity ZI_CustomerRevenue{ key Customer : kunnr; key FiscalYear : gjahr; key FiscalPeriod : monat; CustomerName : name1; Region : regio; Revenue : wertv8; Quantity : menge_d; OrderCount : abap.int4; AvgOrderValue : wertv8; Currency : waers; " Berechnete KPIs RevenueShare : abap.dec(5,2); " % vom Gesamtumsatz GrowthRate : abap.dec(7,2); " % zum Vorjahr}2. Implémentation de Query avec KPI étendus
CLASS zcl_revenue_report_query DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_rap_query_provider.
PRIVATE SECTION. TYPES: BEGIN OF ty_revenue, Customer TYPE kunnr, FiscalYear TYPE gjahr, FiscalPeriod TYPE monat, CustomerName TYPE name1, Region TYPE regio, Revenue TYPE wertv8, Quantity TYPE menge_d, OrderCount TYPE int4, AvgOrderValue TYPE wertv8, Currency TYPE waers, RevenueShare TYPE decfloat16, GrowthRate TYPE decfloat16, END OF ty_revenue, tt_revenue TYPE STANDARD TABLE OF ty_revenue WITH EMPTY KEY.
METHODS load_aggregated_data IMPORTING it_filter TYPE if_rap_query_filter=>tt_name_range_pairs RETURNING VALUE(rt_data) TYPE tt_revenue.
METHODS calculate_kpis CHANGING ct_data TYPE tt_revenue.
METHODS calculate_growth_rate IMPORTING iv_customer TYPE kunnr iv_year TYPE gjahr iv_period TYPE monat iv_revenue TYPE wertv8 RETURNING VALUE(rv_rate) TYPE decfloat16.ENDCLASS.
CLASS zcl_revenue_report_query IMPLEMENTATION. METHOD if_rap_query_provider~select. IF io_request->is_data_requested( ). " Filter abrufen DATA(lt_filter) = VALUE if_rap_query_filter=>tt_name_range_pairs( ). TRY. lt_filter = io_request->get_filter( )->get_as_ranges( ). CATCH cx_rap_query_filter_no_range. ENDTRY.
" Aggregierte Daten laden DATA(lt_data) = load_aggregated_data( lt_filter ).
" KPIs berechnen (RevenueShare, GrowthRate) calculate_kpis( CHANGING ct_data = lt_data ).
" Sortierung DATA(lt_sort) = io_request->get_sort_elements( ). IF lt_sort IS NOT INITIAL. DATA(lv_sort_str) = ``. LOOP AT lt_sort INTO DATA(ls_sort). IF lv_sort_str IS NOT INITIAL. lv_sort_str = lv_sort_str && ` `. ENDIF. lv_sort_str = lv_sort_str && ls_sort-element_name. IF ls_sort-descending = abap_true. lv_sort_str = lv_sort_str && ` DESCENDING`. ELSE. lv_sort_str = lv_sort_str && ` ASCENDING`. ENDIF. ENDLOOP. SORT lt_data BY (lv_sort_str). ELSE. SORT lt_data BY Revenue DESCENDING. ENDIF.
" Total Count vor Paging DATA(lv_count) = lines( lt_data ).
" Paging DATA(lv_offset) = io_request->get_paging( )->get_offset( ). DATA(lv_size) = io_request->get_paging( )->get_page_size( ).
IF lv_size > 0. DATA lt_paged LIKE lt_data. LOOP AT lt_data INTO DATA(ls_row) FROM lv_offset + 1 TO lv_offset + lv_size. APPEND ls_row TO lt_paged. ENDLOOP. io_response->set_data( lt_paged ). ELSE. io_response->set_data( lt_data ). ENDIF. ENDIF.
IF io_request->is_total_numb_of_rec_requested( ). io_response->set_total_number_of_records( lv_count ). ENDIF. ENDMETHOD.
METHOD load_aggregated_data. " Hauptaggregation mit GROUP BY SELECT vbrk~kunag AS Customer, vbrk~gjahr AS FiscalYear, vbrk~fkdat+4(2) AS FiscalPeriod, kna1~name1 AS CustomerName, kna1~regio AS Region, SUM( vbrp~netwr ) AS Revenue, SUM( vbrp~fkimg ) AS Quantity, COUNT( DISTINCT vbrk~vbeln ) AS OrderCount, AVG( vbrp~netwr ) AS AvgOrderValue, vbrk~waerk AS Currency FROM i_billingdocument AS vbrk INNER JOIN i_billingdocumentitem AS vbrp ON vbrp~billingdocument = vbrk~billingdocument INNER JOIN i_customer AS kna1 ON kna1~customer = vbrk~soldtoparty WHERE vbrk~fkart IN ( 'F2', 'L2' ) " Rechnungen und Gutschriften GROUP BY vbrk~kunag, vbrk~gjahr, vbrk~fkdat+4(2), kna1~name1, kna1~regio, vbrk~waerk INTO TABLE @rt_data.
" Filter anwenden LOOP AT it_filter INTO DATA(ls_filter). CASE ls_filter-name. WHEN 'CUSTOMER'. DELETE rt_data WHERE Customer NOT IN ls_filter-range. WHEN 'FISCALYEAR'. DELETE rt_data WHERE FiscalYear NOT IN ls_filter-range. WHEN 'FISCALPERIOD'. DELETE rt_data WHERE FiscalPeriod NOT IN ls_filter-range. WHEN 'REGION'. DELETE rt_data WHERE Region NOT IN ls_filter-range. ENDCASE. ENDLOOP. ENDMETHOD.
METHOD calculate_kpis. " 1. Gesamtumsatz für RevenueShare berechnen DATA(lv_total_revenue) = REDUCE wertv8( INIT sum = CONV wertv8( 0 ) FOR ls_row IN ct_data NEXT sum = sum + ls_row-Revenue ).
" 2. KPIs pro Zeile berechnen LOOP AT ct_data ASSIGNING FIELD-SYMBOL(<ls_row>). " RevenueShare IF lv_total_revenue > 0. <ls_row>-RevenueShare = ( <ls_row>-Revenue / lv_total_revenue ) * 100. ENDIF.
" GrowthRate zum Vorjahresmonat <ls_row>-GrowthRate = calculate_growth_rate( iv_customer = <ls_row>-Customer iv_year = <ls_row>-FiscalYear iv_period = <ls_row>-FiscalPeriod iv_revenue = <ls_row>-Revenue ). ENDLOOP. ENDMETHOD.
METHOD calculate_growth_rate. " Vorjahresumsatz abrufen DATA(lv_prev_year) = iv_year - 1.
SELECT SINGLE SUM( vbrp~netwr ) AS Revenue FROM i_billingdocument AS vbrk INNER JOIN i_billingdocumentitem AS vbrp ON vbrp~billingdocument = vbrk~billingdocument WHERE vbrk~kunag = @iv_customer AND vbrk~gjahr = @lv_prev_year AND vbrk~fkdat+4(2) = @iv_period AND vbrk~fkart IN ( 'F2', 'L2' ) INTO @DATA(lv_prev_revenue).
IF lv_prev_revenue > 0. rv_rate = ( ( iv_revenue - lv_prev_revenue ) / lv_prev_revenue ) * 100. ELSEIF iv_revenue > 0. rv_rate = 100. " 100% Wachstum wenn Vorjahr = 0 ENDIF. ENDMETHOD.ENDCLASS.Tri des données agrégées
Pour les données agrégées, un tri correct est essentiel :
METHOD apply_aggregation_sorting. DATA(lt_sort) = io_request->get_sort_elements( ).
" Sortierung über mehrere Felder LOOP AT lt_sort INTO DATA(ls_sort). CASE ls_sort-element_name. WHEN 'TOTALREVENUE'. IF ls_sort-descending = abap_true. SORT ct_data BY TotalRevenue DESCENDING. ELSE. SORT ct_data BY TotalRevenue ASCENDING. ENDIF.
WHEN 'ORDERCOUNT'. IF ls_sort-descending = abap_true. SORT ct_data BY OrderCount DESCENDING TotalRevenue DESCENDING. ELSE. SORT ct_data BY OrderCount ASCENDING. ENDIF.
WHEN 'AVGORDERVALUE'. IF ls_sort-descending = abap_true. SORT ct_data BY AvgOrderValue DESCENDING. ELSE. SORT ct_data BY AvgOrderValue ASCENDING. ENDIF.
WHEN 'CUSTOMERNAME'. IF ls_sort-descending = abap_true. SORT ct_data BY CustomerName DESCENDING. ELSE. SORT ct_data BY CustomerName ASCENDING. ENDIF. ENDCASE. ENDLOOP.
" Default-Sortierung wenn nichts angegeben IF lt_sort IS INITIAL. SORT ct_data BY TotalRevenue DESCENDING. ENDIF.ENDMETHOD.Pagination des résultats agrégés
La pagination doit avoir lieu après l’agrégation, mais avant la sortie :
┌─────────────────────────────────────────────────┐│ 1. SELECT avec GROUP BY ││ → Données brutes agrégées (ex. 500 lignes) │├─────────────────────────────────────────────────┤│ 2. Calcul des KPI ││ → RevenueShare, GrowthRate, etc. │├─────────────────────────────────────────────────┤│ 3. Filtres sur champs agrégés ││ → ex. TotalRevenue > 10000 │├─────────────────────────────────────────────────┤│ 4. Enregistrer le Total Count ││ → Pour infos de pagination (ex. 487 lignes) │├─────────────────────────────────────────────────┤│ 5. Tri ││ → Selon paramètres de requête │├─────────────────────────────────────────────────┤│ 6. Appliquer la pagination ││ → Retourner uniquement la page X de Y │└─────────────────────────────────────────────────┘METHOD if_rap_query_provider~select. " Schritt 1-3: Aggregation und Filter DATA(lt_data) = aggregate_and_filter( io_request ).
" Schritt 4: Total Count VOR Paging speichern DATA(lv_total_count) = lines( lt_data ).
" Schritt 5: Sortierung apply_sorting( EXPORTING it_sort = io_request->get_sort_elements( ) CHANGING ct_data = lt_data ).
" Schritt 6: Paging DATA(lv_offset) = io_request->get_paging( )->get_offset( ). DATA(lv_page_size) = io_request->get_paging( )->get_page_size( ).
IF lv_page_size > 0. DATA lt_paged LIKE lt_data. DATA(lv_from) = lv_offset + 1. DATA(lv_to) = lv_offset + lv_page_size.
LOOP AT lt_data INTO DATA(ls_row) FROM lv_from TO lv_to. APPEND ls_row TO lt_paged. ENDLOOP.
io_response->set_data( lt_paged ). ELSE. io_response->set_data( lt_data ). ENDIF.
" Total Count für Pagination-Controls IF io_request->is_total_numb_of_rec_requested( ). io_response->set_total_number_of_records( lv_total_count ). ENDIF.ENDMETHOD.Filtres sur les champs agrégés
Parfois, on souhaite filtrer par valeurs agrégées (ex. “uniquement les clients avec CA > 10000”) :
METHOD apply_aggregation_filter. " Standard-Filter (vor Aggregation) LOOP AT it_filter INTO DATA(ls_filter). CASE ls_filter-name. " Filter auf Grunddaten - in WHERE-Clause WHEN 'CUSTOMER' OR 'FISCALYEAR' OR 'REGION'. " Diese Filter bereits in der SQL-Abfrage
" Filter auf aggregierte Werte - nachträglich WHEN 'TOTALREVENUE'. DELETE ct_data WHERE TotalRevenue NOT IN ls_filter-range.
WHEN 'ORDERCOUNT'. DELETE ct_data WHERE OrderCount NOT IN ls_filter-range.
WHEN 'AVGORDERVALUE'. DELETE ct_data WHERE AvgOrderValue NOT IN ls_filter-range. ENDCASE. ENDLOOP.ENDMETHOD.Logique équivalente à HAVING
Pour les filtres d’agrégation complexes, utilisez une logique équivalente à HAVING :
METHOD aggregate_with_having. " Aggregation durchführen SELECT kunag AS Customer, gjahr AS FiscalYear, SUM( netwr ) AS TotalRevenue, COUNT( * ) AS OrderCount, AVG( netwr ) AS AvgOrderValue FROM i_billingdocument GROUP BY kunag, gjahr HAVING SUM( netwr ) > 10000 " Mindest-Umsatz AND COUNT( * ) >= 5 " Mindest-Bestellungen INTO TABLE @rt_data.ENDMETHOD.Exposition du service
La Custom Entity est exposée comme service OData comme d’habitude :
@EndUserText.label: 'Umsatz-Reporting Service"define service ZUI_RevenueReport { expose ZI_CustomerRevenue as CustomerRevenue; expose ZI_SalesStatistics as SalesStatistics;}Conseils de performance pour les Queries d’agrégation
| Conseil | Description |
|---|---|
| Utiliser les index | Les champs GROUP BY doivent être indexés |
| Appliquer les filtres tôt | WHERE avant GROUP BY réduit le volume de données |
| Limiter la pagination | Max. 500 lignes par page |
| Mise en cache | Mettre en cache les agrégations fréquemment utilisées |
| Asynchrone | Rapports longs en tant que Background Job |
" Beispiel: Caching für häufige AggregationenCLASS-DATA: gt_cache TYPE tt_statistics, gv_cache_key TYPE string, gv_cache_time TYPE timestamp.
METHOD get_cached_or_fresh. " Cache-Key aus Filter bauen DATA(lv_key) = build_cache_key( it_filter ).
" Cache gültig? (5 Minuten) DATA(lv_now) = utclong_current( ). DATA(lv_age) = cl_abap_tstmp=>subtract( tstmp1 = lv_now tstmp2 = gv_cache_time ).
IF lv_key = gv_cache_key AND lv_age < 300. rt_data = gt_cache. ELSE. rt_data = aggregate_fresh( it_filter ). gt_cache = rt_data. gv_cache_key = lv_key. gv_cache_time = lv_now. ENDIF.ENDMETHOD.Bonnes pratiques
- Agrégation dans la base de données - SELECT avec GROUP BY au lieu de LOOP AT avec COLLECT
- Appliquer les filtres tôt - La clause WHERE réduit le volume de données avant le groupement
- Utiliser HAVING - Pour les filtres sur valeurs agrégées
- Total Count séparé - Enregistrer avant pagination pour une pagination correcte
- Tri flexible - Permettre un tri dynamique sur tous les champs
- Calculer les KPI - RevenueShare, GrowthRate après agrégation
- Utiliser la mise en cache - Mettre en cache les rapports fréquemment consultés
- Gestion des erreurs - TRY-CATCH pour la conversion des filtres
Résumé
Les Custom Queries avec agrégation permettent un reporting performant dans RAP :
- GROUP BY dans ABAP SQL pour une agrégation efficace
- SUM, COUNT, AVG, MIN, MAX comme fonctions d’agrégation
- Pagination après agrégation pour de grands volumes de résultats
- Tri dynamique sur tous les champs
- Calcul des KPI après l’agrégation de base
- Filtres sur valeurs agrégées pour des analyses flexibles
Sujets avancés
- RAP Custom Entities - Bases des sources de données externes
- ABAP SQL Window Functions - Fonctions analytiques
- ABAP SQL Nouveautés 2024/2025 - GROUPING SETS et plus
- Custom Analytical Queries - Intégration SAP Analytics Cloud