SAP RAP报表示例:从CDS视图到Fiori的只读应用开发指南
2026/9/9 18:14:53 网站建设 项目流程

1. RAP报表示例到底在做什么:先搞清楚我们要解决的问题

做SAP开发的,这些年多少都会碰到这么个尴尬局面:业务方一句“给我出一张报表”,你第一反应可能是拼个ALV、挂个事务码,或者写个Query,再或者扯上BW那边搭模型。这个例子里的RAP,指的并不是说唱,而是SAP新一代的ABAP RESTful Application Programming Model,国内社区习惯简称RAP。它是SAP在ABAP平台(尤其是S/4HANA和BTP ABAP环境)上主推的、基于Fiori和OData的一种开发范式,严格来说叫Robotic Application Programming也好、RESTful Application Programming Model也好,都不会影响你理解这件事。

这个“RAP - 报表示例”的项目,本质上是拿RAP技术栈做一套查询类型的报表应用:底层是CDS视图建模,中间走行为定义和OData服务暴露,上层用SAP Fiori Elements自动生成列表报表界面。换句话说,你不需要手撸一个前端表格,只需把数据模型、行为和服务定义好,界面就能自动出来,这是RAP做报表最吸引人的地方。

这个项目适合谁?一类是刚接触S/4HANA、BTP ABAP Environment的顾问和开发,想找一条清晰的RAP落地路径;另一类是做了多年传统ABAP开发、面对新项目要求“必须用RAP或者Steampunk方式交付”的人,这个例子能帮你把脑子里的ALV惯性掰过来。你可能不需要它去处理上亿行数据的大数据场景,但在企业级ERP范围内的列表报表、状态跟踪、简单汇总这种日常需求,它足够扎实、干净、可维护。

2. 报表示例的数据模型设计:CDS视图不是随便建两行就完事

2.1 从数据库表到基础视图CDS View

任何RAP报表,第一步都是建立数据模型。这里的“数据模型”在绝大多数情况下指的就是CDS视图。你要先搞清楚一件事:RAP里的实体,既可以基于数据库表,也可以基于CDS视图,但作为报表场景,我建议你直接在CDS视图层做建模,因为它的语义化能力、关联能力、字段计算能力更适合查询场景。

拿报表示例来说,假设我们要做一个“采购订单执行情况报表”,先得有基础的采购订单抬头和行项目数据。底层表是EKKO(采购订单抬头)和EKPO(行项目),但你不能把这两张数据库表直接暴露给Fiori,这既不安全也不优雅。于是第一层就是建一个基础CDS视图,比如:

@AbapCatalog.sqlViewName: 'ZRAPPO_V01' @AbapCatalog.compiler.compareFilter: true @AccessControl.authorizationCheck: #CHECK @EndUserText.label: '采购订单基础视图' define root view entity ZI_PurchaseOrderBase as select from ekko as header inner join ekpo as item on header.ebeln = item.ebeln { header.ebeln as PurchaseOrder, header.lifnr as Supplier, header.bsart as DocType, header.erdat as CreatedOn, item.ebelp as ItemNo, item.matnr as Material, item.menge as Quantity, item.netpr as NetPrice, item.netwr as NetValue, item.loesz as DeletionIndicator }

这里有个容易忽略的点:@AccessControl.authorizationCheck: #CHECK表示这个视图会走权限检查,如果你还没有建PFCG角色和权限对象,报表一上去可能直接查不到数据。很多新手会把这里改成#NOT_REQUIRED来绕过权限,图一时省事,结果上线后涉及敏感采购单价时就成了合规问题。报表示例里,我建议第一版先保留#CHECK,把权限对象一起做成标配。

基础视图建好之后,先激活,确认能查出数据,再做下一步。这一步的核心不在于CDS语法多花哨,而在于你要想清楚:你最终要在报表界面显示哪些字段,业务过滤条件有哪些,是否需要汇总行。

2.2 消费视图与查询视图的拆分思路

RAP里有个很容易让人犯迷糊的概念,就是“消费视图”和“查询视图”的拆分。说实话,我在实际项目里见过不少人把字段、关联、计算逻辑全堆在同一个CDS视图里,最后视图变成一团乱麻。报表示例这种规模虽小,但也应该养成拆开的习惯。

