一文说清楚ABAP中的‘显示提交(Explicit Commit)’和‘隐式提交(Implicit Commit)’

1. 背景

做ABAP开发的同学一定会遇到'显示提交(Explicit Commit)'和'隐式提交(Implicit Commit)'这两个概念,但很多时候会产生混淆。

今天,让我们用一篇博文,系统地去阐述下这两个概念的基础原理。

2. DB LUW vs SAP LUW

要厘清commit这件事,需要非常明确地知道两个概念:DB LUW vs SAP LUW

  • DB LUW : 数据库层的一次提交/回滚边界,从上一次提交/回滚之后开始,到下一次提交/回滚结束。Open SQL 的 INSERT/UPDATE/DELETE 等改动属于当前 DB LUW。
  • SAP LUW : ABAP 应用层的逻辑事务,可能跨多个对话步(Dynpro),也即可能会涉及多个screen,多个function call 。在应用语义上要求"要么全做,要么全不做"。SAP LUW以关键字COMMIT WORKROLLBACK WORK的结束。SAP LUW 结束时,会触发一次 DB LUW 的提交/回滚 ,并释放SAP锁等。

3. DB COMMIT

首先,我们聊一下什么是DB COMMIT?其实很简单,中文的意思就是数据库提交

通过DB COMMIT,我们期望数据库从一致性状态A变更到一致性状态B。 也就是说,我们期望对于数据库的操作是all or nothing

要么所有变更全部成功,要么所有变更全部失败。通过DB COMMIT操作,数据库系统会关闭当前的DB LUW,同时打开一个新的DB LUW。

我们平时说的'显示提交(Explicit Commit)'和'隐式提交(Implicit Commit)',其实暗指的语义背景都是指DB COMMIT。

换句话说,就是数据库的显示提交 or 数据库的隐式提交

3.1 DB Explicit COMMIT

在 ABAP 程序中,可以通过以下方式,显式触发数据库提交:

  1. 使用相应数据库Native SQL语句 。 在 ADBC 中只能使用接口 IF_SQL_CONNECTION 的方法 COMMIT 来执行提交。其他情况下,数据库接口可能无法检测到事务的结束,从而可能无法执行某些必要动作。在 EXECENDEXEC 之间静态嵌入的native SQL的COMMIT 语句会被数据库接口检测到,并执行所需动作。

  2. 执行 ABAP SQL 语句 COMMIT CONNECTION 关键字。 这样,数据库提交会在指定的数据库连接上执行。

  3. 调用函数模块DB_COMMIT 。该函数模块封装了相应的本机 SQL 语句。默认情况下,数据库提交在当前为 EXEC SQL 打开的连接上触发。若向输入参数 IV_DEFAULT 传入 abap_true,则会在标准连接上显式触发提交。函数模块 DB_COMMIT 会触发类 CL_DBI_TRANSACTION_STATE 的事件 DB_TRANSACTION_FINISHED,该事件由应用日志框架处理。

  4. 执行 ABAP SQL 语句 COMMIT WORK 关键字 。 这个关键字会在所有当前已打开的数据库连接上,执行数据库提交动作。 COMMIT WORK 还会关闭当前的 SAP LUW,并执行相关联的动作 (下文中将进一步详细解释)。

注意:在 AMDP 方法中不允许使用 COMMIT 语句。

为了更好理解DB Explicit COMMIT的方式,我们可以看下DB_COMMIT这个函数的标准代码:

js 复制代码
FUNCTION db_commit.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*"  IMPORTING
*"     REFERENCE(IV_DEFAULT) TYPE  ABAP_BOOL DEFAULT ABAP_FALSE
*"----------------------------------------------------------------------

  IF iv_default = abap_false.
    EXEC SQL.
      COMMIT WORK
    ENDEXEC.
  ELSE.
    COMMIT CONNECTION default.
  ENDIF.

  CALL METHOD cl_dbi_transaction_state=>raise_db_transaction_finished
    EXPORTING
      iv_kind = cl_dbi_transaction_state=>gc_commit.

ENDFUNCTION.

在这个函数中,我们可以看到,它封装两种我们提到的DB COMMIT方式,也即使用native sql或使用COMMIT CONNECTION关键字。

