[[{“value”:”
Introduction
The Draft capability in SAP RAP allows users to create, edit, and save business data without immediately changing the active business data.
This is what powers the familiar Draft behavior in Fiori Elements applications. For example, a user can start filling in a form, navigate away, and later return to continue working on it — all before the data is finally activated as an active business record.
Under the hood, RAP stores draft instances in a separate draft persistence (draft table), which is managed by the RAP framework.
This introduces an important consideration when implementing access control. DCL (Data Control Language) is defined on CDS views, not directly on database tables. Also, access restrictions defined for the active CDS view do not automatically restrict access to draft instances.
To address this requirement, SAP RAP provides the Draft Query View — a CDS view built on top of the draft table and connected to the behavior definition using the query addition.
Once the Draft Query View is assigned, RAP can use this CDS view when querying draft instances. This provides a CDS-based layer where access restrictions can be defined specifically for draft data.
In this blog, I’ll walk through building a Price Change Request application end-to-end and demonstrate how the Draft Query View can be used to control access to draft instances.
Why Does Draft Data Need Special Handling?
A Draft Query View is a CDS view that reads data from the draft persistence.
By default, DCL access restrictions defined for the active CDS view entity do not automatically apply to draft data. Draft instances therefore require their own access-control consideration.
By assigning a Draft Query View using the query addition in the behavior definition, RAP can use the CDS view when reading draft instances. This allows you to define purpose-built access restrictions for draft data, similar to how access control is defined for active data.
Implementation steps
Step 1 : Create a Data Base Table
@EndUserText.label : ‘Price Change Request’
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zaf_dt_pricerequ {
key client : abap.clnt not null;
key request_id : abap.char(10) not null;
material : abap.char(18);
sales_org : abap.char(4);
customer : abap.char(10);
current_price : abap.dec(15,2);
new_price : abap.dec(15,2);
currency : abap.cuky;
valid_from : abap.dats;
reason : abap.char(90);
status : abap.char(20);
changed_at : timestampl;
changed_by : abap.char(15);
created_at : timestampl;
created_by : abap.char(15);
}
Step 2 : Create root CDS view for table
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: ‘Price Change Request’
@Metadata.ignorePropagatedAnnotations: true
define root view entity ZAF_I_PRICEREQU as select from zaf_dt_pricerequ
{
key request_id as RequestId,
material as Material,
sales_org as SalesOrg,
customer as Customer,
current_price as CurrentPrice,
new_price as NewPrice,
currency as Currency,
valid_from as ValidFrom,
reason as Reason,
status as Status,
@Semantics.systemDateTime.lastChangedAt: true
changed_at as ChangedAt,
changed_by as ChangedBy,
created_at as CreatedAt,
created_by as CreatedBy
}
Step 3: create projection view
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: ‘Projection View’
@Metadata.ignorePropagatedAnnotations: true
@Metadata.allowExtensions: true
define root view entity ZAF_C_PRICEREQDF
provider contract transactional_query
as projection on ZAF_I_PRICEREQU
{
key RequestId,
Material,
SalesOrg,
Customer,
CurrentPrice,
NewPrice,
Currency,
ValidFrom,
Reason,
Status,
ChangedAt,
ChangedBy,
CreatedAt,
CreatedBy
}
Step 4: Create the Projection Behavior Definition
projection;
strict ( 2 );
use draft;
define behavior for ZAF_C_PRICEREQDF
{
use create;
use update;
use delete;
use action Activate;
use action Discard;
use action Edit;
use action Resume;
use action Prepare;
}
Step 5: Create Metadata Extension
@Metadata.layer: #CORE
annotate entity ZAF_C_PRICEREQDF
with
{
@UI.facet: [
{
id: ‘PriceRequestHeader’,
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: ‘Price Change Request Details’,
position: 10
}
]
@UI.lineItem: [ { position: 10, label: ‘Request ID’ } ]
@UI.identification: [ { position: 10, label: ‘Request ID’ } ]
RequestId;
@UI.lineItem: [ { position: 20, label: ‘Material’ } ]
@UI.identification: [ { position: 20, label: ‘Material’ } ]
Material;
@UI.lineItem: [ { position: 30, label: ‘Sales Organization’ } ]
@UI.identification: [ { position: 30, label: ‘Sales Organization’ } ]
SalesOrg;
@UI.lineItem: [ { position: 40, label: ‘Customer’ } ]
@UI.identification: [ { position: 40, label: ‘Customer’ } ]
Customer;
@UI.lineItem: [ { position: 50, label: ‘Current Price’ } ]
@UI.identification: [ { position: 50, label: ‘Current Price’ } ]
CurrentPrice;
@UI.lineItem: [ { position: 60, label: ‘New Price’ } ]
@UI.identification: [ { position: 60, label: ‘New Price’ } ]
NewPrice;
@UI.lineItem: [ { position: 70, label: ‘Currency’ } ]
@UI.identification: [ { position: 70, label: ‘Currency’ } ]
Currency;
@UI.lineItem: [ { position: 80, label: ‘Valid From’ } ]
@UI.identification: [ { position: 80, label: ‘Valid From’ } ]
ValidFrom;
@UI.lineItem: [ { position: 90, label: ‘Status’ } ]
@UI.identification: [ { position: 90, label: ‘Status’ } ]
Status;
@UI.identification: [ { position: 100, label: ‘Reason’ } ]
Reason;
@UI.hidden: true
ChangedAt;
@UI.hidden: true
ChangedBy;
@UI.hidden: true
CreatedAt;
@UI.hidden: true
CreatedBy;
}
Step 6: Create Behavior Definition
- Create the Draft Table.
- After creating the Draft Table, create an Interface CDS View on top of the Draft Table.
- In the behavior definition, declare the Draft Table using the query keyword and provide the name of the Interface CDS View created on top of the Draft Table.
A few important points about this definition:
- Draft table zaf_df_pricedf query ZAF_I_PRICEREQUDF is the key line for this entire blog — it declares the draft table AND assigns the draft query view that will secure it.
Step 7: Create Draft Data Base Table
@EndUserText.label : ‘Draft table for entity ZAF_I_PRICEREQU’
@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zaf_df_pricedf {
key mandt : mandt not null;
key requestid : abap.char(10) not null;
material : abap.char(18);
salesorg : abap.char(4);
customer : abap.char(10);
currentprice : abap.dec(15,2);
newprice : abap.dec(15,2);
currency : abap.cuky;
validfrom : abap.dats;
reason : abap.char(90);
status : abap.char(20);
changedat : timestampl;
changedby : abap.char(15);
createdat : timestampl;
createdby : abap.char(15);
“%admin” : include sych_bdl_draft_admin_inc;
}
Step 8: Interface view of Draft data base table
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: ‘View on draft table’
@Metadata.ignorePropagatedAnnotations: true
define view entity ZAF_I_PRICEREQUDF
as select from zaf_df_pricedf
association [0..1] to I_DraftAdministrativeData as _DraftAdminData
on $projection.DraftAdministrativeDataUUID = _DraftAdminData.DraftUUID
{
key requestid as Requestid,
material as Material,
salesorg as Salesorg,
customer as Customer,
currentprice as Currentprice,
newprice as Newprice,
currency as Currency,
validfrom as Validfrom,
reason as Reason,
status as Status,
changedat as Changedat,
changedby as Changedby,
createdat as Createdat,
createdby as Createdby,
draftentitycreationdatetime as Draftentitycreationdatetime,
draftentitylastchangedatetime as Draftentitylastchangedatetime,
draftadministrativedatauuid as DraftAdministrativeDataUUID,
draftentityoperationcode as Draftentityoperationcode,
hasactiveentity as Hasactiveentity,
draftfieldchanges as Draftfieldchanges,
_DraftAdminData.InProcessByUser as InProcessByUser,
_DraftAdminData
}
Step 9: Create the DCL Roles
Two separate DCL roles are needed — one for the active view, one for the draft query view.
Active data access control:
@EndUserText.label: ‘Access control for price request active’
@MappingRole: true
define role ZAF_DCL_PRICEREQU {
grant
select
on
ZAF_I_PRICEREQU
where
Createdby = aspect user;
}
Draft data access control:
@EndUserText.label: ‘Access control for price request draft’
@MappingRole: true
define role ZAF_DCL_PRICEREQU_DF {
grant
select
on
ZAF_I_PRICEREQUDF
where
InProcessByUser = aspect user;
}
Notice these two roles filter on different fields — Createdby for active data, but InProcessByUser for draft data. This isn’t arbitrary; it comes down to when each field actually gets populated.
When a user clicks Create, RAP immediately and automatically inserts a row into the draft table — before any custom ABAP code runs. At that exact moment, the framework populates the draft admin data (including InProcessByUser, accessible via the association to I_DraftAdministrativeData), because that’s system-managed data the framework itself owns.
Createdby, on the other hand, is a business field — it only gets a value inside the handler class’s create method, which only executes later, when Activate/Save is triggered. So immediately after Create, Createdby on the draft table is still blank, and filtering on it would silently hide every newly created draft.
The rule of thumb: filter draft data using framework-populated draft admin fields, and filter active data using your own business fields — by the time something is active, your own logic has already run and populated them correctly.
Step 10: Create the Behavior Handler and Saver Classes
CLASS lhc_ZAF_I_PRICEREQU DEFINITION
INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations
FOR INSTANCE AUTHORIZATION
IMPORTING
keys REQUEST requested_authorizations
FOR zaf_i_pricerequ
RESULT result.
METHODS create
FOR MODIFY
IMPORTING entities FOR CREATE zaf_i_pricerequ.
METHODS earlynumbering_create
FOR NUMBERING
IMPORTING entities FOR CREATE zaf_i_pricerequ.
METHODS update
FOR MODIFY
IMPORTING entities FOR UPDATE zaf_i_pricerequ.
METHODS delete
FOR MODIFY
IMPORTING keys FOR DELETE zaf_i_pricerequ.
METHODS read
FOR READ
IMPORTING keys FOR READ zaf_i_pricerequ
RESULT result.
METHODS lock
FOR LOCK
IMPORTING keys FOR LOCK zaf_i_pricerequ.
ENDCLASS.
CLASS lhc_ZAF_I_PRICEREQU IMPLEMENTATION.
METHOD get_instance_authorizations.
result = VALUE #(
FOR key IN keys
( %tky = key-%tky
%update = if_abap_behv=>auth-allowed
%delete = if_abap_behv=>auth-allowed ) ).
ENDMETHOD.
METHOD earlynumbering_create.
DATA:
lv_max_active TYPE i,
lv_max_draft TYPE i,
lv_max_id TYPE i,
lv_new_id TYPE zaf_dt_pricerequ-request_id.
SELECT MAX( request_id )
FROM zaf_dt_pricerequ
INTO @DATA(lv_ext_active).
IF lv_ext_active IS NOT INITIAL.
lv_max_active = CONV i( lv_ext_active+2 ).
ENDIF.
SELECT MAX( requestid )
FROM zaf_df_pricedf
INTO @DATA(lv_ext_draft).
IF lv_ext_draft IS NOT INITIAL.
lv_max_draft = CONV i( lv_ext_draft+2 ).
ENDIF.
lv_max_id = nmax( val1 = lv_max_active val2 = lv_max_draft ).
IF lv_max_id < 100010.
lv_max_id = 100010.
ENDIF.
LOOP AT entities ASSIGNING FIELD-SYMBOL(<fs_entity>).
IF <fs_entity>-RequestId IS NOT INITIAL.
lv_new_id = <fs_entity>-RequestId.
ELSE.
WHILE 1 = 1.
lv_max_id += 1.
lv_new_id = |PR{ lv_max_id }|.
SELECT SINGLE request_id FROM zaf_dt_pricerequ WHERE request_id = @LV_new_id INTO @DATA(lv_check_act).
SELECT SINGLE requestid FROM zaf_df_pricedf WHERE requestid = @LV_new_id INTO @DATA(lv_check_dft).
IF lv_check_act IS INITIAL AND lv_check_dft IS INITIAL.
EXIT.
ENDIF.
ENDWHILE.
ENDIF.
APPEND VALUE #(
%cid = <fs_entity>-%cid
RequestId = lv_new_id
%is_draft = <fs_entity>-%is_draft
) TO mapped-zaf_i_pricerequ.
ENDLOOP.
ENDMETHOD.
METHOD create.
zcl_af_pricerequ=>get_instance( )->create_data(
EXPORTING
entities = entities
CHANGING
mapped = mapped
failed = failed
reported = reported ).
ENDMETHOD.
METHOD update.
zcl_af_pricerequ=>get_instance( )->update_data(
EXPORTING
entities = entities
CHANGING
mapped = mapped
failed = failed
reported = reported ).
ENDMETHOD.
METHOD delete.
zcl_af_pricerequ=>get_instance( )->delete_data(
EXPORTING
keys = keys
CHANGING
mapped = mapped
failed = failed
reported = reported ).
ENDMETHOD.
METHOD read.
zcl_af_pricerequ=>get_instance( )->read_data(
EXPORTING
keys = keys
CHANGING
result = result
failed = failed
reported = reported ).
ENDMETHOD.
METHOD lock.
ENDMETHOD.
ENDCLASS.
CLASS lsc_ZAF_I_PRICEREQU DEFINITION
INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS finalize REDEFINITION.
METHODS check_before_save REDEFINITION.
METHODS save REDEFINITION.
METHODS cleanup REDEFINITION.
METHODS cleanup_finalize REDEFINITION.
ENDCLASS.
CLASS lsc_ZAF_I_PRICEREQU IMPLEMENTATION.
METHOD finalize.
ENDMETHOD.
METHOD check_before_save.
ENDMETHOD.
METHOD save.
zcl_af_pricerequ=>get_instance( )->save_data( ).
ENDMETHOD.
METHOD cleanup.
ENDMETHOD.
METHOD cleanup_finalize.
ENDMETHOD.
ENDCLASS.
Step 11: Create the Helper Class
CLASS zcl_af_pricerequ DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
tt_create TYPE TABLE FOR CREATE zaf_i_pricerequ,
tt_update TYPE TABLE FOR UPDATE zaf_i_pricerequ,
tt_delete TYPE TABLE FOR DELETE zaf_i_pricerequ,
tt_read TYPE TABLE FOR READ IMPORT zaf_i_pricerequ,
tt_result TYPE TABLE FOR READ RESULT zaf_i_pricerequ,
tt_mapped TYPE RESPONSE FOR MAPPED zaf_i_pricerequ,
tt_failed TYPE RESPONSE FOR FAILED zaf_i_pricerequ,
tt_reported TYPE RESPONSE FOR REPORTED zaf_i_pricerequ.
CLASS-METHODS get_instance
RETURNING VALUE(ro_instance) TYPE REF TO zcl_af_pricerequ.
METHODS create_data
IMPORTING
entities TYPE tt_create
CHANGING
mapped TYPE tt_mapped
failed TYPE tt_failed
reported TYPE tt_reported.
METHODS update_data
IMPORTING
entities TYPE tt_update
CHANGING
mapped TYPE tt_mapped
failed TYPE tt_failed
reported TYPE tt_reported.
METHODS delete_data
IMPORTING
keys TYPE tt_delete
CHANGING
mapped TYPE tt_mapped
failed TYPE tt_failed
reported TYPE tt_reported.
METHODS read_data
IMPORTING
keys TYPE tt_read
CHANGING
result TYPE tt_result
failed TYPE tt_failed
reported TYPE tt_reported.
METHODS save_data.
PRIVATE SECTION.
CLASS-DATA mo_instance TYPE REF TO zcl_af_pricerequ.
DATA:
mt_create TYPE STANDARD TABLE OF zaf_dt_pricerequ,
mt_update TYPE STANDARD TABLE OF zaf_dt_pricerequ,
mt_delete TYPE STANDARD TABLE OF zaf_dt_pricerequ.
ENDCLASS.
CLASS zcl_af_pricerequ IMPLEMENTATION.
METHOD get_instance.
IF mo_instance IS INITIAL.
mo_instance = NEW zcl_af_pricerequ( ).
ENDIF.
ro_instance = mo_instance.
ENDMETHOD.
METHOD create_data.
DATA:
ls_db TYPE zaf_dt_pricerequ,
lv_timestamp TYPE timestampl.
GET TIME STAMP FIELD lv_timestamp.
LOOP AT entities ASSIGNING FIELD-SYMBOL(<fs_entity>).
CLEAR ls_db.
ls_db = VALUE #(
client = sy-mandt
request_id = <fs_entity>-RequestId
material = <fs_entity>-Material
sales_org = <fs_entity>-SalesOrg
customer = <fs_entity>-Customer
current_price = <fs_entity>-CurrentPrice
new_price = <fs_entity>-NewPrice
currency = <fs_entity>-Currency
valid_from = <fs_entity>-ValidFrom
reason = <fs_entity>-Reason
status = ‘NEW’
changed_at = lv_timestamp
changed_by = sy-uname
created_at = lv_timestamp
created_by = sy-uname
).
APPEND ls_db TO mt_create.
APPEND VALUE #(
%cid = <fs_entity>-%cid
RequestId = <fs_entity>-RequestId
%is_draft = <fs_entity>-%is_draft
) TO mapped-zaf_i_pricerequ.
ENDLOOP.
ENDMETHOD.
METHOD update_data.
DATA:
ls_db TYPE zaf_dt_pricerequ,
lv_timestamp TYPE timestampl.
GET TIME STAMP FIELD lv_timestamp.
LOOP AT entities ASSIGNING FIELD-SYMBOL(<fs_entity>).
SELECT SINGLE FROM zaf_dt_pricerequ
FIELDS *
WHERE request_id = @<fs_entity>-RequestId
INTO @LS_db.
IF sy-subrc <> 0.
APPEND VALUE #(
%key-RequestId = <fs_entity>-RequestId
) TO failed-zaf_i_pricerequ.
CONTINUE.
ENDIF.
IF <fs_entity>-%control-Material = if_abap_behv=>mk-on.
ls_db-material = <fs_entity>-Material.
ENDIF.
IF <fs_entity>-%control-SalesOrg = if_abap_behv=>mk-on.
ls_db-sales_org = <fs_entity>-SalesOrg.
ENDIF.
IF <fs_entity>-%control-Customer = if_abap_behv=>mk-on.
ls_db-customer = <fs_entity>-Customer.
ENDIF.
IF <fs_entity>-%control-CurrentPrice = if_abap_behv=>mk-on.
ls_db-current_price = <fs_entity>-CurrentPrice.
ENDIF.
IF <fs_entity>-%control-NewPrice = if_abap_behv=>mk-on.
ls_db-new_price = <fs_entity>-NewPrice.
ENDIF.
IF <fs_entity>-%control-Currency = if_abap_behv=>mk-on.
ls_db-currency = <fs_entity>-Currency.
ENDIF.
IF <fs_entity>-%control-ValidFrom = if_abap_behv=>mk-on.
ls_db-valid_from = <fs_entity>-ValidFrom.
ENDIF.
IF <fs_entity>-%control-Reason = if_abap_behv=>mk-on.
ls_db-reason = <fs_entity>-Reason.
ENDIF.
IF <fs_entity>-%control-Status = if_abap_behv=>mk-on.
ls_db-status = <fs_entity>-Status.
ENDIF.
ls_db-changed_at = lv_timestamp.
ls_db-changed_by = sy-uname.
APPEND ls_db TO mt_update.
ENDLOOP.
ENDMETHOD.
METHOD delete_data.
LOOP AT keys ASSIGNING FIELD-SYMBOL(<fs_key>).
SELECT SINGLE request_id
FROM zaf_dt_pricerequ
WHERE request_id = @<fs_key>-RequestId
INTO @DATA(lv_request_id).
IF sy-subrc <> 0.
APPEND VALUE #(
%key-RequestId = <fs_key>-RequestId
) TO failed-zaf_i_pricerequ.
CONTINUE.
ENDIF.
APPEND VALUE #(
client = sy-mandt
request_id = <fs_key>-RequestId
) TO mt_delete.
ENDLOOP.
ENDMETHOD.
METHOD read_data.
IF keys IS INITIAL.
RETURN.
ENDIF.
SELECT
RequestId,
Material,
SalesOrg,
Customer,
CurrentPrice,
NewPrice,
Currency,
ValidFrom,
Reason,
Status,
ChangedAt,
ChangedBy,
CreatedAt,
CreatedBy
FROM zaf_i_pricerequ
FOR ALL ENTRIES IN @Keys
WHERE RequestId = @Keys-RequestId
INTO CORRESPONDING FIELDS OF TABLE @result.
ENDMETHOD.
METHOD save_data.
IF mt_create IS NOT INITIAL.
INSERT zaf_dt_pricerequ FROM TABLE @MT_create.
ENDIF.
IF mt_update IS NOT INITIAL.
UPDATE zaf_dt_pricerequ FROM TABLE @MT_update.
ENDIF.
IF mt_delete IS NOT INITIAL.
DELETE zaf_dt_pricerequ FROM TABLE @MT_delete.
ENDIF.
ENDMETHOD.
ENDCLASS.
Step 12: Create the Service Definition
@EndUserText.label: ‘Price Change Request UI Service’
define service ZAF_SD_PRICEREQU {
expose ZAF_C_PRICEREQDF as PriceRequest;
}
Step 13 : Create the Service Binding
The Binding Type has to be ODATA V4
Result
User A is trying to create the record
Fig 2 — Create Form (Draft in progress): A new Price Change Request being filled in. Request ID PR100021 is already assigned via early numbering, and the “Draft updated” message confirms the entered fields are being persisted to the draft table before the final Create button is clicked.
Fig 3 — Draft Table Data Preview: Confirming PR100021 is correctly persisted in the draft table (zaf_df_pricedf) with every field intact — proof that the draft query view and DCL setup allow this data to be read back correctly, rather than showing up blank as in the original bug.
Now whenever other user (User B) tries to check for the record, which is in draft table, for that User B the draft table record will not be visible.
But for the user (User A), the record in the draft table is visible. So only User A can make changes to the record which is in the draft table.
Conclusion
This blog walked through implementing a draft-enabled RAP business object for a Price Change Request scenario, with particular focus on securing draft data using a Draft Query View.
The key takeaway is that draft-enabled RAP objects work perfectly well without a draft query view — but doing so leaves draft data effectively unprotected by DCL, since access restrictions defined on the active CDS view entity do not automatically extend to draft instances. The query addition in the behavior definition closes this gap by giving draft reads their own dedicated CDS view, which can carry its own DCL role.
A second, less obvious lesson from this implementation: filtering draft data by “ownership” needs to use framework-managed data (via an association to I_DraftAdministrativeData), not custom business fields like CreatedBy — because business fields are only populated once your own class logic runs, which happens later than the moment the draft row is actually created.
Together, these two points are the core of what a Draft Query View is for, and why it matters beyond just being a syntax addition in the behavior definition.
“}]]
Read More Technology Blog Posts by Members articles
#abap