我的习惯是两层结构:底层是纯粹的“数据提供者”,只负责把表和字段、关联关系整理好,不做太多复杂的计算;上层是“消费视图”,面向最终报表界面,加上汇总逻辑、参数、字段注解,甚至可以定义@Consumption.valueHelp之类的UI提示属性。比如上面那个采购订单基础视图,可以再套一层消费视图:

@AbapCatalog.sqlViewName: 'ZRAPPO_V02' @EndUserText.label: '采购订单执行报表' define root view entity ZI_PurchaseOrderQuery as select from ZI_PurchaseOrderBase as base { key base.PurchaseOrder, base.ItemNo, base.Supplier, base.DocType, base.CreatedOn, base.Material, base.Quantity, base.NetPrice, base.NetValue, @EndUserText.label: '未清数量' base.Quantity as OpenQuantity }

拆开有什么好处?第一,出问题时可以快速定位是底层取数错了还是上层计算错了;第二,底层视图可以复用到其他应用,不用再造一遍;第三,上层加语义注解时不影响底层的稳定性。报表场景下,“查询视图”通常就是你RAP项目里实体(Entity)的一部分,后面行为定义和服务定义都要引用它。

2.3 参数处理与关联关系要注意的几个点

报表十有八九需要参数,比如供应商范围、单据日期范围、物料号。CDS视图里可以直接用参数给查询加输入,RAP里也能通过@Consumption.filter把参数暴露成筛选条件。实践中最容易踩的坑是参数与关联组合使用时的性能问题——如果你在消费视图里再加了很多inner join,参数过滤条件的作用顺序会直接影响执行计划,但ABAP开发这边不一定能看到HANA的完整执行计划,所以建议别在一层CDS里放太多关联。

另一个细节是key字段的处理。RAP实体要求有且至少有一个key字段,而且这个key字段在报表场景里往往是逻辑上的“唯一标识”,而不是数据库主键。比如采购订单+行项目组合起来是唯一的,你就在查询视图里把PurchaseOrderItemNo定义成key。如果你不定义key,后面RAP行为的readupdate方法会非常别扭,因为框架拿不到主键来定位数据。

还有一个小技巧:报表场景里很多字段只需要显示、不需要编辑,你可以在CDS视图上直接用@Consumption.hidden: true把某些字段从Fiori界面藏掉,比如内部技术字段、创建时间戳等。这样可以保持界面干净,又不影响底层数据结构。

3. 行为定义与只读场景:报表为什么也要定义行为

3.1 RAP行为定义的基本结构(BDEF)

一说到行为定义,很多人第一反应是“RAP不是用于CRUD的吗,我只做个报表查询,为什么还要定义行为?”其实RAP的行为定义(Behavior Definition,BDEF)在查询场景里同样不可省,哪怕你只做只读报表,框架也要求你有一个最小化的行为定义,否则服务暴露不出来。

行为定义文件通常长这样:

managed implementation in class zcl_bp_purchaseorder unique; strict ( 2 ); define behavior for ZI_PurchaseOrderQuery alias PurchaseOrder persistent table ekko lock master authorization master ( instance ) { field ( readonly ) PurchaseOrder, ItemNo, Supplier, Material, Quantity, NetPrice, NetValue; readonly; }

注意,最后那个readonly;是关键。它告诉RAP框架,这个实体下所有字段、所有操作都是只读的,框架会自动帮你挡掉所有写操作。对于报表场景,这是最安全的状态,也从根上杜绝了误改业务数据的问题。实际项目里,我见过有些开发为了少写点代码,干脆不写readonly而是让框架生成默认行为,结果Fiori界面上出现了编辑和创建按钮,用户一点就报错,反而增加混乱。

persistent table ekko这里得解释一下。如果你是定义在CDS消费视图上,而且视图直接对应物理表,可以指定持久化表;但如果视图经过多层关联、计算,映射关系已经不再简单,RAP就不允许你指定持久化表,而是要求你提供完整的early numberingupdatecreate等Draft相关方法。这时候别硬写,直接把persistent table去掉,把行为定义改成非持久化模式会更合适。

