1、 Association 的核心概念
1.1、 什么是 Association?
定义: Association 是 CDS 中一种声明性的语句,用于定义两个 CDS 实体(视图或表)之间的关系。
本质: 它本身不是一条 JOIN 语句,而是一个元数据定义,描述了“如何连接”的关系。只有在被显式使用时(例如通过路径表达式),它才会在生成的 SQL 中触发实际的 JOIN 操作。
1.2、 为什么需要 Association?
抽象性: 将关系逻辑从查询中分离出来,封装在模型内部,提高复用性和可维护性。
语义丰富: 为数据模型赋予业务含义(如“销售订单属于一个客户”)。
便捷性: 通过简洁的路径表达式访问关联数据,无需编写复杂的 JOIN。
框架集成: 是 SAP Fiori Elements 和 OData 服务自动提供导航功能的基础。
1.3、 基本语法
association[min..max] to TargetEntity as _Alias on $projection.SourceField = _Alias.TargetField[min..max]:基数,定义关联的维度。
[0..1]: 目标实体最多有一条记录与之对应。
[1..1]: 目标实体必须有一条记录与之对应。
[0..*]或[*]: 目标实体有零条或多条记录与之对应。
[1..*]: 目标实体有一条或多条记录与之对应。
TargetEntity: 要关联的目标 CDS 视图或数据库表。
_Alias: 关联的别名,通常以下划线_开头以区分于普通字段。
on ...: 关联条件,类似于 JOIN 的 ON 子句。$projection代表当前视图的字段。
2、Association 的用法与场景
为了进行对比,我们将使用以下简单的数据模型:
YBINV_CUSTOMER: 客户主数据视图
YBINV_ORDER: 销售订单抬头视图
2.1、Association定义与路径表达式
目标: 在订单抬头视图中,关联到客户主数据,以获取客户名称。
2.1.1、定义 Association
// 客户主数据视图 defineview YBINV_CUSTOMER asselect key kunnr, land1, name1 from kna1 // 销售订单头视图 defineview YBINV_ORDER asselect vbeln, auart, kunnr, _customer from vbak // 定义到客户的关联 association[1..1] to YBINV_CUSTOMER as _customer on $projection.kunnr = _customer.kunnr关键点:
此处使用Association关联客户,并定义了一个别名_customer。
基数[1..1]表示每个订单必须对应一个客户。
关联条件使用$projection引用vbak的字段。
2.1.2、使用路径表达式消费 Association
仅定义 Association 不会在结果中看到任何信息。必须在select列表中显式消费它。
// 消费 Association 的视图 defineview YBINV_ORDER_CUSTOMER asselect vbeln, auart, kunnr, // 使用路径表达式将关联实体的字段“带”过来 _customer.name1, // 获取客户名称 _customer.land1 // 获取客户国家 from YBINV_ORDER生成的 SQL 等价于:
SELECT so.vbeln, so.auart, so.kunnr, cust.name1, cust.land1 FROM VBAK AS so LEFT OUTER JOIN KNA1 AS cust ON so.kunnr = cust.kunnr执行结果
2.2、内外连接
基数只定义了表的关系,但不影响最终生成的SQL连接。
比如定义了association[1..1] to vbap as _item on $projection.vbeln = _item.vbeln
但是系统执行查询时,仍然生成的是左连接
因为系统默认采用left outer join 进行关联。
如果要使用内连接(INNER JOIN),需要特殊处理。比如在查询时,加入限制
defineview YBINV_ORDER_CUSTOMER asselect vbeln, auart, kunnr, _item[inner].posnr, //强制使用inner连接 _item.kwmeng from YBINV_ORDER2.3、过滤 Association
在消费时,对 Association 指向的目标实体进行过滤。
比如,我们只想获取德国的客户信息。
defineview YBINV_ORDER_CUSTOMER asselect vbeln, auart, kunnr, // 使用路径表达式将关联实体的字段“带”过来 _customer[land1 ='DE'].name1, // 获取德国客户名称 _customer.land1 // 这个仍然是原始的客户国家 from YBINV_ORDER结果对比:
订单ID | 客户名称 | 客户国家 | 客户名称 |
|---|---|---|---|
SO001 | ABC | DE | ABC |
SO002 | XYZ Corp | US | NULL |
SO003 | German | DE | German |
过滤只影响通过该路径表达式获取的值,不会影响主结果集的行数,也不会影响其他使用同一 Association 的路径。
2.4、暴露 Association 用于导航
在 OData 服务中,我们经常希望将 Association 本身暴露为一个导航链接(Navigation Link),而不是直接展开其字段。
在 OData 元数据中定义/YBINV_UI_CUSTOMER_CDS/YBINV_UI_CUSTOMER('SO001')/toCustomer这样的导航属性。
@OData.publish: true defineview YBINV_UI_CUSTOMER asselect key vbeln, auart, kunnr, // 直接暴露 Association,而不是其字段 // 这将在 OData 元数据中生成一个名为 `toCustomer` 的导航属性 其中'to'是OData自动添加的导航前缀 _customer as Customer from YBINV_ORDER结果与对比:
- 不暴露 Association
: OData 服务只有平铺的字段(如
name1,land1)。 - 暴露 Association
: OData 服务包含一个导航链接。客户端可以通过以下方式访问关联数据:
GET /YBINV_UI_CUSTOMER('SO001')?$expand=toCustomerGET /YBINV_UI_CUSTOMER('SO001')/toCustomer
- 404错误
:对于导航连接,有时系统会自动添加前缀,比如to,所以如果访问报404错误,需要看
/$metadata文件中<NavigationProperty Name="toCustomer">对应的name值。
这种方式提供了更大的灵活性,允许客户端决定是否需要以及何时需要加载关联数据。
2.5、使用$projection进行自关联
当关联条件依赖于当前视图中的计算字段或筛选后的结果时,需要使用$projection。
比如:当客户为空时,默认一个客户,需要使用case when语法得出计算后的字段ZKUNNR,然后通过ZKUNNR查询对应的客户主数据:
defineview YBINV_ORDER asselect key vbeln, auart, case when kunnr isnull then'0000300022'//如果没有客户就默认客户编码 else kunnr endas zkunnr, _customer, _item from vbak // 定义到客户的关联 此处使用处理过的字段关联 association [0..1] to YBINV_CUSTOMER as _customer on $projection.zkunnr = _customer.kunnr$projection确保了关联条件是在当前视图的上下文(包括可能的计算和过滤)中进行的。
3、JOIN和Association的对比
通过以下几点,对两种方法进行深度对比
3.1、灵活性与过度获取数据
JOIN 封装的问题:
// 基础视图已经固定返回所有客户字段 defineview I_SalesOrderHeaderWithCustomer asselectfrom snwd_so innerjoin snwd_bpa {...} { salesorder_id, gross_amount, customer_name, // 总是返回 customer_country, // 总是返回 customer_city, // 总是返回 customer_phone, // 总是返回 customer_email // 总是返回 };使用 Association 的灵活性:
defineview I_SalesOrderHeader asselectfrom snwd_so { key salesorder_id, gross_amount, currency_code, _customer from vbak // 定义到客户的关联 association[1..1] to I_Customer as _customer on $projection.customer_guid = _Customer.customer_id // 消费视图1:只需要客户名称 defineview ZC_SimpleOrder asselectfrom I_SalesOrderHeader { salesorder_id, gross_amount, _Customer.customer_name // 只获取需要的字段 }; // 消费视图2:不需要客户信息 defineview ZC_FinancialReport asselectfrom I_SalesOrderHeader { salesorder_id, gross_amount, currency_code // 不消费 _Customer,不会产生 JOIN //_Customer.customer_name };对比结果:
场景 | JOIN 封装 | Association |
|---|---|---|
只需要1个客户字段 | 仍然获取所有客户字段 | 只获取需要的字段 |
不需要客户数据 | 仍然执行 JOIN | 不执行 JOIN |
需要不同客户字段组合 | 需要创建多个基础视图 | 单一基础视图满足所有需求 |
3.2、维护成本
当客户模型变化时:
JOIN 封装方式:
// 客户表新增了重要字段 altertable snwd_bpa add company_size nvarchar(20); // 需要修改所有封装了JOIN的基础视图 defineview I_SalesOrderHeaderWithCustomer asselectfrom ... { ..., _customer.company_size // 必须手动添加 }; // 所有基于这个视图的消费视图都能看到新字段(可能不需要)Association 方式:
// 只需要在 YBINV_CUSTOMER视图中暴露新字段 defineview I_Customer asselectfrom snwd_bpa { ..., company_size // 在源头添加 }; // 消费视图按需决定是否使用新字段 // 不需要修改任何现有的消费视图3.3、多重关系处理
复杂场景:订单有创建者、修改者、销售员等多个人员关联
JOIN 封装的困境:
defineview I_SalesOrderHeaderWithAllJoins asselectfrom snwd_so innerjoin snwd_bpa as _customer ... leftjoin snwd_emp as _creator ... leftjoin snwd_emp as _salesperson ... leftjoin snwd_bpa as _bill_to_party ... { salesorder_id, _customer.customer_name, _creator.employee_name as creator_name, _salesperson.employee_name as salesperson_name, _bill_to_party.formatted_name as bill_to_name // 字段爆炸,命名冲突等风险 };Association 的清晰方案:
defineview I_SalesOrderHeader asselectfrom snwd_so { key salesorder_id, association [1..1] to I_Customer as _Customer ..., association [0..1] to I_Employee as _CreatedBy ..., association [0..1] to I_Employee as _SalesPerson ..., association [0..1] to I_Customer as _BillToParty ... }; // 消费时 按需选择 defineview ZC_OrderForSales asselectfrom I_SalesOrderHeader { salesorder_id, _Customer.customer_name, _SalesPerson.employee_name // 只关心销售相关 }; defineview ZC_OrderForBilling asselectfrom I_SalesOrderHeader { salesorder_id, _BillToParty.customer_name // 只关心账单相关 };3.4、框架集成与语义化
Association 的独特优势:
3.4.1、OData 导航
@OData.publish: true defineview ZC_SalesOrderForOData asselectfrom I_SalesOrderHeader { key salesorder_id, gross_amount, _Customer as ToCustomer // 自动生成导航属性 };客户端可以调用:/SalesOrders('123')/ToCustomer
3.4.2、文本关联
defineview YBINV_ORDER asselectfrom vbak // 定义到客户的关联 association [1..1] to YBINV_CUSTOMER as _customer on $projection.kunnr = _customer.kunnr { key vbeln, auart, @ObjectModel.text.element: ['name'] //绑定name和kunnr kunnr, @Semantics.text: true _customer.name, //将name作为描述 _customer }在消费视图中定义
@OData.publish: true defineview YBINV_UI_CUSTOMER asselect key vbeln, auart, @ObjectModel.text.association: '_customer'//在客户中展示描述 kunnr, _customer.name, _customer as Customer from YBINV_ORDERUI 自动显示客户名称
3.4.3、搜索帮助
Value Help注解@Consumption.valueHelpDefinition
defineview ZC_SalesOrder asselectfrom I_SalesOrderHeader { @Consumption.valueHelpDefinition: [{ entity: { name: 'I_Customer', element: 'customer_guid' } }] customer_guid, _Customer.customer_name };3.5、性能对比
实际执行计划分析
JOIN 封装视图:
消费时,该视图已经执行了完整的JOIN。
Association 视图:
消费时,优化器可能将路径表达式重写为高效的JOIN,只获取需要的字段。
4、 总结与最佳实践
特性 | 传统 SQL JOIN | CDS Association |
|---|---|---|
| 本质 | 命令式,是查询的一部分 | 声明式,是模型元数据的一部分 |
| 复用性 | 差,JOIN 逻辑在每个查询中重复 | 极佳 ,定义一次,随处消费 |
| 可读性 | 复杂查询可读性差 | 路径表达式 使查询意图更清晰 |
| 灵活性 | 直接控制 JOIN 类型和条件 | 通过基数和过滤间接控制,更抽象 |
| 框架集成 | 无特殊支持 | 深度集成 ,是 Fiori Elements 和 OData 导航的基石 |
| 耦合 | 查询与关系逻辑紧耦合 | 查询与关系逻辑解耦,模型更清晰 |
最佳实践:
始终使用 Association: 在新的 CDS 开发中,优先使用 Association 而不是直接 JOIN。
明确的别名: 使用下划线_开头为 Association 命名,以示区分。
用于文本关联: 结合@ObjectModel.text.association注解,自动为代码字段提供描述文本。
OData 导航: 通过暴露 Association 来构建丰富的、可导航的 OData 服务。
以上就是关于Association的介绍,希望对你有所帮助