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éfixe | Couche | Description |
|---|---|---|
| I_ ou ZI_ | Interface | Business Object avec logique complète |
| C_ ou ZC_ | Consumption | Projection pour cas d’usage spécifique |
| R_ ou ZR_ | Root | Alternative pour Interface-Views |
Pourquoi les Projection Views ?
Cas d’usage pour scénarios Multi-UI
-
Différents groupes d’utilisateurs
- Agents : Accès complet à tous les champs
- Managers : Uniquement approbation et vue d’ensemble
- Partenaires externes : Vue restreinte
-
Différents appareils
- Desktop : Application Fiori riche en détails
- Mobile : Vue simplifiée
-
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: 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 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 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 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: #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 pour vue Manager
@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 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 :
| Binding | Protocole | Utilisation |
|---|---|---|
| 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 |
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 vue2. Nommage cohérent
ZI_Travel → Interface (Base)ZC_Travel → Fiori ConsumptionZC_TravelAPI → API ConsumptionZC_TravelReport → Reporting ConsumptionZC_TravelManager → Manager Consumption3. 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_Booking4. Respecter le Provider Contract
Le provider contract définit l’objectif de la Projection :
| Contract | Utilisation |
|---|---|
| transactional_query | Standard pour UI et API |
| transactional_interface | Pour intégration A2A |
| analytical_query | Pour Embedded Analytics |
Éviter les erreurs courantes
Erreur 1 : Behavior Projection manquante
" 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 BindingErreur 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-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,}Sujets avancés
- RAP Grundlagen - Introduction au RESTful ABAP Programming
- CDS Annotations - UI-Annotations et Metadata Extensions
- RAP Feature Control - Contrôle dynamique des éléments UI
- RAP Authorization - Vérifications d’autorisation dans RAP
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.