上一周我一直在跟 CDS View 表函数较劲。对象本身不复杂:CDS 里一个define function,AMDP 类里一个带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_DEMO=>GET_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 类之间的绑定关系是双向的:
| 方向 | 位置 | 写法 |
|---|---|---|
| 表函数 → AMDP | DDL 注解 | @functions.handler: '类名=>方法名' |
| AMDP → 表函数 | 方法声明 | CLASS-METHODS 方法名 FOR TABLE FUNCTION 表函数名 |
两个方向只要有一处对不上,激活或运行就会报错。我自己的习惯是先写 AMDP 类的方法定义,把FOR TABLE FUNCTION先绑上去,再写表函数 DDL,两边对照着来,这样因为手滑导致名字不一致的概率小很多。
还有一个细节:表函数方法必须是静态方法,即CLASS-METHODS,不能是实例方法。为什么?因为表函数在 SQL 查询上下文中被调用,没有实例化 ABAP 对象的机会。另外,AMDP 类本身一般不需要实例化,标记接口加静态方法就够了。这个细节在手工操作时不明显,但在生成器里如果忘了把方法设为静态,后面填实现代码的时候会卡住。
激活顺序也是个隐性约束。严格来说表函数和它的 AMDP 处理器是"你引用我、我引用你"的循环关系,所以不管先激活哪一个,系统都会因为引用了尚未存在的对象而报错。手工建的时候你感觉不到,因为 Eclipse 会帮你把两个对象一起放进工作区。用生成器就必须主动控制顺序,最稳妥的办法是两个对象都创建为未激活状态,然后按依赖顺序一起激活,这一步我在第四节细说。
3. XCO 库的对象模型:学会一套心法,走遍所有对象
3.1 运行前提:什么系统里才有 XCO
XCO 库不是随便一台老 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_cds=>view( 'XXX' ).,语法能通过就有。如果系统里压根没有这些 API,那这篇文章的写法就不适用,得回到传统的源码拼接方案去。
另外,建议至少保留 Eclipse ADT。XCO 接口多且方法名长,浏览器里的 SE24 看接口定义也可以用,但没有 ADT 的自动补全和结构视图,对着接口名找方法效率低不少。
3.2 创建操作、新规格、执行:XCO 的三板斧
XCO 库对开发对象的操作模式高度统一,核心就三步:拿到对象,发起操作,定义规格,执行操作。
第一步,用工厂方法拿到对象引用,比如xco_cp_cds=>table_function( 'ZTF_DEMO' )拿到表函数对象,xco_cp_abap=>class( 'ZCL_AMDP_DEMO' )拿到类对象。
第二步,对对象发起一个创建操作。操作接口提供了new_specification方法,返回一个"规格"对象。规格就是你对这个对象最终样子的完整描述:类名是什么、短文本是什么、包含哪些方法,等等。
第三步,把规格填满,然后调用操作的execute。XCO 会按照你的规格去创建真实的仓库对象,处理激活和传输等底层细节。
打个不严谨的比方:XCO 的对象操作像是点外卖。对象是你要的那家店,创建操作是打开下单页,规格是你选的菜品、口味和备注,execute 就是确认支付。在支付之前,你随时可以改规格;支付之后,菜品就进了后厨开始做了。
拿创建普通 CDS 视图举例,你立刻能看出这个模式:
DATA(lo_cds_view) = xco_cp_cds=>view( '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_SUM
- AMDP 类名:对应实现类,如 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_abap=>class( 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_abap=>visibility->public ). lo_def->set_final( abap_true ). lo_def->add_interface( xco_cp_abap=>interface( 'IF_AMDP_MARKER_HDB' ) ). " 添加方法,并绑定为 FOR TABLE FUNCTION DATA(lo_method) = lo_def->add_method( iv_method_name ). lo_method->set_visibility( xco_cp_abap=>visibility->public ). lo_method->set_kind( xco_cp_abap=>method_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_abap=>class( 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_cds=>table_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 扩展开发,这套思路值得试一次。