
RAP - Report Pattern
Wie ist das Report Pattern in RAP aufgebaut und was kannst du damit machen? Mehr dazu in diesem ABAP Artikel.
Inhaltsverzeichnis
In diesem Artikel werden wir auf das Report Pattern eingehen und wie du es für dich am besten nutzen kannst. Dabei gehen wir auf die unterschiedlichen Schichten und Möglichkeiten ein.
Einleitung
Das ABAP RESTful Programming Model ist das neue Modell in ABAP, um Cloud Ready und Clean Core Anwendungen zu erstellen. Mit RAP lassen sich neben Anwendungen, auch Schnittstellen für den internen und externen Gebrauch zur Verfügung stellen. Mit den neusten Features ist RAP sehr flexibel was den Aufbau und die Nutzung angeht, weshalb wir die Anwendungen in verschiedene Pattern aufteilen möchten.
Aufbau
Das "Report Pattern" zeichnet sich dadurch aus, dass es dem klassischen Report sehr nah ist. Die Datenquelle der RAP Anwendung ist ein Standard Core Data Service. Dabei verwenden wir zum Speichern der Daten einen "Unmanaged Save", sind aber frei in der Anbindung von zusätzlichen Entitäten und Informationen. Bevor wir mit dem Aufbau starten, hier noch die allgemeine Legende:
Dazu die folgenden Merkmale zur Abgrenzung:
- Datenquelle ist ein Core Data Service (Standard, Custom)
- Daten sollen angezeigt, aber nicht geändert werden
- Zusätzliche Daten per Assoziation (zusätzliche Entität, zusätzliche Felder)
- Unmanaged Save zum Speichern der Informationen
Beispiel
In den folgenden Abschnitten erstellen wir eine Reporting Anwendung und erweitern diese Stück für Stück um verschiedene Anforderungen. Die Anwendung soll uns die aktuellen Währungen im System anzeigen. Dazu möchten wir noch eigene Informationen speichern, wie eine Dokumentation zur Währung, ein Bild und zusätzliche Informationen, in welchen Ländern unser Unternehmen die Währung verwendet.
Daten
Im ersten Schritt bauen wir dazu unser zusätzliches Datenmodell auf und erstellen die verschiedenen Layer des RAP Objekts (Interface und Consumption).
Tabellen
Im ersten Schritt legen wir eine Tabelle für Zusatzinformationen, an die wir an der Währung speichern wollen. Da wir nicht direkt die Währungstabelle und den Core Data Service erweitern wollen, legen wir die Tabelle im Kundennamensraum an.
@EndUserText.label : 'Additional Currency Informations'
define table zbs_drp_addcurr {
key client : abap.clnt not null;
key currency : waers not null;
ccomment : abap.char(60);
documentation : abap.string(0);
picture_url : abap.string(0);
last_editor : abap.char(12);
}
Dazu benötigen wir noch eine Entität unter der Währung, wo wir Zusatzinformationen zu den Ländern hinterlegen wollen. Die Tabelle sieht deshalb wie folgt aus:
@EndUserText.label : 'Country Assignment'
define table zbs_drp_country {
key client : abap.clnt not null;
key currency : waers not null;
key country : land1 not null;
ranking : abap.int2;
}
Basis
Da wir später mit Core Data Services arbeiten wollen, legen wir über den beiden Tabellen einen Basis Layer an, der die Tabelle als einfachen Views zur Verfügung stellt. Hier haben wir auch noch einmal die Möglichkeit, die Felder für unser Datenmodell zu normalisieren.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Additional Informations'
define view entity ZBS_B_DRPAdditionalCurrency
as select from zbs_drp_addcurr
{
key currency as Currency,
ccomment as CurrencyComment,
documentation as Documentation,
picture_url as PictureURL,
last_editor as LastEditor
}
Der Basis View für die Länder Entität sieht nun wie folgt aus:
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Country Assignment'
define view entity ZBS_B_DRPCurrencyCountry
as select from zbs_drp_country
{
key currency as Currency,
key country as Country,
ranking as CountryRanking
}
Hinweis: Der Basis-Layer ist optional, dient in diesem Fall aber dazu, die Felder der Tabelle sauber für die weitere Verarbeitung zur Verfügung zu stellen.
Interface
Im Interface Layer modellieren wir nun die Daten, die wir in unserem Modell für die Anzeige später benötigen und stellen schon einmal alle Felder für den Consumption Layer zur Verfügung. Im Root View stellen wir die Beziehung zu den Ländern her und übernehmen alle Zusatzdaten, die wir später für die Erweiterung benötigen.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Currency Overview'
define root view entity ZBS_R_DRPCurrency
as select from I_Currency
composition of many ZBS_I_DRPCurrencyCountry as _Country
association of one to one ZBS_B_DRPAdditionalCurrency as _Data on _Data.Currency = $projection.Currency
association of one to one I_BusinessUserVH as _User on _User.UserID = $projection.lasteditor
{
key Currency,
Decimals,
CurrencyISOCode,
AlternativeCurrencyKey,
_Text[ Language = $session.system_language ].CurrencyName,
_Text[ Language = $session.system_language ].CurrencyShortName,
_Data.CurrencyComment,
_Data.Documentation,
_Data.PictureURL,
_Data.LastEditor,
_Country,
_User
}
Der View für die Länderzuordnung stellt die Beziehung zu der Root Entität her und stellt bereits alle Informationen, wie zum Beispiel den Namen, für den Consumption View zur Verfügung.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Country Assignment'
define view entity ZBS_I_DRPCurrencyCountry
as select from ZBS_B_DRPCurrencyCountry
association to parent ZBS_R_DRPCurrency as _Currency on _Currency.Currency = $projection.Currency
association of one to one I_Country as _Country on _Country.Country = $projection.Country
{
key Currency,
key Country,
_Country._Text[ Language = $session.system_language ].CountryName,
CountryRanking,
_Currency
}
Projektion
Im nächsten Schritt erzeugen wir die Projektionsschicht für die Währung. Hier wirst du bereits auch eine allgemeine Suche finden über den Schlüssel und die Textfelder (@Search). Zusätzlich lesen wir Felder, die Wir nur im Consumption Layer anzeigen wollen, wie den Namen des letzten Änderers.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Consumption for Currency'
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZBS_C_DRPCurrency
provider contract transactional_query
as projection on ZBS_R_DRPCurrency
{
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 1.0
@Search.ranking: #HIGH
key Currency,
Decimals,
CurrencyISOCode,
AlternativeCurrencyKey,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.7
@Search.ranking: #MEDIUM
CurrencyName,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@Search.ranking: #MEDIUM
CurrencyShortName,
CurrencyComment,
Documentation,
PictureURL,
LastEditor,
_User.PersonFullName as EditorName,
_Country : redirected to composition child ZBS_C_DRPCurrencyCountry
}
Für die Zuordnung der Länder binden wir bereits die Standardsuchhilfe an. Du solltest auch sicherstellen, dass für den nächsten Schritt die Annotation "@Metadata.allowExtensions: true" gesetzt ist, um die Metadaten zu erzeugen und damit dem UI ein Aussehen zu geben.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Consumption for Country Assignment'
@Metadata.allowExtensions: true
define view entity ZBS_C_DRPCurrencyCountry
as projection on ZBS_I_DRPCurrencyCountry
{
key Currency,
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_CountryVH', element: 'Country' } }]
key Country,
CountryName,
CountryRanking,
_Currency : redirected to parent ZBS_C_DRPCurrency
}
Service und UI
Damit wir nun aus den Daten den ersten Service und eine erste UI zu sehen bekommen, können wir nun unser RAP Objekt nach außen freigeben. Im ersten Schritt geben wir dazu den Service mit einem Binding nach außen frei, im zweiten Schritt erzeugen wir Metadaten, um die Elemente auf dem Bild zuzuordnen.
Service
Dazu legen wir auf unserem Consumption View über das Kontextmenü eine Service Definition an. In diesem Fall können wir auch die "LeadingEntity" definieren, die Annotation sorgt für eine Markierung im Service Binding.
@EndUserText.label: 'Currency Service'
@ObjectModel.leadingEntity.name: 'ZBS_C_DRPCurrency'
define service ZBS_DEMO_DRP_CURRENCY {
expose ZBS_C_DRPCurrency as Currency;
expose ZBS_C_DRPCurrencyCountry as Country;
}
Zum Abschluss legen wir noch ein Service Binding an. In diesem Fall generieren wir einen OData v4 Service als UI, da wir hier unsere App erzeugen wollen.
Zum Abschluss nicht vergessen auf den "Publish" Button zu drücken, damit der Service angelegt wird und von außen aufgerufen werden kann.
Metadaten
Solltest du bereits den Preview deiner Anwendung aufgerufen haben, erhältst du dort ein leeres Bild, aber immerhin mit einer Liste. Über das Zahnrad und die Anpassung der Filter kannst du dir bereits Elemente einblenden und somit die ersten Daten ansehen. Damit wir aber als Default gleich unser aufgeräumtes UI finden, erzeugen wir noch Metadata Extensions für unsere Consumption Views.
@Metadata.layer: #CUSTOMER
@UI: {
headerInfo: {
typeName: 'Currency',
typeNamePlural: 'Currencies',
title: { value: 'Currency' },
description: { value: 'CurrencyName' },
imageUrl: 'PictureURL'
}
}
annotate entity ZBS_C_DRPCurrency with
{
@UI.facet : [
{
id : 'idTechFields',
label : 'Technical Details',
position : 10,
type : #IDENTIFICATION_REFERENCE,
targetQualifier: 'TECH'
},
{
id : 'idAddFields',
label : 'Additional Info',
position : 20,
type : #IDENTIFICATION_REFERENCE,
targetQualifier: 'ADD'
},
{
id : 'idDocumentation',
label : 'Documentation',
position : 30,
type : #IDENTIFICATION_REFERENCE,
targetQualifier: 'DOCU'
},
{
id : 'idChanges',
label : 'Changes',
position : 40,
type : #IDENTIFICATION_REFERENCE,
targetQualifier: 'CHANGE'
},
{
id : 'idCountryTable',
label : 'Country Assignment',
position : 50,
type : #LINEITEM_REFERENCE,
targetElement: '_Country'
}
]
@UI:{
lineItem: [{ position: 10 }],
selectionField: [{ position: 10 }]
}
Currency;
@UI:{
lineItem: [{ position: 20 }]
}
CurrencyName;
@UI:{
lineItem: [{ position: 30 }]
}
CurrencyShortName;
@UI:{
identification: [{ position: 10, qualifier: 'TECH' }]
}
CurrencyISOCode;
@UI:{
identification: [{ position: 20, qualifier: 'TECH' }]
}
Decimals;
@UI:{
identification: [{ position: 30, qualifier: 'TECH' }]
}
AlternativeCurrencyKey;
@UI:{
identification: [{ position: 40, qualifier: 'ADD' }]
}
@UI.multiLineText: true
@EndUserText.label: 'Comment'
CurrencyComment;
@UI:{
identification: [{ position: 50, qualifier: 'DOCU' }]
}
@UI.multiLineText: true
@EndUserText.label: 'Informations'
Documentation;
@UI:{
identification: [{ position: 60, qualifier: 'ADD' }]
}
@EndUserText.label: 'Currency Image'
PictureURL;
@UI:{
identification: [{ position: 70, qualifier: 'CHANGE' }]
}
@EndUserText.label: 'Last Changer (ID)'
LastEditor;
@UI:{
lineItem: [{ position: 40 }],
identification: [{ position: 80, qualifier: 'CHANGE' }]
}
@EndUserText.label: 'Last Changer'
EditorName;
}
Hier vergeben wir Texte für unsere eingebauten Typen, platzieren die Elemente auf dem Bild, bilden Gruppen und Filter. Die Zuordnung der Länder ist recht überschaubar, da wir die Informationen nur im Detailbild anzeigen wollen und die Objektseite nicht benötigen. Die Währung können wir ausblenden, da die Information nur doppelt wäre.
@Metadata.layer: #CUSTOMER
annotate entity ZBS_C_DRPCurrencyCountry with
{
@UI.hidden: true
Currency;
@UI:{
lineItem: [{ position: 10 }]
}
Country;
@UI:{
lineItem: [{ position: 20 }]
}
CountryName;
@UI:{
lineItem: [{ position: 30 }]
}
CountryRanking;
}
Preview
Da die Anwendung nun auch ein Bild hat, können wir einen ersten Preview der Anwendung wagen. Die Liste für die Auswertung steht nun soweit zur Verfügung. Auf der Einstiegsseite erhalten wir alle nötigen Informationen zur Währung.
Die Detailseite ist nun auch entsprechend aufgeräumt und bietet zusätzliche Informationen zur Währung, die aus unserer eigenen Tabelle kommen. Damit haben wir den Standard um zusätzliche Felder "erweitert", die wir nun auch im nächsten Abschnitt änderbar machen wollen.
Verhalten
Damit die App nun interaktiv und änderbar wird, müssen wir noch ein Verhalten implementieren. Bisher handelt es sich um eine reine Anzeige-App, womit der Anwender aber auch bereits arbeiten kann, um an die nötigen Informationen zu kommen.
Draft
Wenn wir die Standardoperationen verwenden wollen, müssen wir zusätzlich noch für den OData v4 Service das Draft Handling aktivieren. Dazu müssen wir in unserer Haupttabelle noch die folgenden Felder ergänzen:
local_last_changed : abp_locinst_lastchange_tstmpl;
last_changed : abp_lastchange_tstmpl;
Die Felder müssen wir dann bis auf Interface Ebene veröffentlichen, damit wir sie in der Verhaltensdefinition nutzen können. Dazu benötigen wir für jeden View noch eine Draft Tabelle, die dann alle Felder des Views puffert. Für die Währung wäre das damit:
@EndUserText.label : 'Draft für Currency'
define table zbs_drp_currd {
key client : abap.clnt not null;
key currency : waers not null;
decimals : abap.int1;
currencyisocode : abap.char(3);
alternativecurrencykey : abap.char(3);
currencyname : abap.char(40);
currencyshortname : abap.char(15);
currencycomment : abap.char(60);
documentation : abap.string(0);
pictureurl : abap.string(0);
lasteditor : abap.char(12);
locallastchanged : abp_locinst_lastchange_tstmpl;
lastchanged : abp_lastchange_tstmpl;
"%admin" : include sych_bdl_draft_admin_inc;
}
Für die Länderzuordnung würde die Draft Tabelle entsprechend so aussehen.
@EndUserText.label : 'Draft for Country'
define table zbs_drp_cound {
key client : abap.clnt not null;
key currency : waers not null;
key country : land1 not null;
countryname : abap.char(50);
countryranking : abap.int2;
"%admin" : include sych_bdl_draft_admin_inc;
}
Verhaltensdefinition
Nun können wir das entsprechende Verhalten implementieren. Da wir die normale UPDATE Aktion von RAP verwenden wollen, müssen wir Draft implementieren. Dazu setzen wir CREATE und DELETE auf Internal, da wir diese Aktionen dem Anwender nicht anbieten wollen, zumindest für die Root Entität. Dabei verwenden wir einen Unmanaged Save, um das Speichern der Daten selbst zu übernehmen. Managed hilft uns dabei, die Daten im Draft und im Transaktionalen Puffer zu verwalten, damit müssen wir uns nicht darum kümmern. Die definierten Timestamps benötigen wir für die ETAGs. Alle Felder, die aus dem Standard kommen, setzen wir auf READONLY, damit der Anwender nicht denkt, dass er diese auch ändern könnte über die Anwendung.
managed implementation in class zbp_bs_drp_currency unique;
strict ( 2 );
with draft;
define behavior for ZBS_R_DRPCurrency alias Currency
with unmanaged save
draft table zbs_drp_currd
etag master LocalLastChanged
lock master total etag LastChanged
authorization master ( instance )
{
internal create;
update;
internal delete;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
field ( readonly )
Currency,
Decimals,
CurrencyISOCode,
AlternativeCurrencyKey,
CurrencyName,
CurrencyShortName,
LastEditor;
mapping for zbs_drp_addcurr
{
Currency = currency;
CurrencyComment = ccomment;
Documentation = documentation;
PictureURL = picture_url;
LastEditor = last_editor;
LocalLastChanged = local_last_changed;
LastChanged = last_changed;
}
association _Country { create; with draft; }
}
define behavior for ZBS_I_DRPCurrencyCountry alias Country
with unmanaged save
draft table zbs_drp_cound
lock dependent by _Currency
authorization dependent by _Currency
{
update;
delete;
field ( readonly )
Currency,
CountryName;
field ( readonly : update )
Country;
mapping for zbs_drp_country
{
Currency = currency;
Country = country;
CountryRanking = ranking;
}
association _Currency { with draft; }
}
In der Projektion geben wir nun die Aktionen und Möglichkeiten nach Außen, die der Anwender zum Arbeiten mit unserem Objekt benötigt. Die Draft Aktionen (Edit, Activate, Discard, Resume und Prepare) sind dabei verpflichtend, damit der Draft verwendet werden kann.
projection;
strict ( 2 );
use draft;
define behavior for ZBS_C_DRPCurrency alias Currency
{
use update;
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
use association _Country { create; with draft; }
}
define behavior for ZBS_C_DRPCurrencyCountry alias Country
{
use update;
use delete;
use association _Currency { with draft; }
}
Verhaltensimplementierung
In der Verhaltensimplementierung kümmern wir uns nun um das Speichern der geänderten Datensätze in der Methode SAVE_MODIFIED. Dabei kommt uns zugute, dass wir in der Verhaltensdefinition das Mapping konfiguriert haben. Mit dem Mapping können wir beim CORRESPONDING den Zusatz MAPPING FROM ENTITY nutzen, um so die normalisierten Felder der Entität auf die Datenbankstruktur zu mappen. Du solltest allerdings auch beim UPDATE mit der %CONTROL Struktur arbeiten, da sonst Felder auf der Datenbank ausversehen gelöscht werden können.
CLASS lhc_Currency DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR Currency RESULT result.
ENDCLASS.
CLASS lhc_Currency IMPLEMENTATION.
METHOD get_instance_authorizations.
ENDMETHOD.
ENDCLASS.
CLASS lsc_ZBS_R_DRPCURRENCY DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS
save_modified REDEFINITION.
METHODS
cleanup_finalize REDEFINITION.
ENDCLASS.
CLASS lsc_ZBS_R_DRPCURRENCY IMPLEMENTATION.
METHOD save_modified.
LOOP AT update-currency INTO DATA(ls_new_currency).
ls_new_currency-LastEditor = cl_abap_context_info=>get_user_technical_name( ).
INSERT zbs_drp_addcurr FROM @ls_new_currency MAPPING FROM ENTITY.
IF sy-subrc <> 0.
UPDATE zbs_drp_addcurr FROM @ls_new_currency INDICATORS SET STRUCTURE %control MAPPING FROM ENTITY.
ENDIF.
ENDLOOP.
LOOP AT create-country INTO DATA(ls_create_country).
INSERT zbs_drp_country FROM @ls_create_country MAPPING FROM ENTITY.
ENDLOOP.
LOOP AT update-country INTO DATA(ls_update_country).
UPDATE zbs_drp_country FROM @ls_update_country INDICATORS SET STRUCTURE %control MAPPING FROM ENTITY.
ENDLOOP.
LOOP AT delete-country INTO DATA(ls_delete_country).
DELETE zbs_drp_country FROM @( CORRESPONDING zbs_drp_country( ls_delete_country MAPPING FROM ENTITY ) ).
ENDLOOP.
ENDMETHOD.
METHOD cleanup_finalize.
ENDMETHOD.
ENDCLASS.
Bei der Währung müssen wir uns nur um den UPDATE Fall kümmern, da die Datensätze von der Datenbank kommen. Dafür müssen wir allerdings den MODIFY verwenden, da wir nicht wissen, ob der Datensatz bereits gespeichert wurde.
Zusammenfassung
Das "Report Pattern" kann in mehreren Schritten implementiert werden, je nach Anforderung an den Report. Als Grundlage benötigst du eigentlich immer:
- Datenmodell
- Service
- Metadaten
- Berechtigung (Access Control)
Damit kannst du die Daten bereits im UI anzeigen und der User kann damit interagieren. Dies würde auch einem reinen Anzeige-ALV entsprechen. Zum Schluss solltest du ebenfalls nicht vergessen, die verschiedenen Core Data Services mit Berechtigungen (Access Control) auszustatten. Da unsere Standard Entitäten keine Berechtigungsprüfungen haben, haben wir ebenfalls keine implementiert. Möchtest du noch Änderungen vornehmen, benötigst du weitere Bestandteile:
- Draft (wenn benötigt)
- Verhaltensdefinition
- Verhaltensimplementierung
Die letzten Bestandteile entsprechen schon sehr stark einem Report mit ändernden Funktionalitäten. Hier erhältst du auch Mehrwerte zur Erweiterung der Standard-Entitäten, ohne diese zwingend modifizieren zu müssen. Das Endergebnis sieht nun wie folgt aus.
In der App ist auch noch das Feature versteckt, dass wir über das Feld "Bild URL", das Bild im Header der Objektseite dynamisch anpassen können. Zusätzlich können wir über den "Edit" Button alle unsere eigenen Felder anpassen und auf der Datenbank speichern.
Vollständiges Beispiel
Da das Beispiel recht komplex ist und viele Objekte enthält, findest du alle erstellten Objekte in einem Commit in unserem GitHub Repository zum Thema ABAP RESTful Programming Model. Oder du schaust dir das Paket ZBS_DEMO_RAP_PATTERN_REPORT im Repository an für die vollständige Anwendung. Damit kannst du die verschiedenen Objekte noch einmal abgleichen, die wir nach der Änderung nicht vollständig aufgelistet haben.
Fazit
Mit dem Report Pattern kannst du einfach Anzeigereports umsetzen, aber auch komplexe Änderungstransaktionen erstellen, um Kundenerweiterungen zu erzeugen, die Zusatzdaten verarbeiten.