需要注意的是,通过DB_COMMIT这个函数,仅仅会完成数据库的提交,它不会触发SAP LUW

通常而言,当系统通过Native SQL中的COMMIT WORK当前有效数据库连接上执行一次数据库提交时,这个连接就是 AS ABAP 系统中央数据库的标准连接(也称默认连接default connetion)。

但需要注意:可以通过以下 EXEC SQL 语句全局设置本机 SQL 使用的"当前数据库连接":

js 复制代码
 EXEC SQL. 
 CONNECT TO con 
 ENDEXEC. 
 
 EXEC SQL. 
 SET CONNECTION con 
 ENDEXEC.

因此,如果在调用 DB_COMMIT 之前,已用上述语句把当前的本机 SQL 连接切换为某个次级数据库连接(secondary connection),那么提交将发生在该次级连接上,而不是标准连接上。这可能对事务逻辑产生不良影响,导致数据不一致。 需要注意。

函数DB_COMMIT的参数IV_DEFAULT是可选的。将其设为 abap_true(默认值为 abap_false)时,无论当前的本机 SQL 连接被设置为何值,都强制在"标准连接"上执行数据库提交。

辨析:

与 Open SQL 的 COMMIT WORK 不同,调用 DB_COMMIT 并不会关闭当前的 SAP LUW,即不会触发与 SAP LUW 结束相关的事件。

但是,在数据库提交成功后,会触发类 CL_DBI_TRANSACTION_STATE 的事件 DB_TRANSACTION_FINISHED,其参数 KIND 的取值为 CL_DBI_TRANSACTION_STATE=>GC_COMMIT

看一个小例子:

js 复制代码
DATA lt_extract TYPE TABLE OF zgg_caa_extract.

lt_extract = VALUE #( ( rbukrs = 'CFP1' belnr = '9000000001' gjahr = '2025' racct = '0000910000' ) ).

INSERT zgg_caa_extract FROM TABLE lt_extract.

CALL FUNCTION 'DB_COMMIT'.

BREAK-POINT.

WRITE: 'Program finished'.

可以看到,执行完DB_COMMIT 后,程序并未结束,但已经完成了数据库的更新。

3.2 DB Implicit COMMIT

数据库的隐式提交是AS ABAP架构上的一个设计。

因为AS ABAP 使用自身的工作进程(work process)登录到数据库系统。一个工作进程一次只能执行一个数据库 LUW,不能干预其他工作进程所属的数据库 LUW。由于一个 ABAP 程序在运行期间可能由不同的工作进程执行,凡是可能导致工作进程切换的动作,都必须先结束当前工作进程的数据库 LUW。

下面是一些典型的场景:

  1. 一个对话步骤的完成(Dialog Step Completion) : 例如有屏幕切换的动作,这个例如下面的这些关键字:
    • CALL TRANSACTION ...:从当前程序切换到另一个事务前,系统会对当前 DB LUW 执行一次隐式 COMMIT,以确保两个事务相互独立。
    • LEAVE TO TRANSACTION ...:同上,离开当前事务、进入新事务前隐式 COMMIT。
    • SUBMIT 报表(含 AND RETURN 场景):SUBMIT 前后通常各有一次隐式 COMMIT,使得被提交的可执行程序与当前程序的 LUW 相互隔离。注意与"提交后台作业"是两个概念。
    • LEAVE PROGRAMSTOP:结束当前程序时的隐式 COMMIT。
  2. sRFC/aRFC调用:以同步或异步远程函数调用方式调用函数模块时,当前工作进程将控制权交给其他工作进程或系统,此时会触发隐式DB COMMIT。例外:在更新期间(update),sRFC 和 aRFC 不会导致工作进程切换或执行数据库提交。
  3. WAIT关键字:当WAIT语句中断当前工作进程时,在将进程移交之前会执行隐式提交。
  4. 错误或警告消息 :在对话步骤期间发送消息(例如,类型为 IWE 的 MESSAGE)时,当控制返回到 SAP GUI 时会触发隐式提交。
  5. HTTP/HTTPS 通信:在互联网通信框架(ICF)交互期间,在发送响应之前会执行数据库提交操作。