3.2 报表场景下的行为实现技巧

报表场景中,行为定义里最常用的其实是“计算字段”和“动作(Action)”。还是拿采购订单报表举例,你可能需要一张报表显示“逾期天数”“交货状态”。这些字段如果完全用CDS表达式写,会很痛苦,尤其涉及日期运算、状态判断时。更优雅的做法是在行为定义里声明一个计算字段,然后在行为实现类里写ABAP逻辑。

行为定义里加一个:

field ( readonly ) OverdueDays, Status;

然后在行为实现类中(通常是zcl_bp_purchaseorder)实现determine方法。这里注意ABAP语法细节,RAP里计算字段在只读场景一般用determine action方式实现,或者你用@ReadOnly配合class方法自动计算。下面是示例片段:

METHOD get_instance_features. " 示例:计算逾期天数 READ ENTITIES OF zi_purchaseorderquery IN LOCAL MODE ENTITY PurchaseOrder FIELDS ( PurchaseOrder ItemNo CreatedOn Quantity ) WITH CORRESPONDING #( it_key_tab ) RESULT DATA(lt_po). LOOP AT lt_po INTO DATA(ls_po). DATA(lv_overdue) = cl_abap_context_info=>get_system_date( ) - ls_po-CreatedOn. IF lv_overdue > 30. APPEND VALUE #( id = ls_po-PurchaseOrder %param-Status = '逾期' ) TO it_overdue. ENDIF. ENDLOOP. ENDMETHOD.

这里边有个经验之谈:尽量别在CDS视图里用DATS_DAYS_BETWEEN之类函数做复杂逻辑,性能并非最优,而且可读性差。放到行为实现里之后,测试、调试、断点都方便很多。

3.3 权限控制与会话管理

行为定义里有个重要属性是authorization master ( instance ),它表示实例级权限。报表不是所有人都能看所有数据,比如采购人员通常只能看自己负责的采购组织。RAP支持在行为实现里通过get_instance_authorizations方法返回实例权限,也可以配合@AccessControl.authorizationCheck: #CHECK配合CDS access policy做行级过滤。

最简单的做法是CDS访问控制视图(DCL):

@EndUserText.label: '采购订单权限控制' define behavior for ZI_PurchaseOrderQuery authorization master ( instance ) { readonly; }

真正实现行级过滤记得建DCL:

@EndUserText.label: '采购订单行级权限' define role ZRAP_PO_ROLE { grant select on ZI_PurchaseOrderQuery where ( Supplier ) = aspect pfcg_auth( 'KONZERN', 'SUPPLIER', 'ACTVT' ); }

这里面pfcg_auth的绑定比较绕,新手容易卡住。原则上,需要确保你的SAP用户有对应的PFCG角色,并且角色里分配了KONZERNSUPPLIER这两个权限字段的合适值,否则报表刷新不出数据。RAP的权限判断是后端强制的,比Fiori前端的按钮隐藏可靠得多。

4. 服务暴露与消费:从OData服务到Fiori报表页面的完整链路

4.1 服务定义与服务绑定的配置要点

建好查询视图和行为定义之后,RAP报表还差最关键一步:把它暴露成OData服务。在ABAP环境(Steampunk)里,你需要建立两个对象:Service Definition(服务定义)和Service Binding(服务绑定)。

服务定义很简单,一条语句:

define service ZUI_PurchaseOrderReport { expose ZI_PurchaseOrderQuery as PurchaseOrder; }

服务绑定时,选择绑定类型为OData V2或者OData V4。报表/Fiori Elements一般推荐OData V4,因为支持的UI注解更丰富,V4版本的响应模型在新SAP BTP项目里支持也更完整。如果你还在老S/4HANA on-premise上用旧版本,V2可能更稳妥。绑定界面上有一个“暴露名称”的选项,通常用ZUI_PURCHASEORDER_REPORT这样的可读名称,便于Fiori前端引用。

