CDS Association详解

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
// 客户主数据视图
define view YBINV_CUSTOMER
  as select
    key kunnr,
        land1,
        name1
  from kna1

// 销售订单头视图
define view YBINV_ORDER
  as select
    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 的视图
define view YBINV_ORDER_CUSTOMER
  as select
    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

执行结果

image

2.2、内外连接

基数只定义了表的关系,但不影响最终生成的SQL连接。
比如定义了
association[1..1] to vbap as _item on $projection.vbeln = _item.vbeln
但是系统执行查询时,仍然生成的是左连接
image

image
因为系统默认采用left outer join 进行关联。
如果要使用内连接(INNER JOIN),需要特殊处理。比如在查询时,加入限制

define view YBINV_ORDER_CUSTOMER
  as select
    vbeln,
    auart,
    kunnr,
    _item[inner].posnr, //强制使用inner连接
    _item.kwmeng
  from YBINV_ORDER

image

2.3、过滤 Association

在消费时,对 Association 指向的目标实体进行过滤。

比如,我们只想获取德国的客户信息。

define view YBINV_ORDER_CUSTOMER
  as select
    vbeln,
    auart,
    kunnr,
    // 使用路径表达式将关联实体的字段“带”过来
    _customer[land1 = 'DE'].name1, // 获取德国客户名称
    _customer.land1 // 这个仍然是原始的客户国家
  from YBINV_ORDER

结果对比

订单ID 客户名称 客户国家 客户名称
SO001 ABC Company DE ABC Company
SO002 XYZ Corp US NULL
SO003 German GmbH DE German GmbH

过滤只影响通过该路径表达式获取的值,不会影响主结果集的行数,也不会影响其他使用同一 Association 的路径。


2.4、暴露 Association 用于导航

在 OData 服务中,我们经常希望将 Association 本身暴露为一个导航链接(Navigation Link),而不是直接展开其字段。

在 OData 元数据中定义 /YBINV_UI_CUSTOMER_CDS/YBINV_UI_CUSTOMER('SO001')/toCustomer 这样的导航属性。

@OData.publish: true
define view YBINV_UI_CUSTOMER as select
    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=toCustomer
    • GET /YBINV_UI_CUSTOMER('SO001')/toCustomer
  • 404错误:对于导航连接,有时系统会自动添加前缀,比如to,所以如果访问报404错误,需要看/$metadata文件中<NavigationProperty Name="toCustomer">对应的name值。

这种方式提供了更大的灵活性,允许客户端决定是否需要以及何时需要加载关联数据。


2.5、使用 $projection 进行自关联

当关联条件依赖于当前视图中的计算字段或筛选后的结果时,需要使用 $projection

比如:当客户为空时,默认一个客户,需要使用case when语法得出计算后的字段ZKUNNR,然后通过ZKUNNR查询对应的客户主数据:

define view YBINV_ORDER
  as select
    key vbeln,
        auart,
        case
        when kunnr is null
        then '0000300022' //如果没有客户就默认客户编码
        else kunnr
        end as 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 封装的问题

// 基础视图已经固定返回所有客户字段
define view I_SalesOrderHeaderWithCustomer as select from snwd_so
  inner join snwd_bpa {...}
{
  salesorder_id,
  gross_amount,
  customer_name,     // 总是返回
  customer_country,  // 总是返回  
  customer_city,     // 总是返回
  customer_phone,    // 总是返回
  customer_email     // 总是返回
};

使用 Association 的灵活性

define view I_SalesOrderHeader as select from 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:只需要客户名称
define view ZC_SimpleOrder as select from I_SalesOrderHeader {
  salesorder_id,
  gross_amount,
  _Customer.customer_name  // 只获取需要的字段
};

// 消费视图2:不需要客户信息
define view ZC_FinancialReport as select from I_SalesOrderHeader {
  salesorder_id,
  gross_amount,
  currency_code
  // 不消费 _Customer,不会产生 JOIN
  //_Customer.customer_name 
};

对比结果

场景 JOIN 封装 Association
只需要1个客户字段 仍然获取所有客户字段 只获取需要的字段
不需要客户数据 仍然执行 JOIN 不执行 JOIN
需要不同客户字段组合 需要创建多个基础视图 单一基础视图满足所有需求

3.2、维护成本

当客户模型变化时:

JOIN 封装方式

// 客户表新增了重要字段
alter table snwd_bpa add company_size nvarchar(20);

// 需要修改所有封装了JOIN的基础视图
define view I_SalesOrderHeaderWithCustomer as select from ...
{
  ...,
  _customer.company_size  // 必须手动添加
};

// 所有基于这个视图的消费视图都能看到新字段(可能不需要)

Association 方式

// 只需要在 YBINV_CUSTOMER视图中暴露新字段
define view I_Customer as select from snwd_bpa {
  ...,
  company_size  // 在源头添加
};

// 消费视图按需决定是否使用新字段
// 不需要修改任何现有的消费视图

3.3、多重关系处理

复杂场景:订单有创建者、修改者、销售员等多个人员关联

JOIN 封装的困境

define view I_SalesOrderHeaderWithAllJoins as select from snwd_so
  inner join snwd_bpa as _customer ... 
  left join snwd_emp as _creator ...
  left join snwd_emp as _salesperson ...
  left join 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 的清晰方案

define view I_SalesOrderHeader as select from 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 ...
};

// 消费时 按需选择

define view ZC_OrderForSales as select from I_SalesOrderHeader {
  salesorder_id,
  _Customer.customer_name,
  _SalesPerson.employee_name  // 只关心销售相关
};

define view ZC_OrderForBilling as select from I_SalesOrderHeader {
  salesorder_id, 
  _BillToParty.customer_name  // 只关心账单相关
};

3.4、框架集成与语义化

Association 的独特优势

3.4.1、OData 导航
@OData.publish: true
define view ZC_SalesOrderForOData as select from I_SalesOrderHeader {
  key salesorder_id,
  gross_amount,
  _Customer as ToCustomer  // 自动生成导航属性
};

客户端可以调用:/SalesOrders('123')/ToCustomer

3.4.2、文本关联
define view YBINV_ORDER
  as select from 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
define view YBINV_UI_CUSTOMER
  as select
    key vbeln,
        auart,
        @ObjectModel.text.association: '_customer' //在客户中展示描述
        kunnr,
        _customer.name,
        _customer as Customer
  from YBINV_ORDER

UI 自动显示客户名称
image

3.4.3、搜索帮助

Value Help注解@Consumption.valueHelpDefinition

define view ZC_SalesOrder as select from 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 导航的基石
耦合 查询与关系逻辑紧耦合 查询与关系逻辑解耦,模型更清晰

最佳实践

  1. 始终使用 Association: 在新的 CDS 开发中,优先使用 Association 而不是直接 JOIN。
  2. 明确的别名: 使用下划线 _ 开头为 Association 命名,以示区分。
  3. 用于文本关联: 结合 @ObjectModel.text.association 注解,自动为代码字段提供描述文本。
  4. OData 导航: 通过暴露 Association 来构建丰富的、可导航的 OData 服务。

以上就是关于Association的介绍,希望对你有所帮助

定期更文,欢迎关注

image

posted @ 2025-11-10 17:24  斌将军  阅读(1)  评论(0)    收藏  举报