注:

  • 当退出一个内部会话时,标准连接上不会发生隐式数据库提交,也不会执行数据库回滚。如有需要,必须显式编程实现。
  • 如果使用 ABAP SQL 语句填充的全局临时表(GTT),在发生隐式数据库提交时未通过显式数据库提交或数据库回滚来清空,且没有通过无 WHERE 条件的 DELETE FROM 语句清空,则会发生运行时错误 COMMIT_GTT_ERROR。

举个例子:

js 复制代码
DATA lt_extract TYPE TABLE OF zgg_caa_extract.

lt_extract = VALUE #( ( rbukrs = 'CFP1' belnr = '9000000002' gjahr = '2025' racct = '0000910000' ) ).

INSERT zgg_caa_extract FROM TABLE lt_extract.

*CALL TRANSACTION 'FB03'.
*MESSAGE 'Hello' TYPE 'E'.
CALL FUNCTION 'RFC_PING' DESTINATION 'Q7Q_002'.

WRITE: 'Program finished'.

在上面的代码中,调用了一个RFC后,发生了数据库的隐式提交,Insert语句所操作的数据完成隐式落库。

4. SAP COMMIT

注:SAP COMMIT是我引入的一个词汇,代指ABAP中的COMMIT WORK关键字操作。主要用作与DB COMMIT这个操作的对比和区分。SAP COMMIT是没有隐式的COMMIT,必须通过ABAP 关键字COMMIT WORK完成显示的COMMIT。

可以看到,由于DB COMMIT存在隐式提交的架构特性,这让ABAP程序中保证数据一致性的很有挑战,一方面,程序员必须很小心的去识别可能的隐式提交点,另一方面,对于需要多个对话步的场景,从DB LUW的角度是没办法实现业务数据的最终一致性的。

更准确地说,SAP LUW是传统SAP GUI和dynpros这种经典面向对话应用程序产生的设计概念,因为在这种会话程序中,每一个会话步骤都是一个独立的DB LUW。 通过SAP LUW的设计,可以将若干个相关DB LUW所需处理的数据打包在一起(bundling),然后在一个单独的work process中,形成一个独立的DB LUW完成数据库提交。

4.1 基础技术

SAP LUW的技术架构属于"先注册,后执行"的模式,也即,在应用程序中,对于DB的变更请求先进行注册,然后程序结束前,统一提交执行。

为了实现这套架构,AS ABAP中提供一下的支持性技术:

  1. Update更新(用函数模块打包) : 通过语句 CALL FUNCTION ... IN UPDATE TASK,将一个更新函数模块注册起来,稍后由更新工作进程执行(同步或异步更新),或者由当前工作进程执行(本地更新,local update)。在更新任务中,不能执行被禁止的语句(例如 CALL DIALOG,CALL SCREEN,SUBMIT etc),也不会执行权限检查(authorization check)。 同步和异步更新在各自的更新会话中执行;相反,本地更新只会打开一个新的内部会话。

  2. 远程函数调用(用函数模块打包) : 通过语句 CALL FUNCTION ... IN BACKGROUND UNIT,将一个可远程调用的函数模块注册起来,稍后通过 RFC 接口在后台异步执行(bgRFC)。

  3. 使用子例程打包 : 通过语句 PERFORM ... ON COMMIT,将一个子例程注册起来,以便稍后在另一个工作进程中执行。

4.2 SAP LUW 相关的关键字

SAP LUW 相关的 ABAP SQL 语句有以下3个:

  • COMMIT WORK
  • ROLLBACK WORK
  • SET UPDATE TASK LOCAL

COMMIT WORK 和 ROLLBACK WORK 决定了一个 SAP LUW 的边界。一个 ABAP 程序可以被划分为任意数量的 SAP LUW;当 ABAP 程序结束时,最后一个 SAP LUW 也会随之结束。

通过 CALL TRANSACTION 或 SUBMIT ... AND RETURN 调用 ABAP 程序时,可以实现SAP LUW的嵌套。系统类 CL_SYSTEM_TRANSACTION_STATE 包含用于返回当前 SAP LUW 状态的方法。

