RAP Projection Views : Scénarios Multi-UI avec un Business Object

Catégorie
RAP
Publié
Auteur
Johannes

Les Projection Views forment la couche supérieure de l’architecture RAP et permettent d’exposer un seul Business Object pour différents cas d’usage. Avec les Projections, vous pouvez construire différentes UI, autorisations et comportements sur le même modèle de données.

Le concept de Projection Layer

Dans RAP, l’architecture suit le 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 │
└─────────────────────────────────────────────────────────┘

Conventions de nommage

PréfixeCoucheDescription
I_ ou ZI_InterfaceBusiness Object avec logique complète
C_ ou ZC_ConsumptionProjection pour cas d’usage spécifique
R_ ou ZR_RootAlternative pour Interface-Views

Pourquoi les Projection Views ?

Cas d’usage pour scénarios Multi-UI

  1. Différents groupes d’utilisateurs

    • Agents : Accès complet à tous les champs
    • Managers : Uniquement approbation et vue d’ensemble
    • Partenaires externes : Vue restreinte
  2. Différents appareils

    • Desktop : Application Fiori riche en détails
    • Mobile : Vue simplifiée
  3. Différents objectifs d’utilisation

    • Application transactionnelle : Opérations CRUD
    • Reporting : Accès en lecture seule
    • API : Interface technique

Créer une Projection CDS View

Interface View (Base)

@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 pour 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 pour consommateurs API

@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 pour 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

Chaque Projection View nécessite sa propre Behavior Projection, qui adapte le comportement du Business Object pour ce cas d’usage.

Interface Behavior Definition (Base)

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 pour Fiori App

La Projection expose toutes les opérations pour l’application Fiori :

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 pour API (restreinte)

La Projection API n’expose que les opérations sélectionnées :

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 pour Reporting (Read-Only)

Le Reporting n’expose aucune opération de modification :

projection;
strict ( 2 );
define behavior for ZC_TravelReport alias Travel
{
// Keine Operationen - reine Lesezugriffe
// Read wird automatisch unterstützt
}

Metadata Extensions

Les Metadata Extensions permettent différentes annotations UI par Projection, sans modifier la Projection View elle-même.

Metadata Extension pour 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 pour vue Manager

@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 par Projection

Grâce au Dynamic Feature Control, vous pouvez implémenter différentes règles par Projection. La logique est définie centralement dans la Behavior Implementation, mais contrôlée via la Behavior Projection.

Exemple : Feature Control différencié

Dans l’Interface Behavior, vous définissez les Features :

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;
}

L’implémentation vérifie le contexte :

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.

Dans la Projection API, vous pouvez ensuite omettre complètement les Features ou les remplacer en adaptant la Behavior Projection en conséquence.

Service Definition et Binding

Pour chaque Projection, vous créez un service distinct :

Service Definition pour 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 pour API

@EndUserText.label: 'Travel - API Service"
define service ZAPI_TRAVEL_O4 {
expose ZC_TravelAPI as Travel;
expose ZC_BookingAPI as Booking;
}

Service Binding

Pour chaque service, vous créez un Service Binding avec le protocole correspondant :

BindingProtocoleUtilisation
ZUI_TRAVEL_O4_V4OData V4 - UIFiori Elements
ZAPI_TRAVEL_O4_V4OData V4 - Web APIREST-Clients
ZUI_TRAVEL_O2OData V2Legacy-Fiori-Apps

Bonnes pratiques

1. Séparation claire des couches

✓ Interface View: Modèle de données complet, pas d'annotations UI
✓ Projection View: Champs spécifiques à l'application, pas de logique métier
✓ Metadata Extension: Annotations UI séparées de la vue

2. Nommage cohérent

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

3. Rediriger correctement les associations

Lors de la projection de Compositions, l’association doit être redirigée vers la Child-Projection correspondante :

_Booking : redirected to composition child ZC_Booking

4. Respecter le Provider Contract

Le provider contract définit l’objectif de la Projection :

ContractUtilisation
transactional_queryStandard pour UI et API
transactional_interfacePour intégration A2A
analytical_queryPour Embedded Analytics

Éviter les erreurs courantes

Erreur 1 : Behavior Projection manquante

" 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

Erreur 2 : Association non redirigée

" FALSCH: Association zeigt noch auf Interface
_Booking,
" RICHTIG: Association auf Projection umleiten
_Booking : redirected to composition child ZC_Booking,

Erreur 3 : Hiérarchie incohérente

" 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,
}

Sujets avancés

Conclusion

Les Projection Views sont un concept puissant pour exposer un Business Object pour différents cas d’usage. Grâce à la séparation nette entre Interface Layer, Projection Layer et Service Layer, vous obtenez :

  • Réutilisabilité : Un Business Object, plusieurs UI
  • Maintenabilité : Les modifications de l’Interface affectent toutes les Projections
  • Flexibilité : Différentes fonctionnalités, champs et annotations par cas d’usage
  • Sécurité : Opérations restreintes pour certains groupes d’utilisateurs

Avec la combinaison de Projection Views, Behavior Projections et Metadata Extensions, vous avez un contrôle total sur la façon dont votre Business Object est présenté et utilisé dans différents contextes.