RAP Projection Views: Multi-UI-Szenarien mit einem Business Object

kategorie
RAP
Veröffentlicht
autor
Johannes

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äfixSchichtBeschreibung
I_ oder ZI_InterfaceBusiness Object mit vollständiger Logik
C_ oder ZC_ConsumptionProjection für spezifischen Anwendungsfall
R_ oder ZR_RootAlternative für Interface-Views

Warum Projection Views?

Anwendungsfälle für Multi-UI-Szenarien

  1. Unterschiedliche Benutzergruppen

    • Sachbearbeiter: Voller Zugriff auf alle Felder
    • Manager: Nur Genehmigung und Übersicht
    • Externe Partner: Eingeschränkte Sicht
  2. Unterschiedliche Geräte

    • Desktop: Detailreiche Fiori App
    • Mobile: Vereinfachte Ansicht
  3. 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: true
define 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 Travel
persistent table ztravel
lock master
authorization 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 Booking
persistent table zbooking
lock dependent by _Travel
authorization dependent by _Travel
etag 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: #CORE
annotate 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: #CORE
annotate 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:

BindingProtokollVerwendung
ZUI_TRAVEL_O4_V4OData V4 - UIFiori Elements
ZAPI_TRAVEL_O4_V4OData V4 - Web APIREST-Clients
ZUI_TRAVEL_O2OData V2Legacy-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 View

2. Konsistente Namensgebung

ZI_Travel → Interface (Basis)
ZC_Travel → Fiori Consumption
ZC_TravelAPI → API Consumption
ZC_TravelReport → Reporting Consumption
ZC_TravelManager → Manager Consumption

3. Associations richtig umleiten

Beim Projizieren von Compositions muss die Association auf die entsprechende Child-Projection umgeleitet werden:

_Booking : redirected to composition child ZC_Booking

4. Provider Contract beachten

Der provider contract definiert den Zweck der Projection:

ContractVerwendung
transactional_queryStandard für UI und API
transactional_interfaceFür A2A-Integration
analytical_queryFür Embedded Analytics

Häufige Fehler vermeiden

Fehler 1: Fehlende Behavior Projection

" FALSCH: Nur CDS Projection ohne Behavior
define 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 Binding

Fehler 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-Parent
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel, " Zeigt auf ZI_Travel statt ZC_Travel
}
" RICHTIG: Konsistente Projection-Hierarchie
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel : redirected to parent ZC_Travel,
}

Weiterführende Themen

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.