4.3 SAP LUW 和Internal Session

每次关闭一个内部会话,都会结束当前的 SAP LUW。当程序通过以下方式结束或关闭内部会话时:

  • SUBMIT(不带 AND RETURN)
  • LEAVE TO TRANSACTION

如果当前 SAP LUW 中仍有已注册的过程,那么该 SAP LUW 会被结束,这些过程既不会被调用,也不会被回滚。已注册的更新函数模块会保留在数据库中,但将无法再被执行。

但如果程序是通过以下方式被调用:

  • SUBMIT(带 AND RETURN),或
  • CALL TRANSACTION

那么会开启一个新的 SAP LUW,但不会开启新的DB LUW

这意味着,在该 SAP LUW 中执行数据库回滚,也即会回滚通过 CALL FUNCTION IN UPDATE TASK 或 CALL FUNCTION IN BACKGROUND TASK 在表 VB... 或 ARFCSSTATE、ARFCSDATA 中所做的全部登记。

在某些情况下,被调用程序中的 ROLLBACK WORK 还可能影响到被中断的 SAP LUW。 为避免这种情况,应在调用该程序之前显式执行一次数据库提交(COMMIT)。

对于本地更新(local update),则不会出现这个问题。

4.4 详解 COMMIT WORK

OSQL 中的语句 COMMIT WORK 会关闭当前的 SAP LUW,并开启一个新的 SAP LUW。当前 SAP LUW 中的所有更改请求都会被提交。

为此,COMMIT WORK 会执行以下操作:

  1. 执行当前工作进程中,通过 PERFORM ON COMMIT 注册的所有子例程
  • 执行顺序基于注册顺序,或根据使用附加项 LEVEL 指定的优先级。
  • 在此类子例程中不允许执行以下语句:
    • PERFORM ... ON COMMIT|ROLLBACK
    • COMMIT WORK
    • ROLLBACK WORK
  • 允许执行 CALL FUNCTION ... IN UPDATE TASK。
  1. 为对象服务(Object Services)的持久化服务触发一个内部事件
  • 如果持久化服务注册了事件处理程序,它会收集由该服务管理的对象的更改,并使用 CALL FUNCTION ... IN UPDATE TASK 将这些更改传递给一个特殊的更新函数模块,该模块被注册为最终更新模块。
  1. 触发所有通过 CALL FUNCTION ... IN UPDATE TASK 注册的更新函数模块的处理:在更新工作进程中处理,或对于本地更新也可在当前工作进程中处理。
  • 所有高优先级(VB1)的更新函数模块会按注册顺序,并在同一个DB LUW 中执行。如果未指定附加项关键字AND WAIT,在非本地更新中,程序不会等待更新工作进程的执行完成(异步更新),而是在 COMMIT WORK 后立即继续执行。若指定了 AND WAIT,则程序会在更新工作进程执行完高优先级更新函数模块后才继续(同步更新)。
  • 在所有高优先级更新函数模块成功完成后,会在更新工作进程中,按注册顺序在同一数据库 LUW 中执行低优先级(VB2)的更新函数模块。
  • 在由 COMMIT WORK 触发的更新函数模块处理期间,不允许发生数据库提交或数据库回滚,也不允许修改更新控制。任何会导致此种情况的语句在更新中均为禁用语句,且会始终引发运行时错误。同往常一样,不能进行权限检查。
  • 在高优先级更新函数模块执行完成之后,通过 CALL FUNCTION ... IN BACKGROUND UNIT 以及 CALL FUNCTION ... IN BACKGROUND TASK(已废弃)注册的函数模块,会按每个 RFC 目的地各自的单个数据库 LUW 执行。
  1. 按照相应锁函数模块形参 _SCOPE 的取值,处理当前程序中设置的所有 SAP 锁

  2. 对所有当前打开的数据库连接触发数据库提交(DB COMMIT),这也会终止当前的数据库 LUW,并关闭所有数据库游标

注: 在通过批输入(batch input)执行的程序中,或者通过 CALL TRANSACTION USING 调用程序时,COMMIT WORK 默认会终止批输入处理。此设置可在 CALL TRANSACTION USING 中通过 OPTIONS FROM 传入的结构 CTU_PARAMS 的组件 RACOMMIT 进行覆盖。