绑定完成后要注意激活顺序。常见报错顺序是:CDS视图激活失败 -> 行为定义激活失败 -> 服务定义激活失败 -> 服务绑定激活失败。所以任何时候改了视图层,先逐层激活,别一次性点“激活全部”。

4.2 用CDS-based Fiori Elements搭建列表报表界面

服务激活之后,Fiori Elements的界面是靠CDS视图上的注解自动生成的。你需要回到查询视图里加UI注解,比如列表展示的列顺序、筛选字段、状态图例等。示例:

@UI: { headerInfo: { typeName: 'Purchase Order', typeNamePlural: 'Purchase Orders' }, presentationVariant: [ { sortOrder: [{ by: 'CreatedOn', direction: #DESC }] } ] } @UI.selectionField: [{ position: 10, element: 'Supplier' }] @UI.selectionField: [{ position: 20, element: 'CreatedOn' }]

在Fiori Elements里,“筛选字段”“列表列”“默认排序”都是通过注解控制的,不需要前端开发。生成页面后,你会得到非常标准的列表报表:上方筛选区,下方表格,支持导出Excel、调整列宽、排序、分页。如果UI还需要状态红黄绿(Semantic Colors),可以通过@UI.lineItem配合@UI.chipList注解来实现。

这里推荐的做法是在SAP BSP或Fiori Launchpad里新建一个“SAP Fiori Elements”类型的应用,数据源选择刚才的OData服务的V4端点。如果你用的是Business Application Studio(BAS),也可以直接用CDS-based Fiori Elements模板,选择对应的数据模型,一键生成预览。

4.3 报表导出、跳转、参数默认值等扩展点

Fiori Elements虽然自动生成界面,但业务上总有些扩展要求,比如报表导出、跳转到采购订单详情、默认过滤值等。

第一是导出。Fiori Elements列表自带“导出为Excel”的按钮,这是标准功能,无需额外开发。唯一要注意的是导出行数受后端限制,如果超过限制会提示用户,你要么调大服务绑定的分页大小,要么通过Fiori Elements的导出配置自定义上限。

第二是跳转。报表列表里点某一笔订单,一般想跳到对应采购订单的应用页面。这时候需要在CDS视图给相关字段加上@UI.identification@Consumption.valueHelpDefinition注解,并在Fiori应用配置里配置Navigation,或者用“Link”类型的字段。简单做法是不做页面跳转,而是加一个自定义Action,跳转到SAP GUI事务代码ME23N。这个属于Smart Template之外的个性化动作,需要写一点前端扩展代码,但RAP架构里更推荐你用“Action”后端实现再通过注解暴露给前端。

第三是参数默认值。比如报表默认只看本月的单据,用户希望每次打开自动过滤。Fiori Elements支持在manifest.json里定义filterDefaults;如果你用的是RAP模型驱动方式,在行为定义里也可以设置一个初始值查询,把系统日期传进去做默认条件。我建议用后端的参数默认值,因为前端默认值只影响界面,而后端默认值可以保证任何客户端访问OData时都一致。

5. 实操过程:从零到一个可用的RAP报表应用

5.1 环境准备与项目创建

这个例子最好是跑在SAP BTP ABAP Environment(Steampunk)或者S/4HANA Cloud上,因为RAP是这些环境的默认开发范式。老一点S/4HANA 1909 plus on-premise通过Add-on也能支持,但步骤差异较大。下面以BTP ABAP Environment为例,我估摸这也刚好是新手最常选的环境。

控制台打开 Eclipse ADT,当前版本最好不低于某一年份的Feature Pack。先创建一个Package,比如ZRESP_REPORTING。包类型选择“Development”,并且要在包属性里设置Software Component: HOME_OBJECTS。如果你在本地系统开发,这个设置可以忽略,但放到BTP上好几个人一起开发时,软件组件不同会导致传输混乱。

创建好包后,逐个创建上面提到的CDS视图、行为定义、服务定义、服务绑定,最后给用户或权限角色赋上对应的OData服务访问权。环境里还需要在“Communication Arrangement”里配置对应的Communication User,指定入站服务,Fiori应用才真正能连上OData。

5.2 演进版本:第一版只读查询

我的习惯是不要一上来就把所有字段、注解、权限全部做完。先把最核心的只读查询跑通,验证RAP端到端的链路。具体步骤:

  1. 创建基础CDS视图(ZI_PurchaseOrderBase)。
  2. 创建消费CDS视图(ZI_PurchaseOrderQuery),加上@UI.lineItem@UI.selectionField注解。
  3. 创建最小行为定义,只写readonly;
  4. 创建服务定义,expose ZI_PurchaseOrderQuery as PurchaseOrder;
  5. 创建服务绑定,选OData V4,激活后预览URL。
  6. 确认能拿到JSON数据,再进Fiori Elements预览。

这一步跑通了,整个RAP报表的基本骨架就出来了,后面再扩展权限、计算字段、跳转都不会有方向性问题。

5.3 手工代码:处理计算字段与判断逻辑

当遇到复杂计算时,RAP允许在行为实现类里写ABAP。创建行为定义后,ADT会自动帮你生成一个行为实现类的框架(如果你的定义里用了implemented by class的话)。在这个类里,你可以实现各种方法,注意所有方法几乎都要配合READ ENTITIESMODIFY ENTITIES这类RAP语句——它们跟传统的SELECTUPDATE完全不同。

以“状态”为例,最终报表里要显示“正常”“逾期”“已完成”,我的逻辑是:数量为0时状态是已完成;距创建日期超过30天并且数量大于0为逾期;其余正常。在CDS硬写这种三段判断比较绕,于是在行为实现类里:

METHOD get_instance_features. READ ENTITIES OF zi_purchaseorderquery IN LOCAL MODE ENTITY PurchaseOrder FIELDS ( PurchaseOrder Quantity CreatedOn ) WITH CORRESPONDING #( it_key_tab ) RESULT DATA(lt_data). LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<fs_data>). CASE <fs_data>-Quantity. WHEN 0. APPEND VALUE #( id = <fs_data>-PurchaseOrder %param-Status = '已完成' ) TO reported-purchaseorder. WHEN OTHERS. IF cl_abap_context_info=>get_system_date( ) - <fs_data>-CreatedOn > 30. APPEND VALUE #( id = <fs_data>-PurchaseOrder %param-Status = '逾期' ) TO reported-purchaseorder. ELSE. APPEND VALUE #( id = <fs_data>-PurchaseOrder %param-Status = '正常' ) TO reported-purchaseorder. ENDIF. ENDCASE. ENDLOOP. ENDMETHOD.

这个方法里最容易被忽略的点是reported-purchaseorder%param这种赋值方式。它不是直接更新实际数据,而是“上报”一个计算结果给RAP框架,框架再把结果合并到输出里。很多从传统ABAP转过来的人在这里会下意识用MODIFY去更新内部表,结果怎么都对不上。要记住RAP的计算字段思路是“声明式”的,不是你主动写值,而是运行时框架来问你取值。

5.4 部署与联调

在BTP ABAP环境里,Fiori应用要么走SAP Fiori launchpad的“自定义应用”注册,要么通过BAS做前端部署。最省事的方式是从服务绑定里复制OData服务的Service URL,然后在Fiori Launchpad里创建一个Tile,直接指向你生成的Fiori Elements应用。

联调时要验证几个点:用户是否能看到筛选区;列表列宽和默认排序是否符合预期;点击某一行是否能触发导航或动作。如果用户登录之后什么数据都没有,优先查Communication User的权限和DCL定义,别急着怀疑CDS。如果是数据能显示但明显不对,比如金额字段差几位、数量带了很多小数,那通常是CDS视图里类型转换和精度定义出了问题。

6. 常见问题与排查技巧实录

6.1 常见报错速查表

报错现象可能原因排查方向
服务绑定激活失败行为定义未激活或实体缺失先激活CDS视图和行为定义,再看服务定义
OData返回403或500权限对象未配置或DCL阻断检查Communication User权限、DCL角色绑定
Fiori界面空白,列出不来CDS视图annotation不识别检查@UI.lineItem是否写对,view activation是否有warning
行为实现类无法激活ABAP语法或RAP接口不匹配检查方法签名,READ ENTITIES语句是否对应正确实体
报表只能查少量数据OData分页大小限制服务绑定里调整分页大小或启用@Consumption分页配置

这个速查表我建议直接打印出来贴工位。不是我夸张,做RAP项目时,七八成的时间是在跟上面这些报错纠缠,尤其是DCL权限和激活顺序的问题,几乎每个项目都会遇到。

6.2 没有数据或数据不正确的定位顺序

先看CDS视图本身能不能查出数据。在ADT里选中CDS视图直接按F8,预览数据,这一步能过滤掉大量“权限黑盒”和“OData映射”问题。如果原生预览没有数据,改SQL条件;如果原生预览有数据,但Fiori没数据,百分百是权限或服务绑定问题。

我再分享一个实战中的土办法:在行为实现类里用一个静态方法打个断点,然后用Postman调OData接口,看断点有没有触发。如果断点都没进来,说明请求根本没到你RAP后端;如果断点进来了,就看返回的reportedresult结构是否为空。这个方法比在Fiori浏览器里看Network面板要直观很多,尤其是在服务端才能看到数据过滤规则的情况下。

6.3 服务激活失败与锁定问题

RAP开发中经常有个恼人事:明明改了一个CDS注解,但点激活一直提示失败,原因是对象被其他任务锁定。因为多人共用BTP环境时,对象锁跟传输请求绑定,如果你在旧传输请求里创建了某个对象,后来切到新请求,旧对象依然被锁,激活就报“object is temporarily locked”。

解法不复杂。要么打开ADT的“Transport Organizer”视图,找到所有锁定该对象的传输请求,把对象释放或把当前工作台切换回那个请求;要么干脆用系统管理员的身份,在Admin工具里执行对象解锁。这个看似简单的问题,实际影响工作效率很大,我见过有人卡了一上午。

还有一个秒招:如果你只是想快速跑个demo,不关心传输,可以在ABAP环境里关掉“Transport Request”校验,在开发服务里定义“Local Object”,这样对象不会被锁定,激活也快,但仅限于个人开发环境,团队协作千万别这么干。

6.4 调试RAP作业的独家技巧

最后说点文档里不常写的。RAP的调试思路跟传统ABAP报表差距很大,新手最容易懵。传统报表你可以在START-OF-SELECTION断点,然后逐行看内存表数据;RAP模式下,框架帮你做了太多黑盒调度,断点打在行为实现类里的时候,某些方法甚至压根不是每次查询都执行。比如get_instance_features只在Fiori需要该特性字段时才会调用,如果你在界面上根本没放特性列,方法可能不会被触发,断点就永远不弹。

因此,调试RAP报表前,先明确你的断点打在哪个阶段:是在CDS的字段计算?行为实现的determine?还是服务绑定层?如果是CDS计算有问题,直接用SQL预览+SQL跟踪更直接;如果是行为实现逻辑问题,就在ADT里用“ABAP Debugger”从OData请求进入,看请求链路栈。点击“Navigate to”上的服务URL时,用浏览器的开发者工具先看一下OData请求的URL参数,哪些字段被请求了,行为实现大概率就调用哪些。

另外值得提醒的是,RAP的日志不像ALV报表有S事务代码可以看整个程序输出。建议你在行为实现类里加上ABAP application log(SLG0/SLG1),每次跑数都把结果数量、耗时、异常写进去。正式环境不会让你随便挂断点,有日志你才有依据去排查问题。这个习惯我建议从第一个RAP项目就开始养成。

我在实际项目里最深的体会就是:RAP报表能不能做好,不在你会不会写那两行CDS,而在你愿不愿意承认“数据建模、权限、服务、前端这一条链路是个系统工程”。别看Fiori Elements最后给出一张挺漂亮的表格,背后环环相扣,狡猾得很。但你只要把一个只读报表跑通、跑顺,RAP的其他能力——新增、编辑、草稿、动作——其实都是在同一个框架上往深处走,不会再有那种“完全新范式”的门槛感。这个报表示例看起来小,但对团队里想要上手RAP的人来说,是一条很值得反复踩的入门路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询