ABAP OData Services: RESTful Web Services with SAP Gateway

Category
ABAP-Statements
Published
Author
Johannes

OData (Open Data Protocol) is the standard for RESTful APIs in SAP. With the SAP Gateway and the ABAP RESTful Application Programming Model (RAP), modern web services can be created.

OData Fundamentals

ConceptDescription
EntityData object (e.g., Order)
Entity SetCollection of Entities
Navigation PropertyRelationship between Entities
Service DocumentDescribes available resources

HTTP Methods and CRUD

MethodOperationDPC Method
GETReadGET_ENTITY, GET_ENTITYSET
POSTCreateCREATE_ENTITY
PUTUpdate (complete)UPDATE_ENTITY
PATCHUpdate (partial)UPDATE_ENTITY
DELETEDeleteDELETE_ENTITY

Classic Gateway (SEGW)

Data Provider Class - Reading an Entity

METHOD /iwbep/if_mgw_appl_srv_runtime~get_entity.
DATA: ls_key TYPE /iwbep/s_mgw_name_value_pair,
ls_order TYPE zcl_zorders_mpc=>ts_order.
" Read key from request
READ TABLE it_key_tab INTO ls_key WITH KEY name = 'OrderId'.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
textid = /iwbep/cx_mgw_busi_exception=>business_error
message = 'OrderId is required'.
ENDIF.
" Read data from database
SELECT SINGLE * FROM zorders INTO CORRESPONDING FIELDS OF ls_order
WHERE order_id = ls_key-value.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
textid = /iwbep/cx_mgw_busi_exception=>business_error_with_nav
message = 'Order not found'.
ENDIF.
" Return result
er_entity = ls_order.
ENDMETHOD.

Entity Set with Filter and Paging

METHOD /iwbep/if_mgw_appl_srv_runtime~get_entityset.
DATA: lt_orders TYPE zcl_zorders_mpc=>tt_order,
lt_filter TYPE /iwbep/t_mgw_select_option,
ls_paging TYPE /iwbep/s_mgw_paging.
" Read filter options
lt_filter = io_tech_request_context->get_filter( )->get_filter_select_options( ).
" Paging parameters
ls_paging = io_tech_request_context->get_paging( ).
" Create dynamic WHERE condition
DATA(lo_filter) = io_tech_request_context->get_filter( ).
DATA(lv_where) = lo_filter->get_filter_string( ).
" Read data
SELECT * FROM zorders
WHERE (lv_where)
ORDER BY order_id
INTO CORRESPONDING FIELDS OF TABLE lt_orders
UP TO ls_paging-top ROWS
OFFSET ls_paging-skip.
" Total count for $inlinecount
IF io_tech_request_context->has_inlinecount( ) = abap_true.
SELECT COUNT(*) FROM zorders WHERE (lv_where)
INTO @DATA(lv_count).
es_response_context-inlinecount = lv_count.
ENDIF.
et_entityset = lt_orders.
ENDMETHOD.

Creating an Entity

METHOD /iwbep/if_mgw_appl_srv_runtime~create_entity.
DATA: ls_order_in TYPE zcl_zorders_mpc=>ts_order,
ls_order_out TYPE zcl_zorders_mpc=>ts_order.
" Read request body
io_data_provider->read_entry_data( IMPORTING es_data = ls_order_in ).
" Validation
IF ls_order_in-customer_id IS INITIAL.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
message = 'Customer ID is required'.
ENDIF.
" Generate new ID
SELECT MAX( order_id ) FROM zorders INTO @DATA(lv_max_id).
ls_order_in-order_id = lv_max_id + 1.
ls_order_in-created_at = sy-datum.
ls_order_in-created_by = sy-uname.
" Save data
INSERT zorders FROM ls_order_in.
IF sy-subrc = 0.
ls_order_out = ls_order_in.
er_entity = ls_order_out.
ELSE.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
message = 'Error creating entity'.
ENDIF.
ENDMETHOD.

Updating an Entity

