RAP Projection Views: Escenarios Multi-UI con un Business Object

Kategorie
RAP
Veröffentlicht
Autor
Johannes

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

PrefijoCapaDescripción
I_ o ZI_InterfaceBusiness Object con lógica completa
C_ o ZC_ConsumptionProjection para caso de uso específico
R_ o ZR_RootAlternativa para Interface-Views

¿Por qué Projection Views?

Casos de uso para escenarios Multi-UI

  1. Diferentes grupos de usuarios

    • Empleados: Acceso completo a todos los campos
    • Gerentes: Solo aprobación y vista general
    • Socios externos: Vista restringida
  2. Diferentes dispositivos

    • Desktop: App Fiori detallada
    • Mobile: Vista simplificada
  3. 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: 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,
// 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 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 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: #CORE
annotate 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: #CORE
annotate 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:

BindingProtocoloUso
ZUI_TRAVEL_O4_V4OData V4 - UIFiori Elements
ZAPI_TRAVEL_O4_V4OData V4 - Web APIClientes REST
ZUI_TRAVEL_O2OData V2Apps 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 View

2. Nomenclatura consistente

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

3. Redirigir associations correctamente

Al proyectar Compositions, la Association debe redirigirse a la Projection hija correspondiente:

_Booking : redirected to composition child ZC_Booking

4. Observar Provider Contract

El provider contract define el propósito de la Projection:

ContractUso
transactional_queryEstándar para UI y API
transactional_interfacePara integración A2A
analytical_queryPara Embedded Analytics

Evitar errores comunes

Error 1: Behavior Projection faltante

" INCORRECTO: Solo CDS Projection sin Behavior
define 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 Binding

Error 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-Parent
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel, " Apunta a ZI_Travel en lugar de ZC_Travel
}
" CORRECTO: Jerarquía de Projection consistente
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel : redirected to parent ZC_Travel,
}

Temas relacionados

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.