Projection Views forman la capa superior de la arquitectura RAP y permiten exponer un único Business Object para diferentes casos de uso. Con Projections puedes construir diferentes UIs, permisos y comportamientos sobre el mismo modelo de datos.
El concepto de Projection Layer
En RAP, la arquitectura sigue el 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-focus) │ │ (minimal) │ │ (readonly) │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │├─────────┼────────────────┼────────────────┼─────────────┤│ │ Business Object Layer (I_*) │ ││ │ ┌────────▼────────┐ │ ││ └───────► ZI_Travel ◄───────┘ ││ │ (Lógica core) │ ││ └─────────────────┘ │├─────────────────────────────────────────────────────────┤│ Database Layer │└─────────────────────────────────────────────────────────┘Convenciones de nomenclatura
| Prefijo | Capa | Descripción |
|---|---|---|
| I_ o ZI_ | Interface | Business Object con lógica completa |
| C_ o ZC_ | Consumption | Projection para caso de uso específico |
| R_ o ZR_ | Root | Alternativa para Interface-Views |
¿Por qué Projection Views?
Casos de uso para escenarios Multi-UI
-
Diferentes grupos de usuarios
- Empleados: Acceso completo a todos los campos
- Gerentes: Solo aprobación y vista general
- Socios externos: Vista restringida
-
Diferentes dispositivos
- Desktop: App Fiori detallada
- Mobile: Vista simplificada
-
Diferentes propósitos
- App transaccional: Operaciones CRUD
- Reporting: Solo lectura
- API: Interfaz técnica
Crear 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 para 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,
// Campos calculados solo para esta Projection case Status when 'A' then 'Aceptado' when 'X' then 'Rechazado' when 'O' then 'Abierto' else 'Desconocido' end as StatusText,
CreatedBy, CreatedAt, LastChangedBy, LastChangedAt,
// Exponer associations _Booking : redirected to composition child ZC_Booking, _Agency, _Customer}Projection View para consumidores 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,
// Sin campos específicos de UI // Sin campos calculados para Status-Text
LastChangedAt,
_Booking : redirected to composition child ZC_BookingAPI}Projection View para 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,
// Calcular duración dats_days_between( BeginDate, EndDate ) as TravelDuration,
TotalPrice, CurrencyCode, Status, CreatedAt,
// Para agregaciones @DefaultAggregation: #SUM TotalPrice as SumTotalPrice,
@DefaultAggregation: #COUNT cast( 1 as abap.int4 ) as TravelCount}Behavior Projection
Cada Projection View necesita su propia Behavior Projection, que adapta el comportamiento del Business Object para este caso de uso.
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 para Fiori App
La Projection expone todas las operaciones para la aplicación 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 para API (restringido)
La Projection API expone solo operaciones seleccionadas:
projection;strict ( 2 );
define behavior for ZC_TravelAPI alias Travel{ use create; use update; // Sin delete - API no puede eliminar
use action acceptTravel; // Sin rejectTravel para API
use association _Booking { create; }}
define behavior for ZC_BookingAPI alias Booking{ use update; // Sin delete para Bookings vía API
use association _Travel;}Behavior Projection para Reporting (Read-Only)
El Reporting no expone operaciones de modificación:
projection;strict ( 2 );
define behavior for ZC_TravelReport alias Travel{ // Sin operaciones - solo acceso de lectura // Read se soporta automáticamente}Metadata Extensions
Metadata Extensions permiten diferentes anotaciones UI por Projection, sin modificar la Projection View misma.
Metadata Extension para Fiori App
@Metadata.layer: #COREannotate entity ZC_Travel with{ @UI.facet: [ { id: 'TravelHeader', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'Detalles del viaje', position: 10 }, { id: 'Bookings', purpose: #STANDARD, type: #LINEITEM_REFERENCE, label: 'Reservas', position: 20, targetElement: '_Booking' } ]
@UI: { headerInfo: { typeName: 'Viaje', typeNamePlural: 'Viajes', 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: 'Aceptar', position: 10 }, { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Rechazar', position: 20 } ] StatusText;}Metadata Extension para Vista de Gerentes
@Metadata.layer: #COREannotate entity ZC_TravelManager with{ @UI.facet: [ { id: 'Overview', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, label: 'Resumen', position: 10 } ]
@UI: { headerInfo: { typeName: 'Aprobación', typeNamePlural: 'Aprobaciones', title: { type: #STANDARD, value: 'Description' } } }
// Solo campos relevantes para gerentes @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;
// Botones de acción prominentes @UI.lineItem: [ { type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Aprobar', position: 50 }, { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Rechazar', position: 60 } ] Description;}Feature Control por Projection
A través de Dynamic Feature Control puedes implementar diferentes reglas por Projection. La lógica se define centralmente en la Behavior Implementation, pero se controla a través de la Behavior Projection.
Ejemplo: Diferente Feature Control
En el Interface Behavior defines los features:
define behavior for ZI_Travel alias Travel{ // ...
// Features para control diferenciado action ( features: instance ) acceptTravel result [1] $self; action ( features: instance ) rejectTravel result [1] $self; delete ( features: instance );
field ( features: instance ) Status;}La implementación verifica el contexto:
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
" Features dependientes del estado %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 )
" Eliminar solo para viajes abiertos %delete = COND #( WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled ELSE if_abap_behv=>fc-o-disabled )
" Campo Status readonly si no está abierto %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.En la Projection API puedes omitir features completamente o sobrescribirlos ajustando la Behavior Projection correspondientemente.
Service Definition y Binding
Para cada Projection creas un servicio propio:
Service Definition para 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 para API
@EndUserText.label: 'Travel - API Service'define service ZAPI_TRAVEL_O4 { expose ZC_TravelAPI as Travel; expose ZC_BookingAPI as Booking;}Service Binding
Para cada servicio creas un Service Binding con el protocolo correspondiente:
| Binding | Protocolo | Uso |
|---|---|---|
| ZUI_TRAVEL_O4_V4 | OData V4 - UI | Fiori Elements |
| ZAPI_TRAVEL_O4_V4 | OData V4 - Web API | Clientes REST |
| ZUI_TRAVEL_O2 | OData V2 | Apps Fiori legacy |
Mejores prácticas
1. Separación clara de capas
✓ Interface View: Modelo de datos completo, sin anotaciones UI✓ Projection View: Campos específicos de aplicación, sin lógica de negocio✓ Metadata Extension: Anotaciones UI separadas de la View2. Nomenclatura consistente
ZI_Travel → Interface (Base)ZC_Travel → Fiori ConsumptionZC_TravelAPI → API ConsumptionZC_TravelReport → Reporting ConsumptionZC_TravelManager → Manager Consumption3. Redirigir associations correctamente
Al proyectar Compositions, la Association debe redirigirse a la Projection hija correspondiente:
_Booking : redirected to composition child ZC_Booking4. Observar Provider Contract
El provider contract define el propósito de la Projection:
| Contract | Uso |
|---|---|
| transactional_query | Estándar para UI y API |
| transactional_interface | Para integración A2A |
| analytical_query | Para Embedded Analytics |
Evitar errores comunes
Error 1: Behavior Projection faltante
" INCORRECTO: Solo CDS Projection sin Behaviordefine root view entity ZC_Travel as projection on ZI_Travel { ... }
" CORRECTO: CDS Projection CON Behavior Projection" 1. Crear CDS View" 2. Crear Behavior Projection" 3. Service Definition y BindingError 2: Association no redirigida
" INCORRECTO: Association aún apunta a Interface_Booking,
" CORRECTO: Redirigir Association a Projection_Booking : redirected to composition child ZC_Booking,Error 3: Jerarquía inconsistente
" INCORRECTO: Child-Projection apunta a Interface-Parentdefine view entity ZC_Booking as projection on ZI_Booking{ _Travel, " Apunta a ZI_Travel en lugar de ZC_Travel}
" CORRECTO: Jerarquía de Projection consistentedefine view entity ZC_Booking as projection on ZI_Booking{ _Travel : redirected to parent ZC_Travel,}Temas relacionados
- Fundamentos RAP - Introducción al RESTful ABAP Programming
- Anotaciones CDS - UI-Annotations y Metadata Extensions
- RAP Feature Control - Control dinámico de elementos UI
- RAP Authorization - Verificaciones de permisos en RAP
Conclusión
Los Projection Views son un concepto poderoso para exponer un Business Object para diferentes casos de uso. A través de la separación limpia de Interface Layer, Projection Layer y Service Layer logras:
- Reutilización: Un Business Object, múltiples UIs
- Mantenibilidad: Cambios en Interface afectan todas las Projections
- Flexibilidad: Diferentes features, campos y anotaciones por caso de uso
- Seguridad: Operaciones restringidas para ciertos grupos de usuarios
Con la combinación de Projection Views, Behavior Projections y Metadata Extensions tienes control total sobre cómo se presenta y usa tu Business Object en diferentes contextos.