METHOD /iwbep/if_mgw_appl_srv_runtime~update_entity.
DATA: ls_order TYPE zcl_zorders_mpc=>ts_order,
ls_key TYPE /iwbep/s_mgw_name_value_pair.
" Read key and data
READ TABLE it_key_tab INTO ls_key WITH KEY name = 'OrderId'.
io_data_provider->read_entry_data( IMPORTING es_data = ls_order ).
" Check order exists
SELECT SINGLE * FROM zorders INTO @DATA(ls_existing)
WHERE order_id = @ls_key-value.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
message = 'Order not found'.
ENDIF.
" Perform update
ls_order-order_id = ls_key-value.
ls_order-changed_at = sy-datum.
ls_order-changed_by = sy-uname.
UPDATE zorders FROM ls_order.
er_entity = ls_order.
ENDMETHOD.

Deleting an Entity

METHOD /iwbep/if_mgw_appl_srv_runtime~delete_entity.
DATA: ls_key TYPE /iwbep/s_mgw_name_value_pair.
READ TABLE it_key_tab INTO ls_key WITH KEY name = 'OrderId'.
DELETE FROM zorders WHERE order_id = ls_key-value.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
EXPORTING
message = 'Delete failed'.
ENDIF.
ENDMETHOD.
METHOD /iwbep/if_mgw_appl_srv_runtime~get_expanded_entityset.
DATA: lt_orders TYPE zcl_zorders_mpc=>tt_order,
lt_items TYPE zcl_zorders_mpc=>tt_orderitem.
" Load orders with $expand=Items
SELECT * FROM zorders INTO CORRESPONDING FIELDS OF TABLE lt_orders.
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<ls_order>).
" Load items for the order
SELECT * FROM zorderitems
WHERE order_id = <ls_order>-order_id
INTO CORRESPONDING FIELDS OF TABLE lt_items.
" Add expand data
APPEND VALUE #(
key = <ls_order>-order_id
data = lt_items
) TO et_expanded_tech_clauses.
ENDLOOP.
et_entityset = lt_orders.
ENDMETHOD.

Deep Insert

METHOD /iwbep/if_mgw_appl_srv_runtime~create_deep_entity.
DATA: ls_order TYPE zcl_zorders_mpc=>ts_order_deep.
" Read deep structure
io_data_provider->read_entry_data( IMPORTING es_data = ls_order ).
" Create order
INSERT zorders FROM CORRESPONDING #( ls_order ).
" Create items
LOOP AT ls_order-items ASSIGNING FIELD-SYMBOL(<ls_item>).
<ls_item>-order_id = ls_order-order_id.
INSERT zorderitems FROM CORRESPONDING #( <ls_item> ).
ENDLOOP.
er_deep_entity = ls_order.
ENDMETHOD.

Function Import

METHOD /iwbep/if_mgw_appl_srv_runtime~execute_action.
CASE iv_action_name.
WHEN 'ConfirmOrder'.
" Read parameter
DATA(lv_order_id) = VALUE #( it_parameter[ name = 'OrderId' ]-value OPTIONAL ).
" Confirm order
UPDATE zorders SET status = 'C' WHERE order_id = lv_order_id.
IF sy-subrc = 0.
er_data = VALUE zcl_zorders_mpc=>ts_result( success = abap_true ).
ENDIF.
WHEN 'GetOrderStatistics'.
SELECT COUNT(*) AS total,
SUM( CASE WHEN status = 'O' THEN 1 ELSE 0 END ) AS open,
SUM( CASE WHEN status = 'C' THEN 1 ELSE 0 END ) AS closed
FROM zorders INTO @DATA(ls_stats).
er_data = ls_stats.
ENDCASE.
ENDMETHOD.

RAP-based OData Services (Modern)

CDS View with Service Definition

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Orders'
@Metadata.allowExtensions: true
define root view entity ZI_Order
as select from zorders
composition [0..*] of ZI_OrderItem as _Items
{
key order_id as OrderId,
customer_id as CustomerId,
order_date as OrderDate,
status as Status,
total_amount as TotalAmount,
currency as Currency,
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
_Items
}

Projection View

@EndUserText.label: 'Orders Projection'
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
define root view entity ZC_Order
provider contract transactional_query
as projection on ZI_Order
{
key OrderId,
CustomerId,
OrderDate,
Status,
@Semantics.amount.currencyCode: 'Currency'
TotalAmount,
Currency,
_Items : redirected to composition child ZC_OrderItem
}

Service Definition

@EndUserText.label: 'Order Service'
define service ZSB_Order {
expose ZC_Order as Orders;
expose ZC_OrderItem as OrderItems;
}

