
上一周我一直在跟 CDS View 表函数较劲。对象本身不复杂CDS 里一个define functionAMDP 类里一个带BY DATABASE FUNCTION的实现方法二者一对绑在一起就能在 Open SQL 里调用 HANA SQLScript 的能力。复杂的是数量——当项目里要处理的不止一张表而是七八个模块、二十几套来源数据时重复劳动会反噬你的耐心。我最初的方案是在 Eclipse 里手工一个个建。建到第五个我停下来了决定用 SAP XCO 库写一个生成器一次性把 CDS 表函数和对应的 AMDP 类全部生成出来。这篇就把原理、代码骨架和踩坑记录都摊开讲希望对正在批量做 CDS 扩展开发的同行有参考价值。1. 手动建表函数的重复劳动有多磨人1.1 Eclipse 和 SE24 之间来回跑的一天先复盘一下手工建表函数的标准动作你才能理解为什么我要写生成器。在 Eclipse ADT 里新建一个 Core Data Services 数据定义模板选 Table Function。第一行先写三个注解functions.handler指向一个还不存在的处理类AccessControl.authorizationCheck: #NOT_REQUIRED先关掉权限检查ClientHandling.algorithm: #NONE告诉框架不要自动注入 client 条件。然后定义返回视图的字段结构保存激活。切到 SE24开始建 AMDP 类。类必须是 public、final还要INTERFACES if_amdp_marker_hdb这个标记接口没有它编译器不认这是 AMDP。接着添加方法方法名的写法是CLASS-METHODS get_data FOR TABLE FUNCTION ztf_demo这一步等于把类方法和表函数焊死在一起。然后进方法实现写METHOD get_data BY DATABASE FUNCTION FOR HDB LANGUAGE SQLSCRIPT OPTIONS READ-ONLY USING ...下面跟着一段 SQLScript。保存激活。如果表函数那边注解里的类名方法名和你这边任何一个字母不一致报错回去改。如果 SQLScript 返回的字段结构和表函数声明的不一致报错回去改。如果USING列表少写了一张表运行时直接 dump继续改。一张表函数运气好十几分钟运气不好一个小时都出不来。单看一次并不吓人吓人的是后面还有几十个。1.2 当你要的不是一张表而是一批表我这个项目的触发点是一批物料凭证和历史库存的快照计算。底表结构相近但明细口径各不相同有的要按公司代码和物料号聚合有的要按期间重算可用量有的要关联序列号状态表做 EDEL 更新逻辑的模拟。这些逻辑 Open SQL 写起来很别扭放在 HANA 上用 SQLScript 实现才顺手所以 CDS 表函数几乎是唯一选择。建第三张的时候我开始分心除了表名、字段、SQLScript 里的聚合条件其他代码几乎一模一样。建第五张的时候我做了决定——先停下手头的活写一个能用配置表驱动的生成器。理由很简单手工复制粘贴做的事情越机械出错概率越高出错的成本不是重新建一张而是你把一张字段完全搞错的表函数激活进了系统后面所有引用它的 CDS 视图和报表都会莫名其妙。那段时间项目组还在讨论一套 SAP MD07 相关的可用量重算方案里面要新加大约二十个口径不同的表函数。如果全部手工做两天起步如果生成器能跑通配置表里插二十行十分钟搞定。这笔账不需要算。1.3 XCO 库到底是什么为什么它能治这个痛点XCO 全称 eXtensible Composition是 SAP 为 ABAP 平台提供的一套面向开发对象的编程接口。你可以把它理解成 ABAP 仓库对象世界的标准 API用代码去创建、读取、修改、删除 ABAP 类、接口、CDS 视图、表函数、包、数据元素这些东西。以前我们想做批量生成最常见的手段是拼字符串然后用工具类直接灌源码。那套做法能用但对象元的元数据结构、激活时机的控制、不同对象之间的依赖关系全部靠你自己维护做多了非常痛苦。XCO 的价值在于给了你一套统一的对象模型让你像描述业务对象一样描述 Code 时代的开发对象。尤其在 BTP ABAP 环境里传统 SE80 你能做的操作被大幅限制XCO 反而成了代码生成这类需求的正路。它有学习门槛但门槛不在复杂度而在概念转换。转换过来之后你会发现写一个生成器的复杂度跟写一个普通 ABAP 报表差不了太多。2. CDS 表函数和 AMDP 类两个对象一条命2.1 表函数是 CDS 向外借力的接口普通 CDS 视图的本质是 SELECT 语句不管是define view还是define view entity最终都映射为数据库视图层的查询。它很强但有个边界Open SQL 表达不了的逻辑比如临时表、过程式循环、复杂的窗口函数编排它就不擅长了。表函数就是 CDS 世界里向外借力的机制。它仍然是一个 CDS 对象声明的时候定义了函数名、返回结构和注解看起来像视图但真正的数据获取逻辑被交给了背后注册的处理方法。这个处理方法不是在 SQL 层而是在 ABAP 类里用 SQLScript 实现。你可以把表函数理解成 CDS 层对外发布的函数签名签名长什么样调用方就怎么用真正处理数据的是签名背后的实施者。典型的表函数 DDL 长这样AccessControl.authorizationCheck: #NOT_REQUIRED ClientHandling.algorithm: #NONE functions.handler: ZCL_AMDP_DEMOGET_DATA EndUserText.label: Demo table function define function ZTF_DEMO returns view ZTF_DEMO as select from zsrc_table { key bukrs, key gjahr, dmbtr as amount, waers as currency }注意两点。第一functions.handler里写的类和方法名必须和 AMDP 类里声明的一模一样。第二returns view后面的字段结构是 AMDP 方法返回集合的约定两边必须一致。DDL 里的select from部分主要用来描述返回字段的元数据真正跑的逻辑在 AMDP 方法里。2.2 AMDP 类真正干活的 SQLScript 之家AMDP 类在 ABAP 里就是一个普通类但有特殊标记。它必须实现接口IF_AMDP_MARKER_HDB而且里面的方法通过FOR TABLE FUNCTION绑定到具体的表函数上方法实现必须用BY DATABASE FUNCTION FOR HDB语法声明为数据库函数。CLASS zcl_amdp_demo DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_amdp_marker_hdb. CLASS-METHODS get_data FOR TABLE FUNCTION ztf_demo. ENDCLASS. CLASS zcl_amdp_demo IMPLEMENTATION. METHOD get_data BY DATABASE FUNCTION FOR HDB LANGUAGE SQLSCRIPT OPTIONS READ-ONLY USING zsrc_table. RETURN SELECT bukrs, gjahr, SUM( dmbtr ) AS amount, waers FROM zsrc_table GROUP BY bukrs, gjahr, waers; ENDMETHOD. ENDCLASS.这里最容易被忽略的是USING列表。SQLScript 方法在 HANA 上执行它不能像 ABAP 那样随便去访问数据库表必须在USING后面把要读的表全部列出来HANA 才会授权这个函数过程访问那些表。少写一张表不会在激活时直接报错但运行时会很惨。2.3 绑定关系、激活顺序和那些容易漏掉的细节表函数和 AMDP 类之间的绑定关系是双向的方向位置写法表函数 → AMDPDDL 注解functions.handler: 类名方法名AMDP → 表函数方法声明CLASS-METHODS 方法名 FOR TABLE FUNCTION 表函数名两个方向只要有一处对不上激活或运行就会报错。我自己的习惯是先写 AMDP 类的方法定义把FOR TABLE FUNCTION先绑上去再写表函数 DDL两边对照着来这样因为手滑导致名字不一致的概率小很多。还有一个细节表函数方法必须是静态方法即CLASS-METHODS不能是实例方法。为什么因为表函数在 SQL 查询上下文中被调用没有实例化 ABAP 对象的机会。另外AMDP 类本身一般不需要实例化标记接口加静态方法就够了。这个细节在手工操作时不明显但在生成器里如果忘了把方法设为静态后面填实现代码的时候会卡住。激活顺序也是个隐性约束。严格来说表函数和它的 AMDP 处理器是你引用我、我引用你的循环关系所以不管先激活哪一个系统都会因为引用了尚未存在的对象而报错。手工建的时候你感觉不到因为 Eclipse 会帮你把两个对象一起放进工作区。用生成器就必须主动控制顺序最稳妥的办法是两个对象都创建为未激活状态然后按依赖顺序一起激活这一步我在第四节细说。3. XCO 库的对象模型学会一套心法走遍所有对象3.1 运行前提什么系统里才有 XCOXCO 库不是随便一台老 S/4 都能跑的。ABAP Cloud 模型、BTP ABAP 环境、以及较新的 ABAP Platform 版本里XCO 都是内置组件直接能用。比较老的内部部署系统就要先确认xco_cp_cds、xco_cp_abap这些入口类是否存在。最简单的检查方法在 SE24 里搜XCO_CP_CDS或XCO_CP_ABAP能看到类接口说明说明组件在。或者在 ABAP 编辑器里打一行DATA(lo_cds) xco_cp_cdsview( XXX ).语法能通过就有。如果系统里压根没有这些 API那这篇文章的写法就不适用得回到传统的源码拼接方案去。另外建议至少保留 Eclipse ADT。XCO 接口多且方法名长浏览器里的 SE24 看接口定义也可以用但没有 ADT 的自动补全和结构视图对着接口名找方法效率低不少。3.2 创建操作、新规格、执行XCO 的三板斧XCO 库对开发对象的操作模式高度统一核心就三步拿到对象发起操作定义规格执行操作。第一步用工厂方法拿到对象引用比如xco_cp_cdstable_function( ZTF_DEMO )拿到表函数对象xco_cp_abapclass( ZCL_AMDP_DEMO )拿到类对象。第二步对对象发起一个创建操作。操作接口提供了new_specification方法返回一个规格对象。规格就是你对这个对象最终样子的完整描述类名是什么、短文本是什么、包含哪些方法等等。第三步把规格填满然后调用操作的execute。XCO 会按照你的规格去创建真实的仓库对象处理激活和传输等底层细节。打个不严谨的比方XCO 的对象操作像是点外卖。对象是你要的那家店创建操作是打开下单页规格是你选的菜品、口味和备注execute 就是确认支付。在支付之前你随时可以改规格支付之后菜品就进了后厨开始做了。拿创建普通 CDS 视图举例你立刻能看出这个模式DATA(lo_cds_view) xco_cp_cdsview( ZI_DEMO ). DATA(lo_operation) lo_cds_view-create_operation( ). DATA(lo_spec) lo_operation-new_specification( ). lo_spec-set_short_description( Demo CDS View ). 这里通过 specification 的 content 方法设置完整 DDL 文本 lo_operation-execute( ).很多刚接触 XCO 的人会纠结为什么不能直接给源码文本然后激活其实 XCO 恰恰允许你这么做规格可以设置完整的内容/源码文本只是它把这件事包装成了对象化的 API。理解了这一点后面生成代码时你就不会迷失在方法名里。3.3 配置表驱动生成器先想清楚再写代码动手写生成器之前先设计配置表。我的做法很简单一张定制的表存每个表函数的参数每行代表一个待生成对象函数名CDS 表函数名称如 ZTF_MSEG_SUMAMDP 类名对应实现类如 ZCL_AMDP_MSEG_SUM方法名类里的静态方法如 FOR_MSEG_SUM来源表SQLScript 要读取的底表短文本对象的描述信息额外参数分组字段、聚合字段、过滤条件或者干脆放一段可替换的模板子句主报表的逻辑就三块读配置表 → 逐个调用生成器方法 → 汇总结果和错误信息。生成器方法内部按顺序处理 AMDP 类定义、AMDP 类实现、表函数 DDL、激活四个环节。先有这张配置表写代码的时候思路会清晰很多。因为 XCO 的方法多而细你如果边写边想下一步造哪个对象很容易把自己绕晕。配置表就是你的开发任务清单。4. 生成器核心实现一次造出表函数和 AMDP 类4.1 先搭 AMDP 类定义、接口、方法绑定我用一个ZCL_CDS_AMDP_GENERATOR来封装生成逻辑。这个方法接收函数名、类名、方法名、短文本等参数返回成功与否。生成 AMDP 类定义的核心代码大致是这样注意我用了概念性的方法名不同版本 XCO 的精确入口可能有差异但对象-操作-规格这个框架是稳定的实现时打开接口定义对照着补全即可METHOD create_amdp_class. DATA(lo_class) xco_cp_abapclass( iv_class_name ). DATA(lo_create) lo_class-create_operation( ). DATA(lo_spec) lo_create-new_specification( ). lo_spec-set_short_description( iv_short_text ). 类定义部分 DATA(lo_def) lo_spec-definition( ). lo_def-set_visibility( xco_cp_abapvisibility-public ). lo_def-set_final( abap_true ). lo_def-add_interface( xco_cp_abapinterface( IF_AMDP_MARKER_HDB ) ). 添加方法并绑定为 FOR TABLE FUNCTION DATA(lo_method) lo_def-add_method( iv_method_name ). lo_method-set_visibility( xco_cp_abapvisibility-public ). lo_method-set_kind( xco_cp_abapmethod_kind-class_method ). lo_method-set_for_table_function( iv_function_name ). 示意入口以实际接口为准 保存但先不激活后面统一激活 DATA(lo_result) lo_create-execute( ). ENDMETHOD.这个方法做两件关键事。第一给类加了IF_AMDP_MARKER_HDB接口这是 AMDP 类的身份证。第二方法被声明为静态方法并绑定了FOR TABLE FUNCTION这是让 ABAP 编译器知道这个类里有表函数处理器的关键步骤。如果你的 XCO 版本里没有set_for_table_function这种现成入口退一步的做法是先创建一个普通类并添加方法生成后直接用 ADT 或源码工具给方法补上FOR TABLE FUNCTION声明。两种方式最终产物一样只是自动化程度不同。我在实际开发中更倾向于把这个声明也放进生成的源码文本里因为 XCO 对 AMDP 方法声明的封装在不同版本间并不一致源码级别的替换最可控。4.2 灌入 SQLScript 实现模板替换是关键类定义造好了接下来是实现。AMDP 方法的实现本质上是一段带特殊前缀的代码代码里的BY DATABASE FUNCTION段完全可以用文本模板拼出来。我建议把实现文本做成模板把变化点留成占位符运行时替换。METHOD build_amdp_impl_source. 模板中的占位符{class_name} {method_name} {func_name} {source_table} lv_template |METHOD {method_name} BY DATABASE FUNCTION FOR HDB\n| | LANGUAGE SQLSCRIPT\n| | OPTIONS READ-ONLY\n| | USING {source_table}\n| | RETURN SELECT \n| | {field_list} \n| | FROM {source_table}\n| | {where_clause}\n| | {group_clause};\n| | ENDMETHOD.|. 生成器内部按配置表做字符串替换 lv_source replace( val lv_template sub |{class_name}| with lv_class_name ). ... 其余替换 ... 把实现代码写入规格的 implementation 部分 DATA(lo_class) xco_cp_abapclass( iv_class_name ). DATA(lo_update) lo_class-update_operation( ). DATA(lo_spec) lo_update-new_specification( ). lo_spec-implementation( )-add_method_implementation( iv_method_name iv_method_name iv_source lv_source ). lo_update-execute( ). ENDMETHOD.这里USING列表我直接用来源表名生成因为 SQLScript 方法里会访问这张表。如果 SQLScript 里还要关联其他表记得把关联表都加进USING。模板替换的好处是以后想调整返回字段的结构只需要在配置表里改字段清单完全不用动代码。4.3 生成 CDS 表函数并接上处理器AMDP 类和方法都已就位现在生成 CDS 表函数。这里我直接用 XCO 的表函数入口把完整 DDL 作为内容文本放进去METHOD create_table_function. 由配置表生成完整 DDL 文本 lv_ddl |AccessControl.authorizationCheck: #NOT_REQUIRED\n| |ClientHandling.algorithm: #NONE\n| |functions.handler: {class_name}{method_name}\n| |EndUserText.label: {short_text}\n| |define function {function_name}\n| |returns view {function_name}\n| | as select from {source_table}\n| | { {field_definitions} }\n|. DATA(lo_tf) xco_cp_cdstable_function( iv_function_name ). DATA(lo_create) lo_tf-create_operation( ). DATA(lo_spec) lo_create-new_specification( ). lo_spec-content( )-set_source( lv_ddl ). lo_create-execute( ). 记下激活顺序表函数要在 AMDP 已存在的前提下激活 INSERT INTO lv_activation_queue VALUE iv_function_name. ENDMETHOD.注意field_definitions要和 AMDP 方法返回的字段结构完全一致。我在生成器里专门做了一个一致性校验把配置表里的字段清单同时用于 AMDP 的RETURN SELECT和表函数 DDL 的返回结构从源头杜绝两边字段对不上的问题。顺便说一句functions.handler里的类名和方法名我直接拼进 DDL而不是手工维护可以少掉一大类低级错误。4.4 激活、传输请求与整体流程串接生成器主流程的串接顺序很关键。我的做法是先把 AMDP 类建出来再更新实现再建表函数最后统一激活。激活这一环在不同运行环境下表现不一样。BTP ABAP 环境里对象创建后默认进入激活状态xco 的execute会处理内部部署环境通常涉及传输请求所以要有可用请求号。我在生成器里加了一个参数iv_transport内部部署时必须有值BTP 环境传空就行。激活队列建议按依赖顺序处理。先激活 AMDP 类它的定义和实现都完整了再激活表函数。如果表函数激活时报错找不到处理类基本都是因为 AMDP 类还没激活或者方法名和注解不一致。生成器里可以捕获异常并把对象名连同原因记录到结果表方便批量跑完统一处理。5. 从能跑到跑得稳我踩过的五个坑5.1 对象命名和 30 字符限制ABAP 开发对象的名称最长 30 个字符。CDS 表函数名、AMDP 类名、方法名都在这个约束内。问题不在单个名字而在你自动拼接时容易产生超过限制的组合。例如你把类名定为ZCL_AMDP_FOR_TABLE_FUNC_MSEG_HISTORY_2024数一下字符数大概率超。超了之后 XCO 会在execute阶段报一个很晦涩的异常你根本看不出来是名字长度问题。我的建议是生成器里写一个简单的校验函数入参名字超出 28 字符直接拒绝并提示缩短。为什么留 2 个字符余量因为很多系统会在对象名后追加后缀做版本处理或者你可能后续要给方法加_IMP之类的后缀提前留余量可以避免被逼着改配置。5.2 循环引用式绑定先造谁都会报错表函数和 AMDP 类相互引用这给生成器带来了激活问题。如果你先建表函数再建 AMDP 类表函数激活时引用的处理类还不存在报错。如果反过来AMDP 类激活时它绑定的表函数也不存在同样报错。正确的策略是把两个对象都创建为未激活状态然后一起激活。我在生成器里用了一个最简单的队列机制不逐个执行激活而是把本次 batch 涉及的所有对象名收集起来全部创建完之后再统一激活。这样既解决了循环引用也顺便提升了批量生成的效率。如果你用的是 BTP ABAP 环境激活是自动的那就只需要保证生成顺序是类先、函数后剩下的交给框架。5.3 幂等性设计生成器不能长出双份对象生成器最大的敌人是重复运行。第一次跑得很愉快二十个对象全部生成成功。第二次你不小心点了输出XCO 试图再创建同名类系统会告诉你对象已存在。最尴尬的是配置表里某些行已经生成过了某些没有你还得人工去对比。从一开始就要做幂等。在创建之前用 XCO 的read_operation判断对象是否已存在如果存在就走update_operation更新不存在才走create_operation。这个判断成本很低但对使用体验帮助极大。生成器跑完后在结果列表里显示新建了几个、更新了几个、跳过几个一眼就能看出配置表的变化。5.4 SQLScript 的大小写、USING 列表和字段类型HANA SQLScript 对对象名大小写是敏感的。ABAP 开发中通常约定所有表名、字段名大写但你在生成器里拼 SQLScript 字符串时如果来源表名在配置表里是Zsrc_Table这种混合大小写拼出来在 HANA 上执行就可能找不到对象。我踩过一次后直接在配置表输入时就统一转成大写。USING列表同样容易出错。SQLScript 里除了主表你可能还要关联配置表、序列号状态表这些表都必须出现在USING里。生成器最好维护一个来源表集合字段运行时把所有表拼进USING列表别只放主表。还有个隐蔽问题字段类型映射。比如 AMDP 返回的QUAN字段在表函数 DDL 里如果用错了数据元素激活时会报类型不匹配。我建议配置表里同时维护字段名和数据元素名生成 DDL 和 SQLScript 时保持一致避免激活报错之后再来回试。5.5 云环境特性和授权注解如果你的目标是 BTP ABAP 环境表函数 DDL 里的AccessControl.authorizationCheck: #NOT_REQUIRED不是可写可不写的它决定 CDS 编译时是否套用 DCL 权限检查。手动建的时候 Eclipse 会提示但生成器拼文本时如果漏了激活直接失败。ClientHandling.algorithm: #NONE同理它告诉框架这张表函数内部自己处理 client 逻辑不要自动注入 client 条件。这两个注解作为固定前缀写进生成模板里能少踩两个坑。AMDP 类的权限方面因为OPTIONS READ-ONLY确保方法只读不会在运行时写入数据这在云环境里是硬性要求甚至可以说写上它不仅仅是习惯问题而是合规问题。6. 一套代码生成器能走多远6.1 把生成器接到配置界面生成器本身是一个 ABAP 类配置表是什么形态完全由你决定。你可以用 SM30 维护配置表也可以做一个简单的 ODATA 服务暴露出来让业务顾问自己加配置行。我在项目上的做法是做了一个配置维护界面字段包括函数名、类名、来源表、字段结构。业务顾问拿到新需求后不用懂 XCO 也不用懂 AMDP只要在维护界面里复制一行类似的配置改掉表名和字段点执行后台就自动批量生成。这比让他们找开发同事排期快太多了。6.2 同一个套路不止表函数XCO 的对象化模型一旦熟悉你会发现它适用于几乎所有开发对象。普通 CDS 视图、抽象实体、自定义实体、接口、数据元素、包、消息类套路完全一致拿对象 → 创建操作 → 填规格 → 执行。生成器的框架代码是可以复用的只需要把规格填充部分按对象类型换掉。我后来把这个生成器扩展到了普通 CDS 视图的批量生成实现成本非常低。因为视图 DDL 的模板比表函数还简单连 AMDP 类都不用生成。可以这么说XCO 让你用一套心法管理整个 ABAP 资产库而表函数生成只是它的小试牛刀。6.3 最小可复现骨架留给想动手的人如果你看完也想搞一个自己的生成器最小可复现的骨架是这几样东西一张配置表字段就是函数名、类名、方法名、来源表、短文本。一个配置维护视图哪怕是临时用 SE11 直接维护数据也行。一个 ABAP 类封装create_amdp_class、update_amdp_impl、create_table_function、activate_queue四个方法。一个后台报告读配置表循环调用生成器最后输出结果清单。先把这一套跑通再往里加字段映射校验、幂等更新、传输请求管理这些增强功能。我在本项目里从零到全部跑通大概花了一个下午加一个上午——前提是先理解 CDS 表函数和 AMDP 的绑定原理并且对 XCO 的对象-操作-规格模式有概念。最后再分享一个实际体会用 XCO 批量生成对象最大的收获不是省下的那点手工时间而是把开发对象变成了配置数据。对象多了配置表就是你的资产清单每次需求变更改配置比改代码快得多而且不容易出错。如果你也正在做大批量 CDS 扩展开发这套思路值得试一次。