在 Excel 里使用 ODBC 读取云平台上 CDS view 的数据

笔者的日常工作中,会使用 API Business Hub 这个 portal 上提供的各种 Restful API,同客户的第三方系统进行集成:

在 API Business Hub 网站上,这些 Restful API,分为 SOAP,OData 和 REST 三类。

SOAP,即简单对象访问协议,是一种标准的通信协议,它定义了如何在网络中传输消息。SOAP 基于 XML(可扩展标记语言),旨在实现程序间的通信,无论这些程序是什么语言编写或在什么平台上运行。SOAP 通过定义一套严格的消息结构和处理方式,使得不同系统之间的交互变得可能。

SOAP 的一个显著特点是它的可扩展性,通过使用不同的传输协议(如 HTTP、SMTP 等)来满足不同的网络需求。此外,SOAP 消息包含一个 envelope(信封),它又包含了 header(头部)和 body(正文),其中 header 提供了消息的处理指令,而 body 则包含了实际的调用和响应信息。

OData,全称开放数据协议,是一种基于 REST 原则的数据访问协议。它旨在使用简单的 HTTP 请求来发布、读取、编辑和删除数据。通过标准的 URI(统一资源标识符)和 HTTP 方法(如 GET、POST、PUT、DELETE 等),OData 使得访问不同数据源变得简单和统一。

OData 的一个关键特征是其对资源的操作和查询能力。客户端可以通过 URI 对数据进行过滤、排序、分组和选择,这种灵活性使得 OData 非常适合构建数据密集型的应用程序。OData 服务描述包括了服务元数据(如模型定义),使客户端能够理解服务的结构和使用方法。

REST,或表征状态转移,不是一个标准或协议,而是一组架构原则。RESTful API 使用 HTTP 协议的标准方法(如 GET、POST、PUT、DELETE)来处理数据。REST 强调资源的状态通过 URL 访问,并且交互状态应该无状态,即每个请求包含了处理该请求所需的所有信息。

RESTful API 设计的核心是资源,每个资源由其唯一的 URI 表示。这种设计使得 RESTful API 简单、轻量级并且易于理解和使用。相比于 SOAP,REST 通常使用 JSON(JavaScript 对象表示法)作为数据交换格式,因为它更加轻量和易于读写。

在国内的项目实施里,Cloud Application Studio 里通过 Restful API SDK 消费 S/4HANA 的 Restful API,是一个非常常见的需求:

传统的 OData 协议基于 HTTP,在使用 OData 服务进行业务数据的创建或者修改时,需要开发人员在 HTTP 层面操作 POST 请求的 body 内容,步骤繁琐且容易出错。

比如下图是笔者 2019 年曾经做过的一个项目,里面使用 Java 消费 C4C OData 服务创建销售订单的测试代码片段,其中第88行变量 body,包含的就是手动拼接 HTTP Post 请求的负载内容,代码可读性很差且不易维护。

完整的源代码:

java 复制代码
package odata;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

public class SimpleOrderCreator {

	private final String SERVICEURL = "https://my5000jerry.c4c.hybriscloud.cn//c4c/odata/cust/v1/zjerrysalesorder/CustomerQuoteCollection";
	HttpClient m_httpClient;

	private HttpClient getHttpClient() {
		if (this.m_httpClient == null) {
			this.m_httpClient = HttpClientBuilder.create().build();
		}
		return this.m_httpClient;
	}
	
	private String getCSRFToken(){
		final HttpGet get = new HttpGet(SERVICEURL);
		get.setHeader("Authorization", "Basic 0FOR0pFUlJZMTpTYXB0ZXN0MQ==");
		get.setHeader("x-csrf-token", "fetch");
		HttpHost proxy = new HttpHost("proxy.sha..corp", 8080, "http");

		RequestConfig config = RequestConfig.custom().setProxy(proxy).build();

		get.setConfig(config);

		HttpResponse response;
		String token = null;
		try {
			response = getHttpClient().execute(get);
			/*Header[] header = response.getAllHeaders();
			for( int i =0; i < header.length; i++){
				System.out.println("Header: " + header[i].getValue());
			}*/
			token = response.getFirstHeader("x-csrf-token").getValue();
			System.out.println("token: " + token);
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException | UnsupportedOperationException e) {
			e.printStackTrace();
		}
		return token;
	}
	