Behavior Definition

managed implementation in class zbp_i_order unique;
strict ( 2 );
define behavior for ZI_Order alias Order
persistent table zorders
lock master
authorization master ( instance )
{
field ( readonly ) OrderId, CreatedBy, CreatedAt;
field ( mandatory ) CustomerId;
create;
update;
delete;
action confirmOrder result [1] $self;
determination setDefaults on modify { create; }
validation validateCustomer on save { field CustomerId; }
association _Items { create; }
}

Behavior Implementation

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS:
get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations
RESULT result,
confirmOrder FOR MODIFY
IMPORTING keys FOR ACTION order~confirmOrder
RESULT result,
setDefaults FOR DETERMINE ON MODIFY
IMPORTING keys FOR order~setDefaults,
validateCustomer FOR VALIDATE ON SAVE
IMPORTING keys FOR order~validateCustomer.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_instance_authorizations.
" Authorization check
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
APPEND VALUE #(
%tky = <key>-%tky
%op-%update = COND #(
WHEN has_authority( ) = abap_true
THEN if_abap_behv=>auth-allowed
ELSE if_abap_behv=>auth-unauthorized )
) TO result.
ENDLOOP.
ENDMETHOD.
METHOD confirmOrder.
" Read orders
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
FIELDS ( Status )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Update status
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
UPDATE FIELDS ( Status )
WITH VALUE #( FOR order IN lt_orders
( %tky = order-%tky
Status = 'C' ) ).
" Return result
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result
( %tky = order-%tky
%param = order ) ).
ENDMETHOD.
METHOD setDefaults.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
FIELDS ( OrderDate Status )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
UPDATE FIELDS ( OrderDate Status )
WITH VALUE #( FOR order IN lt_orders
WHERE ( OrderDate IS INITIAL )
( %tky = order-%tky
OrderDate = cl_abap_context_info=>get_system_date( )
Status = 'O' ) ).
ENDMETHOD.
METHOD validateCustomer.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY order
FIELDS ( CustomerId )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" Check customer exists
SELECT SINGLE @abap_true FROM zcustomers
WHERE customer_id = @ls_order-CustomerId
INTO @DATA(lv_exists).
IF lv_exists = abap_false.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Customer does not exist' )
) TO failed-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

OData V4 Query Handler

CLASS zcl_order_query DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
ENDCLASS.
CLASS zcl_order_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
DATA: lt_orders TYPE TABLE OF zi_order.
" Read filter
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
" Paging
DATA(lv_top) = io_request->get_paging( )->get_page_size( ).
DATA(lv_skip) = io_request->get_paging( )->get_offset( ).
" Sorting
DATA(lt_sort) = io_request->get_sort_elements( ).
" Read data with dynamic filter
SELECT * FROM zi_order
WHERE customer_id IN @( VALUE #( FOR filter IN lt_filter
WHERE ( name = 'CUSTOMERID' )
( filter-range ) ) )
ORDER BY order_id
INTO TABLE @lt_orders
UP TO @lv_top ROWS
OFFSET @lv_skip.
" Total count
IF io_request->is_total_numb_of_rec_requested( ).
SELECT COUNT(*) FROM zi_order INTO @DATA(lv_count).
io_response->set_total_number_of_records( lv_count ).
ENDIF.
io_response->set_data( lt_orders ).
ENDMETHOD.
ENDCLASS.

Query Options

OptionExampleDescription
$filter$filter=Status eq 'O'Filtering
$select$select=OrderId,StatusField selection
$expand$expand=_ItemsLoad navigation
$orderby$orderby=OrderDate descSorting
$top$top=10Limit
$skip$skip=20Offset
$count$count=trueTotal count

Best Practices

  1. Versioning: Use service versions for breaking changes
  2. Error Handling: Provide meaningful HTTP status codes and messages
  3. Security: Implement authorization checks
  4. Performance: Use $select, load only required fields
  5. ETag: Use optimistic locking for updates
  6. Prefer RAP: Use RAP instead of SEGW for new developments

Important Transactions

TransactionDescription
SEGWGateway Service Builder
/IWFND/MAINT_SERVICEActivate service
/IWFND/GW_CLIENTGateway Client (Test)
/IWFND/ERROR_LOGError log