Projection Views bilden die oberste Schicht der RAP-Architektur und ermöglichen es, ein einzelnes Business Object für verschiedene Anwendungsfälle zu exponieren. Mit Projections kannst du unterschiedliche UIs, Berechtigungen und Verhaltensweisen auf demselben Datenmodell aufbauen.
Das Projection Layer Konzept
In RAP folgt die Architektur dem Layered Architecture Pattern:
┌─────────────────────────────────────────────────────────┐│ Service Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Service A │ │ Service B │ │ Service C │ ││ │ (Fiori App) │ │ (API) │ │ (Analytics) │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │├─────────┼────────────────┼────────────────┼─────────────┤│ │ Projection Layer (C_*) │ ││ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ ││ │ ZC_Travel │ │ ZC_TravelAPI│ │ ZC_TravelRpt│ ││ │ (UI-fokus) │ │ (minimal) │ │ (readonly) │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │├─────────┼────────────────┼────────────────┼─────────────┤│ │ Business Object Layer (I_*) │ ││ │ ┌────────▼────────┐ │ ││ └───────► ZI_Travel ◄───────┘ ││ │ (Kernlogik) │ ││ └─────────────────┘ │├─────────────────────────────────────────────────────────┤│ Database Layer │└─────────────────────────────────────────────────────────┘Namenskonventionen
| Präfix | Schicht | Beschreibung |
|---|---|---|
| I_ oder ZI_ | Interface | Business Object mit vollständiger Logik |
| C_ oder ZC_ | Consumption | Projection für spezifischen Anwendungsfall |
| R_ oder ZR_ | Root | Alternative für Interface-Views |
Warum Projection Views?
Anwendungsfälle für Multi-UI-Szenarien
-
Unterschiedliche Benutzergruppen
- Sachbearbeiter: Voller Zugriff auf alle Felder
- Manager: Nur Genehmigung und Übersicht
- Externe Partner: Eingeschränkte Sicht
-
Unterschiedliche Geräte
- Desktop: Detailreiche Fiori App
- Mobile: Vereinfachte Ansicht
-
Unterschiedliche Verwendungszwecke
- Transaktionale App: CRUD-Operationen
- Reporting: Nur Lesezugriff
- API: Technische Schnittstelle
Projection CDS View erstellen
Interface View (Basis)
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Travel - Interface View'define root view entity ZI_Travel as select from ztravel composition [0..*] of ZI_Booking as _Booking association [1..1] to ZI_Agency as _Agency on $projection.AgencyId = _Agency.AgencyId association [1..1] to ZI_Customer as _Customer on $projection.CustomerId = _Customer.CustomerId{ key travel_id as TravelId, agency_id as AgencyId, customer_id as CustomerId, begin_date as BeginDate, end_date as EndDate, @Semantics.amount.currencyCode: 'CurrencyCode' total_price as TotalPrice, currency_code as CurrencyCode, description as Description, status as Status,
@Semantics.user.createdBy: true created_by as CreatedBy, @Semantics.systemDateTime.createdAt: true created_at as CreatedAt, @Semantics.user.lastChangedBy: true last_changed_by as LastChangedBy, @Semantics.systemDateTime.lastChangedAt: true last_changed_at as LastChangedAt,
// Associations _Booking, _Agency, _Customer}Projection View für Fiori UI
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Travel - Fiori App'@Metadata.allowExtensions: truedefine root view entity ZC_Travel provider contract transactional_query as projection on ZI_Travel{ key TravelId, AgencyId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, Description, Status,
// Berechnete Felder nur für diese Projection case Status when 'A' then 'Akzeptiert' when 'X' then 'Abgelehnt' when 'O' then 'Offen' else 'Unbekannt' end as StatusText,
CreatedBy, CreatedAt, LastChangedBy, LastChangedAt,
// Associations exponieren _Booking : redirected to composition child ZC_Booking, _Agency, _Customer}Projection View für API-Konsumenten
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Travel - API'define root view entity ZC_TravelAPI provider contract transactional_query as projection on ZI_Travel{ key TravelId, AgencyId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, Status,
// Keine UI-spezifischen Felder // Keine berechneten Felder für Status-Text
LastChangedAt,
_Booking : redirected to composition child ZC_BookingAPI}Projection View für Reporting (Read-Only)
@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Travel - Reporting'@ObjectModel.usageType: { serviceQuality: #A, sizeCategory: #L, dataClass: #TRANSACTIONAL}define root view entity ZC_TravelReport provider contract transactional_query as projection on ZI_Travel{ key TravelId, AgencyId, _Agency.Name as AgencyName, CustomerId, _Customer.LastName as CustomerName, BeginDate, EndDate,
// Dauer berechnen dats_days_between( BeginDate, EndDate ) as TravelDuration,
TotalPrice, CurrencyCode, Status, CreatedAt,
// Für Aggregationen @DefaultAggregation: #SUM TotalPrice as SumTotalPrice,
@DefaultAggregation: #COUNT cast( 1 as abap.int4 ) as TravelCount}Behavior Projection
Jede Projection View benötigt eine eigene Behavior Projection, die das Verhalten des Business Objects für diesen Anwendungsfall anpasst.
Interface Behavior Definition (Basis)
managed implementation in class zbp_i_travel unique;strict ( 2 );
define behavior for ZI_Travel alias Travelpersistent table ztravellock masterauthorization master ( instance )etag master LastChangedAt{ create; update; delete;
field ( readonly ) TravelId; field ( mandatory ) AgencyId, CustomerId, BeginDate, EndDate; field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
action acceptTravel result [1] $self; action rejectTravel result [1] $self;
determination setTravelId on save { create; } validation validateDates on save { create; update; } validation validateStatus on save { create; update; }
association _Booking { create; }
mapping for ztravel { TravelId = travel_id; AgencyId = agency_id; CustomerId = customer_id; BeginDate = begin_date; EndDate = end_date; TotalPrice = total_price; CurrencyCode = currency_code; Description = description; Status = status; CreatedBy = created_by; CreatedAt = created_at; LastChangedBy = last_changed_by; LastChangedAt = last_changed_at; }}
define behavior for ZI_Booking alias Bookingpersistent table zbookinglock dependent by _Travelauthorization dependent by _Traveletag master LastChangedAt{ update; delete;
field ( readonly ) BookingId, TravelId;
association _Travel;}Behavior Projection für Fiori App
Die Projection exponiert alle Operationen für die Fiori-Anwendung:
projection;strict ( 2 );
define behavior for ZC_Travel alias Travel{ use create; use update; use delete;
use action acceptTravel; use action rejectTravel;
use association _Booking { create; }}
define behavior for ZC_Booking alias Booking{ use update; use delete;
use association _Travel;}Behavior Projection für API (eingeschränkt)
Die API-Projection exponiert nur ausgewählte Operationen:
projection;strict ( 2 );
define behavior for ZC_TravelAPI alias Travel{ use create; use update; // Kein delete - API darf nicht löschen
use action acceptTravel; // Kein rejectTravel für API
use association _Booking { create; }}
define behavior for ZC_BookingAPI alias Booking{ use update; // Kein delete für Bookings über API
use association _Travel;}Behavior Projection für Reporting (Read-Only)
Das Reporting exponiert keine Änderungsoperationen:
projection;strict ( 2 );
define behavior for ZC_TravelReport alias Travel{ // Keine Operationen - reine Lesezugriffe // Read wird automatisch unterstützt}Metadata Extensions
Metadata Extensions ermöglichen unterschiedliche UI-Annotationen pro Projection, ohne die Projection View selbst zu ändern.
Metadata Extension für Fiori App
@Metadata.layer: #COREannotate entity ZC_Travel with{ @UI.facet: [ { id: 'TravelHeader', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'Reisedetails', position: 10 }, { id: 'Bookings', purpose: #STANDARD, type: #LINEITEM_REFERENCE, label: 'Buchungen', position: 20, targetElement: '_Booking' } ]
@UI: { headerInfo: { typeName: 'Reise', typeNamePlural: 'Reisen', title: { type: #STANDARD, value: 'Description' }, description: { type: #STANDARD, value: 'TravelId' } } }
@UI: { lineItem: [{ position: 10, importance: #HIGH }], identification: [{ position: 10 }], selectionField: [{ position: 10 }] } TravelId;
@UI: { lineItem: [{ position: 20, importance: #HIGH }], identification: [{ position: 20 }], selectionField: [{ position: 20 }] } @Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_Agency', element: 'AgencyId' } }] AgencyId;
@UI: { lineItem: [{ position: 30, importance: #HIGH }], identification: [{ position: 30 }], selectionField: [{ position: 30 }] } @Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_Customer', element: 'CustomerId' } }] CustomerId;
@UI: { lineItem: [{ position: 40, importance: #MEDIUM }], identification: [{ position: 40 }] } BeginDate;
@UI: { lineItem: [{ position: 50, importance: #MEDIUM }], identification: [{ position: 50 }] } EndDate;
@UI: { lineItem: [{ position: 60, importance: #HIGH, criticality: 'StatusCriticality' }], identification: [{ position: 60, criticality: 'StatusCriticality' }], selectionField: [{ position: 40 }] } Status;
@UI: { lineItem: [{ position: 70, importance: #HIGH }], identification: [{ position: 70 }] } TotalPrice;
@UI.identification: [ { type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Akzeptieren', position: 10 }, { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Ablehnen', position: 20 } ] StatusText;}Metadata Extension für Manager-View
@Metadata.layer: #COREannotate entity ZC_TravelManager with{ @UI.facet: [ { id: 'Overview', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'Übersicht', position: 10 } ]
@UI: { headerInfo: { typeName: 'Genehmigung', typeNamePlural: 'Genehmigungen', title: { type: #STANDARD, value: 'Description' } } }
// Nur relevante Felder für Manager @UI.lineItem: [{ position: 10, importance: #HIGH }] TravelId;
@UI.lineItem: [{ position: 20, importance: #HIGH }] CustomerName;
@UI.lineItem: [{ position: 30, importance: #HIGH }] TotalPrice;
@UI.lineItem: [{ position: 40, importance: #HIGH, criticality: 'StatusCriticality' }] Status;
// Prominente Aktions-Buttons @UI.lineItem: [ { type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Genehmigen', position: 50 }, { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Ablehnen', position: 60 } ] Description;}Feature Control pro Projection
Durch Dynamic Feature Control kannst du pro Projection unterschiedliche Regeln implementieren. Die Logik wird zentral in der Behavior Implementation definiert, aber über die Behavior Projection gesteuert.
Beispiel: Unterschiedliches Feature Control
Im Interface Behavior legst du die Features an:
define behavior for ZI_Travel alias Travel{ // ...
// Features für differenzierte Steuerung action ( features: instance ) acceptTravel result [1] $self; action ( features: instance ) rejectTravel result [1] $self; delete ( features: instance );
field ( features: instance ) Status;}Die Implementation prüft den Kontext:
CLASS zbp_i_travel IMPLEMENTATION.
METHOD get_instance_features. READ ENTITIES OF ZI_Travel IN LOCAL MODE ENTITY Travel FIELDS ( Status ) WITH CORRESPONDING #( keys ) RESULT DATA(travels).
LOOP AT travels INTO DATA(travel). APPEND VALUE #( %tky = travel-%tky
" Status-abhängige Features %action-acceptTravel = COND #( WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled )
%action-rejectTravel = COND #( WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled )
" Löschen nur für offene Reisen %delete = COND #( WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled )
" Status-Feld readonly wenn nicht offen %field-Status = COND #( WHEN travel-Status = 'O' THEN if_abap_behv=>fc-f-unrestricted ELSE if_abap_behv=>fc-f-read_only )
) TO result. ENDLOOP. ENDMETHOD.
ENDCLASS.In der API-Projection kannst du dann Features komplett weglassen oder überschreiben, indem du die Behavior Projection entsprechend anpasst.
Service Definition und Binding
Für jede Projection erstellst du einen eigenen Service:
Service Definition für Fiori App
@EndUserText.label: 'Travel - Fiori Service'define service ZUI_TRAVEL_O4 { expose ZC_Travel as Travel; expose ZC_Booking as Booking; expose ZI_Agency as Agency; expose ZI_Customer as Customer;}Service Definition für API
@EndUserText.label: 'Travel - API Service'define service ZAPI_TRAVEL_O4 { expose ZC_TravelAPI as Travel; expose ZC_BookingAPI as Booking;}Service Binding
Für jeden Service erstellst du ein Service Binding mit dem entsprechenden Protokoll:
| Binding | Protokoll | Verwendung |
|---|---|---|
| ZUI_TRAVEL_O4_V4 | OData V4 - UI | Fiori Elements |
| ZAPI_TRAVEL_O4_V4 | OData V4 - Web API | REST-Clients |
| ZUI_TRAVEL_O2 | OData V2 | Legacy-Fiori-Apps |
Best Practices
1. Klare Trennung der Schichten
✓ Interface View: Vollständiges Datenmodell, keine UI-Annotationen✓ Projection View: Anwendungsspezifische Felder, keine Business-Logik✓ Metadata Extension: UI-Annotationen getrennt vom View2. Konsistente Namensgebung
ZI_Travel → Interface (Basis)ZC_Travel → Fiori ConsumptionZC_TravelAPI → API ConsumptionZC_TravelReport → Reporting ConsumptionZC_TravelManager → Manager Consumption3. Associations richtig umleiten
Beim Projizieren von Compositions muss die Association auf die entsprechende Child-Projection umgeleitet werden:
_Booking : redirected to composition child ZC_Booking4. Provider Contract beachten
Der provider contract definiert den Zweck der Projection:
| Contract | Verwendung |
|---|---|
| transactional_query | Standard für UI und API |
| transactional_interface | Für A2A-Integration |
| analytical_query | Für Embedded Analytics |
Häufige Fehler vermeiden
Fehler 1: Fehlende Behavior Projection
" FALSCH: Nur CDS Projection ohne Behaviordefine root view entity ZC_Travel as projection on ZI_Travel { ... }
" RICHTIG: CDS Projection MIT Behavior Projection" 1. CDS View erstellen" 2. Behavior Projection erstellen" 3. Service Definition und BindingFehler 2: Association nicht umgeleitet
" FALSCH: Association zeigt noch auf Interface_Booking,
" RICHTIG: Association auf Projection umleiten_Booking : redirected to composition child ZC_Booking,Fehler 3: Inkonsistente Hierarchie
" FALSCH: Child-Projection zeigt auf Interface-Parentdefine view entity ZC_Booking as projection on ZI_Booking{ _Travel, " Zeigt auf ZI_Travel statt ZC_Travel}
" RICHTIG: Konsistente Projection-Hierarchiedefine view entity ZC_Booking as projection on ZI_Booking{ _Travel : redirected to parent ZC_Travel,}Weiterführende Themen
- RAP Grundlagen - Einführung in das RESTful ABAP Programming
- CDS Annotations - UI-Annotations und Metadata Extensions
- RAP Feature Control - Dynamische Steuerung von UI-Elementen
- RAP Authorization - Berechtigungsprüfungen in RAP
Fazit
Projection Views sind ein mächtiges Konzept, um ein Business Object für verschiedene Anwendungsfälle zu exponieren. Durch die saubere Trennung von Interface Layer, Projection Layer und Service Layer erreichst du:
- Wiederverwendbarkeit: Ein Business Object, mehrere UIs
- Wartbarkeit: Änderungen am Interface wirken auf alle Projections
- Flexibilität: Unterschiedliche Features, Felder und Annotationen pro Anwendungsfall
- Sicherheit: Eingeschränkte Operationen für bestimmte Benutzergruppen
Mit der Kombination aus Projection Views, Behavior Projections und Metadata Extensions hast du volle Kontrolle darüber, wie dein Business Object in verschiedenen Kontexten präsentiert und verwendet wird.