	private void createSO(String token,String body) {
		final HttpPost post = new HttpPost(
				URI.create(SERVICEURL));
		post.setHeader("Authorization",
				"Basic 0FOR0pFUlJZMTpTYXB0ZXN0MQ==");
		post.setHeader("Content-Type", "application/json");
		post.setHeader("X-CSRF-Token", token);
		HttpEntity entity = null;
		try {
			entity = new StringEntity(body);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		post.setEntity(entity);

		HttpResponse response = null;
		try {
			response = getHttpClient().execute(post);
		} catch (ClientProtocolException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		System.out.println("Response statusCode for Batch => "
				+ response.getStatusLine().getStatusCode());
	}
	
	public static void main(String[] args) {
		SimpleOrderCreator tool = new SimpleOrderCreator();
		String token = tool.getCSRFToken();
		String body = "{" + 
    "\"Name\": \"Jerry Test 2019-1-23 16:05PM\"," + 
    "\"TypeCode\": \"2059\"," + 
    "\"BuyerID\":\"ee\"," + 
    "\"PartyID\":\"60102\", " + 
    "\"CustomerQuoteText\":[{" + 
    	"\"Text\": \"test comment by Jerry Wang\", " + 
    	"\"TypeCode\": \"10024\"" + 
    "}]," + 
    "\"CustomerQuoteItem\":[{ " + 
    	"\"ProductID\": \"1042416\"" + 
    "}]," + 
    "\"CustomerQuoteItemProposal\":[{" + 
    	"\"ProductUUID\": \"00163E72-09C6-1EE8-BBDC-AC5F0CB0D795\"," + 
    	"\"Quantity\": \"1\"," + 
    	"\"unitCode\": \"EA\"" + 
    "}]" + 
"}";
		
		tool.createSO(token, body);
	}
}

为了避免这些缺点,提高开发人员使用 S/4HANA OData 进行集成的开发效率,这就是 S/4HANA Cloud SDK 的用武之地:

借助 S/4HANA Cloud SDK,开发人员无需了解 S/4HANA OData 服务的设计细节,可以采用类似自然语言的代码风格,调用 S/4HANA OData 服务,实现数据的增删改查。

下图的例子使用 S/4HANA Cloud SDK 读取 BusinessPartnerCategory 字段值为 2 的前20条 BusinessPartner 数据,并且只返回这些数据的 Partner Name, FullName 和 Customer 等字段。代码采用 Declarative 即声明式的方式编写,可读性大大优于直接操作 HTTP Post 负载的做法。

除了 API Business Hub 上提到的这三种 API 之外,开发人员还可以使用 HANA Client 工具库,直接把待执行的 SQL 语句发送给 HANA Cloud 实例;后者执行完毕后,将结果返回给 HANA Client:

完整的源代码:

sql 复制代码
REPORT zjerry_adbc.

CLASS demo DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS main.
  PRIVATE SECTION.
    CLASS-DATA: BEGIN OF result_line,
                  carrid TYPE sflight-carrid,
                  connid TYPE sflight-connid,
                  fldate TYPE sflight-fldate,
                END OF result_line,
                result_tab LIKE TABLE OF result_line.
ENDCLASS.

CLASS demo IMPLEMENTATION.
  METHOD main.
    DATA: carrid    TYPE sflight-carrid VALUE 'AA',
          cols      TYPE adbc_column_tab,
          lv_carrid TYPE string,
          con_ref   TYPE REF TO cl_sql_connection,
          con_name  TYPE dbcon-con_name VALUE 'DEFAULT'.

    cols = VALUE #( ( CONV adbc_name( 'CARRID' ) )
                    ( CONV adbc_name( 'CONNID' ) )
                    ( CONV adbc_name( 'FLDATE' ) ) ).
    lv_carrid = cl_abap_dyn_prg=>quote( to_upper( carrid ) ).
    TRY.
        con_ref = cl_sql_connection=>get_connection( con_name ).
        DATA(statement) = con_ref->create_statement( ).
        DATA(lv_query) = `SELECT carrid, connid, fldate ` &&
         `FROM sflight ` &&
         `WHERE mandt  = ` && `'` && sy-mandt && `' AND` &&
         `      carrid = ` &&  lv_carrid.
        DATA(result) = statement->execute_query( lv_query ).
        result->set_param_table( itab_ref = REF #( result_tab )
                                 corresponding_fields = cols ).
        IF result->next_package( ) > 0.
          SORT result_tab BY carrid connid fldate.
          WRITE:/ 'Number of lines found: ', lines( result_tab ).
        ENDIF.
        con_ref->close( ).
      CATCH cx_sql_exception INTO DATA(err).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  demo=>main( ).

本文介绍一种类似的数据消费方式:Open Database Connectivity 即 ODBC,开放数据库互连。

ODBC 提供了一种标准的 API 方法来访问数据库管理系统即 DBMS. ODBC API 利用 SQL 来完成绝大多数数据库操作。

下面我们通过一个实际的例子来了解 ODBC 的用法。这个例子的场景是,在 BTP 平台 ABAP 运行环境里,创建数据库表和对应的 CDS view,然后使用本地的 Excel 文件,通过 ODBC 访问 CDS view 的内容。

首先在 BTP ABAP 运行环境里创建两个简单的数据库表,存放订单抬头和行项目信息:

插入一些测试数据:

然后创建对应的 CDS view:

下面我们需要通过 Service Definition 将这两个 CDS view 的数据暴露给外部消费者。

通过 ODBC 将 CDS view 暴露给外部消费者,仍然需要按照上述顺序创建对应的开发对象。 首先创建 Service Definition,将之前创建的两个 CDS view 通过关键字 expose 暴露出去,通过 as 关键字设置别名为 Orders 和 OrderItems:

基于这个 Service Definition 创建一个新的 Service Binding:

Binding 类型,从下拉菜单里选择 SQL1,以支持 ODBC:

Service Binding 的名称起名为 ZORDERS,在文章最后通过 Excel 消费这个 Service 时,能在 Excel 里看到同名的节点,其下包含了通过 Service Definition 暴露出来的 OrderItems 和 Orders 两个视图:

创建新的 Communication Scenario:

在该 Scenario 的 Inbound 标签页里,分配标准的 Inbound Service S_PRIVILEGED_SQL1,用于支持对 CDS view 的 SQL 访问操作。

切换到 Authorization 标签页,添加新的 Authorization 对象 S_SQL_VIEW,并分别维护其三个字段的值:

  • SQL_SCHEMA: ZORDERS,即我们要授予访问权限的 Service Binding 名称。
  • SQL_VIEW: *. 这个值意味着允许访问 Service Binding ZORDERS 关联的 Service Definition 中,通过 expose 关键字暴露的所有 CDS view.
  • SQL_VIEWOP: SELECT,只读访问。

点击 Publish Locally:

创建一个新的 Communication System,取名 SQL_ACCESS:

给其分配一个新建的 Communication User. 稍后我们在 Excel 里使用 ODBC 读取 CDS View 数据时,会要求输入该用户的访问密码。

最后,创建 Communication Arrangement,把生成的 Service URL 即下图黄色高亮区域的字段抄下来,后续创建 Data Source 时,会填写该值。

在本地安装 ABAP ODBC Driver,访问 Support Portal,输入关键字 ODBC DRIVER FOR ABAP 进行搜索:

根据操作系统的类型下载对应版本的驱动:

同时下载 CRYPTOLIB:

安装完下载的驱动后,启动操作系统的 ODBC Data Source Administrator 应用,添加一条新的 User Data Source Name(简称为 User DSN):

ABAP ODBC 驱动安装成功后,在新建向导里能看到新的驱动类型:ODBC driver for ABAP.

在 DSN 创建向导中维护 Data Source 的明细。其中 Hostname 字段,来自 Communication Arrangement 的 Service Url 字段值。Crypto Library 字段,为前文从 Support Portal 下载的 CRYPTOLIB 解压到本地后的绝对路径:

Data Source 创建好之后,新建 Excel 文件,选择 From ODBC 进行导入:

选择刚刚建好的名为 Jerry-ABAP 的 Data Source,输入 Communication Scenario 里维护的用户名和密码:

点击 Connect,即可预览 BTP ABAP 环境里 Service Definition 暴露出来的 CDS view 的数据:

点击 Load 按钮,将这些数据导入到 Excel 中。

在 Advanced options 里,我们可以自由编写 SQL 语句,将 Excel 当成一个简单的 SQL 控制台使用:

上图编写的 SQL 语句,将订单行项目的数量进行求和,并显示结果到 Excel 里:

相信通过本文的例子,大家能对如何通过 ODBC 消费 BTP 平台 ABAP 环境的 CDS view 数据有了一个直观的认识,感谢阅读。

相关推荐
Q_192849990616 分钟前
基于Spring Boot的电影网站系统
java·spring boot·后端
豌豆花下猫29 分钟前
Python 潮流周刊#83:uv 的使用技巧(摘要)
后端·python·ai
凡人的AI工具箱36 分钟前
每天40分玩转Django:Django部署概述
开发语言·数据库·后端·python·django
SomeB1oody1 小时前
【Rust自学】7.2. 路径(Path)Pt.1:相对路径、绝对路径与pub关键字
开发语言·后端·rust
SomeB1oody1 小时前
【Rust自学】7.3. 路径(Path)Pt.2:访问父级模块、pub关键字在结构体和枚举类型上的使用
开发语言·后端·rust
Vol火山1 小时前
云原生架构中的中间件容器化:优劣势与实践探索
云原生·中间件·架构·数字化
Bony-2 小时前
Go语言反射从入门到进阶
开发语言·后端·golang
凡人的AI工具箱2 小时前
每天40分玩转Django:Django Email
数据库·人工智能·后端·python·django·sqlite
后端转全栈_小伵2 小时前
SQLite本地数据库的简介和适用场景——集成SpringBoot的图文说明
数据库·spring boot·后端·sqlite·学习方法
dbcat官方3 小时前
2.微服务灰度发布落地实践(agent实现)
微服务·云原生·架构