COMMIT WORK会更新系统字段sy-subrc。sy-subrc 含义为:

  • 0:指定了附加项 AND WAIT,且更新函数模块的更新成功。
  • 4:指定了附加项 AND WAIT,但更新函数模块的更新未成功。
  • 如果未指定 AND WAIT,COMMIT WORK 始终将 sy-subrc 设为 0。

其它提示:

  • 未由 COMMIT WORK 关闭、而是通过结束当前程序或关闭内部会话而结束的 SAP LUW,将被已注册的过程忽略。已注册的更新函数模块会保留在数据库中,但不再可执行。
  • COMMIT WORK 会清空所有当前打开的数据库连接上的全局临时表,并在发生隐式数据库提交时防止运行时错误 COMMIT_GTT_ERROR。
  • 语句 COMMIT WORK 会关闭所有数据库游标。随后访问数据库游标的 ABAP SQL 语句(例如 SELECT 循环)会引发不可捕获的异常。ABAP SQL 语句 FETCH 也是这种情况的一个例子。
  • 如果将通过 PERFORM ... ON COMMIT 注册子例程与通过 CALL FUNCTION ... IN UPDATE TASK 注册更新函数模块结合使用,需要特别注意所涉及的数据库 LUW。尤其是在异步更新中,在更新处理之外注册的过程会在不同的工作进程中执行,因此也在不同的数据库 LUW 中执行。子例程所在数据库 LUW 中的更改可能会在更新函数模块执行之前就被提交,而异步更新期间的数据库回滚并不会回滚这些更改。
  • 语句 PERFORM ... ON COMMIT 可以在更新中执行。所注册的子程序必须定义在当前函数组(函数池)中,并会在当前更新结束时执行。
  • COMMIT WORK 也会在通过 CALL DIALOG 调用的程序中触发数据库提交。
  • 如果由 COMMIT 触发的更新中发生运行时错误,更新工作进程会执行数据库回滚,将其记录在相应的 DDIC 数据库表中,并通过 SAPMail 通知创建这些条目的用户。排除错误原因后,可以再次执行这些被取消的更新条目。

5. 小结

在上面的文章中,我们已经详细了解了COMMIT这个关键字在ABAP语言中的含义,现在让我们做个小结:

首先是关于COMMIT这件事:

  • COMMIT分为两种:DB COMMIT和SAP COMMIT,它们分别对应DB LUW和SAP LUW的提交;SAP LUW的提交 触发DB LUW的提交;DB LUW 的提交不会触发SAP LUW的提交。
  • DB COMMIT分为显示DB COMMIT和隐式DB COMMIT;而SAP COMMIT没有隐式COMMIT一说。

需要注意的点:

  1. 因为显示的COMMIT是由ABAP程序员控制的,但隐式DB COMMIT是由AS ABAP框架触发的,在编程过程中要留意隐式DB COMMIT的影响,例如,在SELECT...ENDSELECT这种打开数据库cursor的场景中,在这个循环内部,不要调用RFC,不要有CALL TRANSACTION等这种会引发隐式DB COMMIT的操作。此种场景会引发DUMP。
  2. 对于使用全局临时表(GTT: Global Temporary Tables)的场景,要注意,在DB COMMIT前,要手动清除其内部的数据,否则会DUMP。
  3. 对于处理业务数据一致性的场景,首推使用SAP LUW技术,通过SAP LUW完成相关数据的打包和提交,不要直接使用DB COMMIT去提交数据(除非有特殊场景,例如要独立存储某些独立于SAP LUW的数据)。
  4. 注意SAP LOCK的释放,SAP COMMIT(注册有IN UPDATE TASK,会释放_scope = 2的锁)会释放SAP LOCK,但DB COMMIT不会影响SAP LOCK。
  5. 调 BAPI 后是否需要 BAPI_TRANSACTION_COMMIT 要看该 BAPI 文档;多数业务 BAPI 不自动提交。
  6. 使用 Native SQL/DDL 要格外小心潜在的数据库隐式提交。