十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ABAP CDS Association导航实践:从语法到性能优化

ABAP CDS Association导航实践:从语法到性能优化 1. 为什么写这篇导航实践从一次慢查询说起先讲个真实经历。去年我做一个航班数据展示的 OData 接口优化功能很简单前端要列出航班号、出发日期、航空公司名称。CDS 视图里用了两个 Association一个关联scarr取航空公司名称一个关联spfli取航线起降城市。结果压测的时候这个接口慢得离谱ST05 一跟踪发现底层 SQL 生成了 7 个 LEFT OUTER JOIN其中有 5 个是从未在界面上展示过的文本表关联。查到最后问题根源是定义 CDS 时为了“方便”在 SELECT 列表里写了个_text.*把所有文本描述字段一次性拖了出来。UI 后来不需要那些字段了但这个投影还在Association 的导航关系就一直挂在查询里每次调用都带着那一堆多余 JOIN 跑一遍。这件事让我意识到CDS Association 这东西用好了是建模的利器用不好就是性能的黑洞。它是 SAP 在 ABAP CDS 中定义的语义关联关系解决的核心问题是“来源”和“目标”之间的数据导航从一个数据源来源出发沿着定义好的路径拿到另一个数据源目标的字段。它看起来像一个外键关系的元数据描述但实际查询时的展开机制、方向、性能影响跟传统 JOIN 有本质区别。这篇博客我会把 Association 的导航实践完整梳理一遍从基础语法、路径表达式、定向过滤到性能边界和调试手段把我实际项目中踩过的坑和验证过的结论都写出来。适合三类人看刚接触 CDS 想搞清楚 Association 到底怎么用的初级 ABAP 开发写了不少 CDS 但被隐式 JOIN 坑过的中级开发以及需要排查 CDS 查询性能问题的顾问。2. Association 语法基础先定义好“来源”与“目标”的契约2.1 一个最小可用的 Association 定义学习 Association 之前先把它在 CDS 视图里的位置搞清楚。一个 Association 不是在查询语句里临时写的条件而是在视图定义时声明的一种元数据契约。它在select from之后、字段列表之前声明。我拿最经典的SFLIGHT航班表举个例子AccessControl.authorizationCheck: #NOT_REQUIRED EndUserText.label: 航班基础视图 define root view entity zv_flight_root as select from sflight as flight association [1..1] to scarr as _carrier on _carrier.carrid flight.carrid { key flight.carrid as CarrierId, key flight.connid as ConnectionId, key flight.fldate as FlightDate, flight.seatsmax as SeatsMax, flight.seatsocc as SeatsOccupied, _carrier.carrname as CarrierName }这个视图里_carrier就是一个 Association它的来源是当前视图的每一行数据通过flight.carrid目标是scarr表导航条件是两边carrid相等。注意字段列表里出现了_carrier.carrname这就是一次显式的导航沿着 Association 路径从SFLIGHT定位到SCARR把航空公司的名字抓出来。有个新手容易混淆的点定义 Association 的时候它就是一个“声明”不会立刻产生 JOIN。只有在你真正去使用它的时候——比如在字段列表里投影它的元素、在另一个 CDS 视图里继续导航、或者 OData 服务展开这个关联——底层 SQL 才会生成对应的连接。这个特性后面性能章节还会重点讲。2.2 Cardinality告诉数据库“目标有几个”Cardinality 是 Association 定义里最容易忽视、但影响非常大的部分。它描述的是“对于来源的一条记录目标最多有几条、最少有几条记录与之对应”。ABAP CDS 支持四种组合Cardinality语义典型场景底层 SQL 连接倾向[0..1]目标最多一条可能没有可空的外键、非强制描述的文本LEFT OUTER JOIN[1..1]目标恰好一条主数据表外键非空INNER JOIN[0..*]目标零到多条明细表、文本表、历史记录合成数据时需小心行数膨胀[1..*]目标至少一条到多条强制存在的父子明细与[0..*]类似[1..1]和[0..1]之所以在导航中最常用是因为它们能保证“来源记录数不膨胀”。一旦你定义了[0..*]或[1..*]目标可能返回多行如果直接投影目标字段结果集会因为“一对多”而翻倍。这个翻倍效应是运行时才出现的不像语法错误那么容易发现。我遇到过一次线上 bug就是有人把[0..1]写成[0..*]结果本来 600 条航班记录膨胀到 9000 条前端列表直接翻了好几页。2.3 ON 条件设计的三条铁律ON 条件是 Association 的导航基础它决定了从来源到目标按什么规则匹配。这里有三条我在代码评审时一定会检查的规则第一ON 条件基本只允许等值比较。ABAP CDS 里的 Association 不像普通 SQL JOIN 那样支持、、BETWEEN之类的范围条件更不能用函数做转换。如果业务上确实需要范围匹配老实写 JOIN不要硬塞进 Association 里。第二尽量用目标表的主键字段做关联。这是为了保证 Cardinality 的语义成立。如果你用[1..1]声明目标恰好一条但 ON 条件关联的字段在目标表里并不唯一运行时数据可能翻倍。你可以在 HANA 里执行select carrid, count(*) from scarr group by carrid having count(*) 1来验证关联字段的唯一性。第三ON 条件里可以引用$session.system_language这类会话变量。这是做多语言文本关联的常用手法。比如关联文本表时让语言字段直接匹配当前登录语言association [0..1] to i_documenttext as _text on _text.document_id flight.carrid and _text.language $session.system_language这条 ON 条件的巧妙之处在于导航行为由会话上下文决定同一个 CDS 视图不同语言用户看到的是不同语言的文本视图本身不需要额外加参数。3. 路径导航的核心写法从点到链再到网3.1 单层导航在 SELECT 列表里直接取目标字段Association 最直接的用途就是把目标表的字段拉进当前查询结果这就是单层导航。语法上就是在字段列表里用“下划线加字段”的方式访问目标元素。我上面zv_flight_root里的_carrier.carrname就是单层导航。单层导航看起来简单但有两点值得注意。一个是在字段列表里给导航字段起别名建议不要省略。比如_carrier.carrname as CarrierName起别名的好处是后续消费端读取字段名比较清晰也避免目标表字段名与来源表字段名冲突。如果两个 Association 都导航出name之类的通用字段没有别名就会在 OData 或 CDS 视图校验阶段报字段名重复。另一个是投影目标字段时尽量只投影你真正需要的字段不要投影多了这个在第五章再展开说。3.2 链式导航用嵌套路径穿透多层数据Association 的导航不局限于一层。只要被关联的目标比如一个 CDS 视图内部也定义了 Association你就可以在路径表达式里继续往后穿。这种一层接一层的访问就是链式导航。举个例子从SFLIGHT导航到SPFLI拿航线再从SPFLI导航到SAIRPORT拿出发机场一次查询里直接拿全define view entity zv_flight_chain as select from sflight as flight association [1..1] to spfli as _conn on _conn.carrid flight.carrid and _conn.connid flight.connid association [0..1] to sairport as _airport on _airport.id _conn.airpfrom { key flight.carrid as CarrierId, key flight.connid as ConnectionId, key flight.fldate as FlightDate, _conn.airpfrom as DepartureAirport, _airport.name as DepartureAirportName }这里_airport这个 Association 的 ON 条件里用到了前面_conn的字段_conn.airpfrom。这种“基于已有 Association 再定义新 Association”的写法是把链式导航直接建模进视图结构里消费方只要访问zv_flight_chain就能一次性拿到出发机场名称。如果你的 NetWeaver 版本对 ON 条件里引用其他 Association 支持得不好稳妥的做法是把中间层拆成一个独立的 CDS 视图再在上一层视图里关联这个中间视图效果一样只是多写一层。还有另一种链式导航更灵活就是在另一个视图的字段列表里直接访问已有视图暴露出来的 Association。假设zv_flight_root已经定义了_carrier那么新视图里可以这样写define view entity zv_flight_customer as select from zv_flight_root as root { root.CarrierId, root._carrier.carrname as CarrierName, root._carrier.currcode as CurrencyCode }路径root._carrier.carrname就是跨视图的链式导航先通过zv_flight_customer关联到zv_flight_root再沿着zv_flight_root内部的 Association 找到scarr。这种写法让基础视图保持精炼由上层消费视图决定要不要继续展开。3.3 多关联组合同一来源导航到多个目标业务需求很少只关联一张表。一个航班的视图可能既要航空公司名称、又要出发机场名称、还要目的地城市文本这就需要同时定义多个 Association让它们形成一种“网状导航”。多个 Association 之间是平行的互不干扰只要各自 ON 条件正确、别名不重复即可define view entity zv_flight_multi as select from sflight as flight association [1..1] to scarr as _carrier on _carrier.carrid flight.carrid association [1..1] to spfli as _conn on _conn.carrid flight.carrid and _conn.connid flight.connid association [0..1] to sairport as _dep on _dep.id _conn.airpfrom association [0..1] to sairport as _arr on _arr.id _conn.airpto { key flight.carrid, key flight.connid, key flight.fldate, _carrier.carrname as CarrierName, _dep.name as DepAirportName, _arr.name as ArrAirportName }这个例子里_dep和_arr都指向sairport但因为 ON 条件分别关联出发机场和到达机场所以两个 Association 各取所需。这种设计比 JOIN 写法更直观你一眼就能看出当前视图依赖哪些外围数据每个关联的语义是什么。我在实际项目中总结的经验是一个视图的 Association 数量最好控制在 5 个以内。超过这个数导航关系会非常复杂后续每个依赖这个视图的消费端都会面临隐式 JOIN 爆炸的风险。4. 导航中的定向过滤让目标更精确的实用手段4.1 在 ON 条件里做定向多语言文本表的典型处理Association 导航不是只有一个“相等关联”那么简单你可以在定义阶段就把过滤条件塞进 ON 条件里实现更精准的定向。最典型的就是文本表关联。假设有一个文档主表和它对应的多语言文本表如果不在 ON 条件里限制语言导航会把所有语言的文本都匹配进来一个文档会膨胀成好几条业务上完全不可用。正确写法是把语言限定在导航契约里AccessControl.authorizationCheck: #NOT_REQUIRED define view entity zv_doc_with_text as select from zdoc_header as doc association [0..1] to zdoc_text as _text on _text.doc_id doc.doc_id and _text.langu $session.system_language { key doc.doc_id as DocId, doc.doc_date as DocDate, _text.description as Description }这样用户在前端看到的是什么语言的系统Description 就自动显示对应语言。注意我把 Cardinality 设计成[0..1]因为确保语言匹配后一个文档在当前语言下只会有零条或一条文本记录。如果某些文档漏维护了某语言[0..1]不会导致整条记录丢失而是 Description 为空。这正好利用了[0..1]与 LEFT OUTER JOIN 的对应关系。4.2 按关联目标过滤EXISTS 比路径条件更可靠很多初学者会想在 WHERE 条件里直接写_carrier.carrname Lufthansa来过滤数据但在 ABAP CDS 里这个写法通常是不被允许的。WHERE 子句中不能直接使用 Association 的路径表达式作为条件字段这是 CDS 语法限制。正确的做法是使用 EXISTS 子查询把导航的判断逻辑放在 EXISTS 里面。例如要查出所有汉莎航空的航班define view entity zv_flight_lh as select from sflight as flight { key flight.carrid, key flight.connid, key flight.fldate } where exists ( select 1 from scarr as _carrier where _carrier.carrid flight.carrid and _carrier.carrname Lufthansa )EXISTS 的语义是“只要存在一条满足条件的记录当前数据行就保留”。这种写法有一个很大的性能优势它是在判断存在性不需要把scarr的字段全部展开投影也没有行数膨胀风险。我见过不少人为了在 WHERE 里做过滤先把_carrier.carrname投影到视图字段上然后在消费端再按这个字段过滤绕了一大圈不仅多生成了一个 JOIN还让视图承担了本不该承担的过滤职责。直接在 CDS 里用 EXISTSSQL 层会更干净。4.3 用路径选择器做内联过滤在导航的同时带条件ABAP CDS 提供了一种带过滤条件的路径访问语法可以在访问 Association 时临时指定匹配条件这就是路径选择器。它的语法是在 Association 名后面用方括号追加过滤器。我前面例子里的_text已经通过 ON 条件限定了语言如果还需要在特定场景下取另一个语言的文本就可以这样define view entity zv_flight_dual_text as select from sflight as flight { key flight.carrid, _text[1: langu E].description as DescriptionEnglish, _text[1: langu D].description as DescriptionGerman }这里[1: langu E]的含义是从_text关联结果中取出满足langu E的第一条记录然后访问它的description字段。写法里的1:表示取第一条。如果你能确定过滤后只有一条记录不写1:也可以但如果过滤条件可能匹配多条不写索引会让查询结果不确定甚至报 cardinality 错误。我建议无论在什么场景都写[1: ...]或[0..1: ...]这种带索引的形式明确告诉系统要取哪一条避免歧义。路径选择器非常灵活它可以在不修改 Association 定义的情况下针对不同场景抓取目标中不同记录。这在文本表、历史记录表的场景非常实用同一个主表既能取英文描述又能取德文描述甚至能取最新的历史记录都不需要拆成多个 Association。5. 导航的性能边界避免隐式 JOIN 拖垮查询5.1 什么时候会真正生成 JOIN别被“声明”骗了很多做 CDS 开发的人以为 Association 不会产生 JOIN这是最危险的误解。准确的说法是定义 Association 时不会立刻生成 JOIN但只要某个消费场景“用到”了它JOIN 就会出现在底层 SQL 中。我梳理下来至少有三个场景会触发 JOIN 生成第一字段列表里直接投影了 Association 的目标元素比如_carrier.carrname。CDS 编译器要取scarr的字段自然会把scarr连接进来。第二其他 CDS 视图对这个视图的 Association 进行链式导航并且导航到了目标字段。第三OData 服务或分析查询展开暴露的 Association比如$expand操作。所以每当你看到一个 CDS 视图定义了 Association先从这几个消费入口排查一遍确认哪些 Association 真的被“激活”了。5.2 隐式 JOIN 与显式 JOIN 的实测差异我用 50 万行SFLIGHT数据做过对比测试一个视图用 Association 投影carrname另一个视图用传统 LEFT OUTER JOIN 关联scarr。从最终生成的 SQL 文本看两者差别不大HANA 的执行计划基本一致查询耗时也几乎相同。这说明 Association 的隐式 JOIN 不是性能差的代名词性能问题的根源在于“多”和“乱”而不是“Association 本身”。那为什么实际系统中 Association 常常和性能问题挂钩我观察到的真正原因是很多开发把多个 Association 全部投影出来导致 JOIN 数量线性增加。比如一个视图定义了 6 个 Association字段列表里一次性投影了 4 个底层就是 4 个 JOIN。再加上这些 JOIN 的目标表可能还有自己的 Association一个视图放大到消费端就变成一个巨型 JOIN 网络。我在文章开头提到的 7 个 JOIN 案例就是这么来的5 个文本表关联中真正在 UI 上展示的只有 1 个另外 4 个完全没有必要投影。5.3 三个容易踩的性能坑及规避方法坑一用_assoc.*投影整个关联目标。这种做法会把目标表的全部字段都纳入查询结果底层 JOIN 后的 SELECT 列表极其庞大。HANA 是列式存储字段越多需要访问的列就越多内存和 CPU 开销成倍增长。而且这些字段通常 90% 都用不上。规避方法很简单只投影需要的字段一个都不要多。坑二Cardinality 定义与实际数据不一致。如果目标表关联字段不唯一你却声明了[1..1]查询结果行数会悄悄膨胀而且这个 bug 很难发现因为结果集看起来“似乎有道理”只是比预期多了一些行。规避方法是在定义 Association 前先验证关联字段的唯一性尤其是数据来自业务表而不是主数据表的时候。曾经有个项目订单视图关联订单状态文本表时用了[0..1]但状态文本表里每个状态码有多条历史文本结果订单数据翻了 12 倍。最后改成按“最新一条”的路径选择器才解决。坑三访问控制与 Association 叠加。如果 CDS 视图的注解是AccessControl.authorizationCheck: #CHECK并且 DCL 权限条件里涉及了关联表的字段那么每个 Association JOIN 都可能附带额外的权限过滤逻辑。JOIN 数量一多权限评估的开销会显著放大。规避方法是对于不需要行级权限控制的视图尽量用#NOT_REQUIRED如果有权限控制把需要过滤的字段集中在一两个 Association 上不要让权限条件散落在多个关联里。6. 排查导航问题调试器中的动态断点技巧6.1 把 CDS 查询结果塞进内表直接看数据Association 导航的结果无非是字段值但很多问题不是语法错而是语义错比如某个字段的值根本不是预期的那一条记录。这时候把 CDS 视图像普通表一样 SELECT 到内表里看数据是最直接的排查方式。在 ABAP 程序里执行下面的代码DATA(lt_flight) NEW cl_salv_bs_runtime_info( )-get_data_ref( ) .其实更常用的是直接 SELECTSELECT CarrierId, CarrierName, FlightDate FROM zv_flight_root INTO TABLE DATA(lt_result) UP TO 100 ROWS. IF sy-subrc 0. cl_demo_outputdisplay( lt_result ). ENDIF.先看看基础视图的导航字段对不对再去排查上层视图或者 OData 的展开逻辑。如果基础视图字段正确说明 Association 定义没问题问题出在消费端如果基础视图字段就不对回到 ON 条件和 Cardinality 上找原因。这种从“来源”到“目标”的正向验证思路比一头扎进调试器要高效得多。6.2 LOOP 里设置动态条件断点精准定位异常数据CDS 查询结果进入内表后我们经常要在 LOOP 循环里逐行检查数据。但如果数据量很大F5 一直按到手指抽筋也到不了你要看的那一行。这时候动态条件断点就是最佳利器。在调试器里把光标停在 LOOP 循环体的代码行上点击设置断点然后在断点属性里填入条件sy-tabix 500或者按业务字段中断wa_result-carrid LH我排查 Association 导航问题时最常用的是字段条件。比如你怀疑某个航班的 CarrierName 显示成了另一家航空公司但不确定它的 Key 是什么。可以先在调试器里打开内表用sy-tabix找到这个可疑行在表中的位置然后设置一个sy-tabix 具体行号的条件断点重新运行程序就能精确定位到这一行执行时的上下文逐个字段展开看确认_carrier.carrname到底是哪一步导航出来的。动态条件断点还有一个好处它不会像普通断点那样每次都停只在条件满足时中断调试过程中按 F8 直接跳过海量无关数据效率提升非常明显。6.3 用 ST05 验证最终 SQL从根上确认 JOIN数据层面的排查只能验证结果对不对但如果性能有问题还得回到数据库层面看 SQL。ST05 是 SAP 标准的 SQL 跟踪工具运行ST05事务码打开跟踪开关执行你的 CDS 查询或 OData 请求然后关闭跟踪并查看分析结果。重点看三点第一实际生成了几个 JOIN。把跟踪里最耗时的那条 SQL 展开逐个 JOIN 核对确认是不是每个 JOIN 都有业务必要。如果发现多余的 LEFT OUTER JOIN回 CDS 视图里找哪个 Association 被投影了或者哪个消费端还在展开它。第二WHERE 条件有没有带上预期的过滤条件。如果你写了 EXISTS 或路径选择器确认底层 SQL 是否体现成了对应的子查询或连接条件。第三投影字段是否精简。看 SELECT 字段列表有没有把不用的大字段也拉进来尤其是文本字段、RAW 字段这类占空间的类型。有一次排查一个 3 秒的查询我发现 ST05 里一条 SQL 有 3 个 LEFT OUTER JOIN其中 2 个是针对文本表的但 OData 模型里根本没有定义文本字段的展示。最后找到原因是上层 CDS 视图里有一个_text.description as Description的投影虽然 UI 没展示但 OData 元数据还是把这个字段暴露了。删掉这个投影后查询时间降到 400 毫秒。这种问题不看 ST05光靠代码审查很难发现。在我实际做过的 CDS 优化项目中ST05 是最后一个“证据工具”它能把你对 Association 展开的判断用 SQL 语句的形式钉死。对占用资源最大的那条 SQL 做逐行分析再回到 CDS 源视图调整定义整个排查链路才算是闭环。最后说一点个人体会Association 导航本身是一种建模思想的体现它把数据之间的语义关系变成视图的一部分。但建模再优雅最终都要落到实际运行的 SQL 上。把 Association 当作一个可以随时免费使用的 JOIN 生成器是最大的误区。你在定义和导航它的时候多想一层“这个关联到底会被谁用、会不会被真心实意地用”就能避开绝大多数坑。每当我新定义一个 Association都会顺手在心里过一遍消费方、Cardinality、投影字段和权限条件这四件事这套思路帮我拦下了好几次线上性能事故。
返回列表