* ABAP Dynamic Report Extraction and ADBC CRUD Practical Solution (V2)
* This version is the complete final version including a dual-level foreground control console
* (configuration table selection + data interception), dynamic table creation,
* RTTS type reconstruction, and ADBC batch insertion/deletion.
* 1. Main Program and Selection Screen (REPORT ZDEMOA & T01)
*&---------------------------------------------------------------------*
*& Report ZDEMOA
*&---------------------------------------------------------------------*
REPORT Zdemoa.
INCLUDE Zdemoa_t01.
INCLUDE Zdemoa_frm.
START-OF-SELECTION.
IF p_back = 'X'.
PERFORM frm_getdata_push.
ELSEIF p_fore = 'X'.
PERFORM frm_fore_select_config.
ENDIF.
*&---------------------------------------------------------------------*
*& INCLUDE ZDEMOA_T01
*&---------------------------------------------------------------------*
TABLES: Zdemoat1.
DATA: gt_Zdemoat1 TYPE TABLE OF Zdemoat1.
TYPES: ts_Zdemoat1 TYPE Zdemoat1.
*&---------------------------------------------------------------------*
*& Selection Screen (Controls foreground/background execution modes)
*&---------------------------------------------------------------------*
SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.
PARAMETERS: p_back TYPE c RADIOBUTTON GROUP g1 DEFAULT 'X', " Background mode: Automatic full push
p_fore TYPE c RADIOBUTTON GROUP g1. " Foreground mode: Pop-up manual selection
SELECTION-SCREEN END OF BLOCK b1.
* 2. Core Business Logic (INCLUDE ZDEMOA_FRM)
*&---------------------------------------------------------------------*
*& INCLUDE ZDEMOA_FRM
*&---------------------------------------------------------------------*
FORM frm_getdata_push.
" --- 1. Data Definition Area ---
DATA: lr_data TYPE REF TO data.
DATA: lv_submit_prog TYPE program,
lv_submit_vari TYPE variant.
" RTTS Description Objects
DATA: lo_table_descr TYPE REF TO cl_abap_tabledescr,
lo_struct_descr TYPE REF TO cl_abap_structdescr,
lt_components TYPE cl_abap_structdescr=>component_table,
ls_component LIKE LINE OF lt_components.
FIELD-SYMBOLS: <lt_alv_data> TYPE STANDARD TABLE,
<ls_row> TYPE any,
<lv_value> TYPE any.
" --- 2. Get configuration table data ---
IF p_back = 'X'.
SELECT * FROM Zdemoat1
INTO CORRESPONDING FIELDS OF TABLE gt_Zdemoat1
WHERE zblock_push = ''.
ELSE.
ENDIF.
SELECT * FROM Zdemoat2
INTO TABLE @DATA(lt_Zdemoat2).
IF sy-subrc = 0.
SORT lt_Zdemoat2 BY progname variant.
ENDIF.
" Predefined mapping internal table
DATA: lt_mapping LIKE lt_Zdemoat2.
" --- 3. Loop through each ALV program ---
LOOP AT gt_Zdemoat1 INTO DATA(ls_Zdemoat1).
" Reset key pointers and variables
CLEAR: lv_submit_prog, lv_submit_vari, lt_mapping.
IF <lt_alv_data> IS ASSIGNED.
UNASSIGN <lt_alv_data>.
ENDIF.
lv_submit_prog = to_upper( condense( ls_Zdemoat1-progname ) ).
lv_submit_vari = to_upper( condense( ls_Zdemoat1-variant ) ).
IF lv_submit_prog IS INITIAL. CONTINUE. ENDIF.
" --- 4. Set runtime parameters and execute ---
cl_salv_bs_runtime_info=>clear( ).
cl_salv_bs_runtime_info=>set(
EXPORTING
display = abap_false
metadata = abap_false
data = abap_true
).
SUBMIT (lv_submit_prog) USING SELECTION-SET lv_submit_vari AND RETURN.
" --- 5. Get data ---
TRY.
cl_salv_bs_runtime_info=>get_data_ref( IMPORTING r_data = lr_data ).
ASSIGN lr_data->* TO <lt_alv_data>.
CATCH cx_salv_bs_sc_runtime_info.
WRITE: / 'Warning: Program', lv_submit_prog, 'did not return ALV data'.
cl_salv_bs_runtime_info=>clear( ).
CONTINUE.
ENDTRY.
" --- 6. Process output logic ---
IF <lt_alv_data> IS ASSIGNED AND <lt_alv_data> IS NOT INITIAL.
" Extract field mapping corresponding to the current program
lt_mapping = VALUE #( FOR ls_m IN lt_Zdemoat2
WHERE ( progname = lv_submit_prog AND variant = lv_submit_vari )
( ls_m ) ).
WRITE: / '-------------------------------------------'.
WRITE: / 'Executing program:', lv_submit_prog, ' Variant:', lv_submit_vari.
TRY.
lo_table_descr ?= cl_abap_tabledescr=>describe_by_data( <lt_alv_data> ).
lo_struct_descr ?= lo_table_descr->get_table_line_type( ).
lt_components = lo_struct_descr->get_components( ).
CATCH cx_root.
WRITE: / 'Error: Dynamic structure parsing failed'.
ENDTRY.
" =====================================================================
" [New] Dynamically insert ID, YEAR, MONTH at the very beginning of the structure definition (lt_components)
" =====================================================================
" 1. Prepare data type description objects (C32 for UUID, C4 for year, C2 for month)
DATA: lo_elem_id TYPE REF TO cl_abap_elemdescr,
lo_elem_year TYPE REF TO cl_abap_elemdescr,
lo_elem_month TYPE REF TO cl_abap_elemdescr.
lo_elem_id = cl_abap_elemdescr=>get_c( 32 ).
IF ls_Zdemoat1-zsfyearmonth = 'X'.
lo_elem_year = cl_abap_elemdescr=>get_c( 4 ).
lo_elem_month = cl_abap_elemdescr=>get_c( 2 ).
ENDIF.
" 2. Insert into positions 1, 2, and 3 in sequence
INSERT VALUE #( name = 'ID' type = lo_elem_id ) INTO lt_components INDEX 1.
IF ls_Zdemoat1-zsfyearmonth = 'X'.
INSERT VALUE #( name = 'YEAR' type = lo_elem_year ) INTO lt_components INDEX 2.
INSERT VALUE #( name = 'MONTH' type = lo_elem_month ) INTO lt_components INDEX 3.
ENDIF.
IF lt_mapping IS NOT INITIAL.
SORT lt_mapping BY sapfield.
" The purpose is to delete redundant fields
LOOP AT lt_components INTO ls_component.
IF ls_component-name = 'ID' OR ls_component-name = 'YEAR' OR ls_component-name = 'MONTH'.
CONTINUE.
ENDIF.
READ TABLE lt_mapping INTO DATA(ls_mapping) WITH KEY sapfield = ls_component-name.
IF sy-subrc <> 0.
DELETE TABLE lt_components FROM ls_component.
ENDIF.
ENDLOOP.
WRITE: / 'Output mode: Partial fields automatic output'.
PERFORM frm_crud_targetdata TABLES lt_components <lt_alv_data> USING ls_Zdemoat1 .
ELSE.
" Case B: All fields automatic output
WRITE: / 'Output mode: All fields automatic output'.
PERFORM frm_crud_targetdata TABLES lt_components <lt_alv_data> USING ls_Zdemoat1 .
ENDIF.
" --- Cleanup inside the loop ---
REFRESH lt_mapping.
UNASSIGN <lt_alv_data>.
ENDIF.
" Ensure the environment is cleaned up to prevent interference
cl_salv_bs_runtime_info=>clear( ).
ENDLOOP.
ENDFORM.
*&---------------------------------------------------------------------*
*& Form frm_crud_targetdata
*&---------------------------------------------------------------------*
FORM frm_crud_targetdata TABLES lt_components TYPE cl_abap_structdescr=>component_table
pt_table TYPE STANDARD TABLE
USING ps_Zdemoat1 TYPE ts_Zdemoat1.
DATA: lv_target_tbname TYPE tabname.
DATA: lt_val_tab TYPE TABLE OF rsparams.
DATA: lv_where TYPE string.
" Correct type name: Use TT_NAMED_SELTABS existing in the system
DATA: lt_shdb_seltables TYPE cl_shdb_seltab=>tt_named_seltables,
ls_shdb_seltables LIKE LINE OF lt_shdb_seltables. " The corresponding row type is usually ts_named_seltab
DATA: lt_where_clauses TYPE TABLE OF string.
IF ps_Zdemoat1-tabname IS NOT INITIAL.
lv_target_tbname = condense( ps_Zdemoat1-tabname ).
ENDIF.
SELECT * FROM Zdemoat3
INTO TABLE @DATA(lt_Zdemoat3)
WHERE progname = @ps_Zdemoat1-progname
AND variant = @ps_Zdemoat1-variant.
IF sy-subrc = 0.
SORT lt_Zdemoat3 BY sapfield.
CALL FUNCTION 'RS_VARIANT_CONTENTS'
EXPORTING
report = ps_Zdemoat1-progname
variant = ps_Zdemoat1-variant
TABLES
valutab = lt_val_tab
EXCEPTIONS
variant_non_existent = 1
variant_obsolete = 2
OTHERS = 3.
IF sy-subrc <> 0.
* Implement suitable error handling here
ENDIF.
LOOP AT lt_val_tab INTO DATA(ls_val_tab).
* If I don't read it, delete this query condition
READ TABLE lt_Zdemoat3 INTO DATA(ls_Zdemoat3) WITH KEY sapfield = ls_val_tab-selname BINARY SEARCH.
IF sy-subrc <> 0.
DELETE TABLE lt_val_tab FROM ls_val_tab.
ENDIF.
IF ls_val_tab-low IS INITIAL AND ls_val_tab-high IS INITIAL.
DELETE TABLE lt_val_tab FROM ls_val_tab.
ENDIF.
IF ls_val_tab-kind = 'P'.
IF ls_val_tab-low IS NOT INITIAL.
APPEND |{ ls_Zdemoat3-dbfield } = '{ ls_val_tab-low }'| TO lt_where_clauses.
ENDIF.
ELSEIF ls_val_tab-kind = 'S'.
IF ls_val_tab-low IS NOT INITIAL OR ls_val_tab-high IS NOT INITIAL.
CLEAR ls_shdb_seltables.
ls_shdb_seltables-name = ls_Zdemoat3-dbfield.
DATA: lr_range_table TYPE REF TO data.
CREATE DATA lr_range_table TYPE rs_t_range.
FIELD-SYMBOLS: <lt_range> TYPE rs_t_range.
ASSIGN lr_range_table->* TO <lt_range>.
APPEND VALUE #( sign = ls_val_tab-sign
opt = ls_val_tab-option
low = ls_val_tab-low
high = ls_val_tab-high ) TO <lt_range>.
IF <lt_range> IS NOT INITIAL.
ls_shdb_seltables-dref = lr_range_table.
APPEND ls_shdb_seltables TO lt_shdb_seltables.
CLEAR:ls_shdb_seltables.
ENDIF.
IF <lt_range> IS ASSIGNED.
UNASSIGN <lt_range>.
ENDIF.
IF lt_shdb_seltables IS NOT INITIAL.
TRY.
" Call the conversion method
DATA(lv_s_where) = cl_shdb_seltab=>combine_seltabs(
it_named_seltabs = lt_shdb_seltables
).
" If the converted SQL snippet is not empty, add it to the summary table
IF lv_s_where IS NOT INITIAL.
APPEND lv_s_where TO lt_where_clauses.
ENDIF.
CATCH cx_shdb_exception.
" Exception handling can be added here, e.g., WRITE: / 'SQL conversion exception'.
ENDTRY.
ENDIF.
CLEAR:lt_shdb_seltables,
lv_s_where.
ENDIF.
ENDIF.
ENDLOOP.
IF ps_Zdemoat1-zsfyearmonth = 'X'.
APPEND | YEAR = '{ sy-datum+0(4) }'| TO lt_where_clauses.
APPEND | MONTH = '{ sy-datum+4(2) }'| TO lt_where_clauses.
ENDIF.
* At this point I got lt_where_clauses
IF lt_where_clauses IS NOT INITIAL.
" Concatenate all rows in the internal table with ' AND '
lv_where = concat_lines_of( table = lt_where_clauses sep = ' AND ' ).
ENDIF.
DATA: lo_new_struct_descr TYPE REF TO cl_abap_structdescr,
lo_new_table_descr TYPE REF TO cl_abap_tabledescr,
lr_db_data TYPE REF TO data. " This is the reference you will use for ADBC
FIELD-SYMBOLS: <lt_db> TYPE STANDARD TABLE. " Entity internal table pointer for subsequent data reading
TRY.
" Step 1: Create a dynamic structure description object using the component table
lo_new_struct_descr = cl_abap_structdescr=>create( p_components = lt_components[] ).
" Step 2: Create a dynamic internal table description object using the dynamic structure
lo_new_table_descr = cl_abap_tabledescr=>create( p_line_type = lo_new_struct_descr ).
" Step 3: Truly allocate space for this dynamic internal table in memory (Data Reference)
CREATE DATA lr_db_data TYPE HANDLE lo_new_table_descr.
" Step 4: Assign the pointer for easy direct access to the data inside later
ASSIGN lr_db_data->* TO <lt_db>.
CATCH cx_root INTO DATA(lx_rtts_err).
WRITE: / 'Dynamic table generation failed:', lx_rtts_err->get_text( ).
RETURN.
ENDTRY.
" =======================================================================
" 2. Combine ADBC (cl_sql_connection) to execute dynamic query with WHERE
" =======================================================================
DATA: go_db TYPE REF TO cl_sql_connection,
go_sqlerr_ref TYPE REF TO cx_sql_exception.
TRY.
" Connect to the external database (ensure 'YOUR DBCO CONF' is the configured connection in DBCO)
go_db = cl_sql_connection=>get_connection( 'YOUR DBCO CONF' ).
" Assemble native SELECT * SQL statement
DATA(lv_stmt) = |SELECT * FROM { lv_target_tbname }|.
IF lv_where IS NOT INITIAL.
lv_stmt = |{ lv_stmt } WHERE { lv_where }|.
ENDIF.
" Create Statement and execute
DATA(lo_stmt_ref) = go_db->create_statement( tab_name_for_trace = lv_target_tbname ).
DATA(lo_res_ref) = lo_stmt_ref->execute_query( lv_stmt ).
" Core: Feed the newly generated lr_db_data directly to the ADBC output table
lo_res_ref->set_param_table( lr_db_data ).
" Fetch data into memory, now <lt_db> contains data!
DATA(lv_row_cnt) = lo_res_ref->next_package( ).
" Close cursor and release resources
lo_res_ref->close( ).
WRITE: / 'Successfully retrieved ', lv_row_cnt, ' records from the external database'.
CATCH cx_sql_exception INTO go_sqlerr_ref.
MESSAGE go_sqlerr_ref->sql_message TYPE 'E'.
ENDTRY.
IF <lt_db> IS NOT INITIAL.
" --- Extremely important security check: NEVER execute delete when WHERE is empty ---
IF lv_target_tbname IS NOT INITIAL AND lv_where IS NOT INITIAL.
TRY.
" 1. Get external database connection
go_db = cl_sql_connection=>get_connection( 'YOUR DBCO CONF' ).
" 2. Directly assemble the complete DELETE SQL statement
DATA(l_stmt_bulk_del) = |DELETE FROM { lv_target_tbname } WHERE { lv_where }|.
" 3. Create Statement object
DATA(l_stmt_ref_del) = go_db->create_statement( tab_name_for_trace = lv_target_tbname ).
" 4. Execute batch deletion at once
DATA(l_rows_deleted) = l_stmt_ref_del->execute_update( l_stmt_bulk_del ).
" 5. Commit transaction (writing/deleting in external system must COMMIT)
go_db->commit( ).
" Output success log
WRITE: / 'Batch deletion successful, affected rows:', l_rows_deleted.
CATCH cx_sql_exception INTO go_sqlerr_ref.
" Core fix: Catch exception of rollback itself
IF go_db IS NOT INITIAL.
TRY.
go_db->rollback( ).
CATCH cx_sql_exception.
" Ignore exceptions that occur during rollback
ENDTRY.
ENDIF.
MESSAGE go_sqlerr_ref->sql_message TYPE 'E'.
CATCH cx_parameter_invalid INTO DATA(lx_param).
MESSAGE lx_param->get_text( ) TYPE 'E'.
ENDTRY.
ELSE.
WRITE: / 'Warning: Table name or WHERE condition is empty. Batch deletion operation terminated to protect data!'.
ENDIF.
ENDIF.
" ***** Prepare to write data to the target database
IF pt_table[] IS NOT INITIAL.
" 1. Clear old data retrieved from previous SELECT, prepare to load new data
CLEAR <lt_db>.
" 2. Dynamically create a single-row structure pointer for data mapping
DATA: lr_db_row TYPE REF TO data.
CREATE DATA lr_db_row TYPE HANDLE lo_new_struct_descr.
FIELD-SYMBOLS: <ls_db_row> TYPE any.
ASSIGN lr_db_row->* TO <ls_db_row>.
" 3. Loop through source data, accurately extract fields present in lt_components
LOOP AT pt_table ASSIGNING FIELD-SYMBOL(<ls_pt_row>).
CLEAR <ls_db_row>.
LOOP AT lt_components INTO DATA(ls_comp_map).
" Get value from ALV source data row
ASSIGN COMPONENT ls_comp_map-name OF STRUCTURE <ls_pt_row> TO FIELD-SYMBOL(<lv_src_val>).
IF sy-subrc = 0.
" Assign value to the dynamic row of the external database
ASSIGN COMPONENT ls_comp_map-name OF STRUCTURE <ls_db_row> TO FIELD-SYMBOL(<lv_tgt_val>).
IF sy-subrc = 0.
<lv_tgt_val> = <lv_src_val>.
ENDIF.
ENDIF.
ENDLOOP.
" --- Forcibly inject dynamic data (ID, YEAR, MONTH) ---
" A. Generate 32-bit UUID (use TRY CATCH to prevent class errors causing a Dump)
TRY.
DATA(lv_new_uuid) = cl_uuid_factory=>create_system_uuid( )->create_uuid_c32( ).
CATCH cx_uuid_error.
lv_new_uuid = sy-sysid && sy-datum && sy-uzeit. " Fallback fault-tolerance solution
ENDTRY.
" B. Assign values to newly added fields of the dynamic structure
ASSIGN COMPONENT 'ID' OF STRUCTURE <ls_db_row> TO FIELD-SYMBOL(<lv_tgt_id>).
IF sy-subrc = 0. <lv_tgt_id> = lv_new_uuid. ENDIF.
IF ps_Zdemoat1-zsfyearmonth = 'X'.
ASSIGN COMPONENT 'YEAR' OF STRUCTURE <ls_db_row> TO FIELD-SYMBOL(<lv_tgt_year>).
IF sy-subrc = 0. <lv_tgt_year> = sy-datum+0(4). ENDIF.
ASSIGN COMPONENT 'MONTH' OF STRUCTURE <ls_db_row> TO FIELD-SYMBOL(<lv_tgt_month>).
IF sy-subrc = 0. <lv_tgt_month> = sy-datum+4(2). ENDIF.
ENDIF.
" Append the mapped single row to the dynamic internal table
APPEND <ls_db_row> TO <lt_db>.
ENDLOOP.
" 4. Assemble dynamic INSERT SQL statement
IF <lt_db> IS NOT INITIAL.
DATA: lt_fields TYPE TABLE OF string,
lt_values TYPE TABLE OF string.
" Generate dynamically based on component table: (FIELD1, FIELD2) VALUES (?, ?)
LOOP AT lt_components INTO DATA(ls_comp_sql).
APPEND ls_comp_sql-name TO lt_fields.
APPEND '?' TO lt_values.
ENDLOOP.
DATA(lv_fields_str) = concat_lines_of( table = lt_fields sep = ',' ).
DATA(lv_vals_str) = concat_lines_of( table = lt_values sep = ',' ).
DATA(lv_insert_sql) = |INSERT INTO { lv_target_tbname } ( { lv_fields_str } ) VALUES ( { lv_vals_str } )|.
" 5. Execute ADBC batch insertion
TRY.
" Ensure connection exists
IF go_db IS INITIAL.
go_db = cl_sql_connection=>get_connection( 'YOUR DBCO CONF' ).
ENDIF.
DATA(lo_stmt_insert) = go_db->create_statement( tab_name_for_trace = lv_target_tbname ).
" Core: Directly bind the entire internal table
lo_stmt_insert->set_param_table( lr_db_data ).
" Execute batch write
DATA(lv_inserted_cnt) = lo_stmt_insert->execute_update( lv_insert_sql ).
go_db->commit( ).
WRITE: / 'Successfully wrote to target database, count:', lv_inserted_cnt.
CATCH cx_sql_exception INTO DATA(lx_sql_ins).
IF go_db IS NOT INITIAL.
TRY.
go_db->rollback( ).
CATCH cx_sql_exception.
ENDTRY.
ENDIF.
MESSAGE lx_sql_ins->sql_message TYPE 'E'.
CATCH cx_parameter_invalid INTO DATA(lx_param_ins).
MESSAGE lx_param_ins->get_text( ) TYPE 'E'.
ENDTRY.
ENDIF.
ENDIF.
ENDIF.
ENDFORM.
*&---------------------------------------------------------------------*
*& Form frm_fore_select_config
*& Description: In foreground mode, first pop up the Zdemoat1 configuration table
*& for the user to manually select the programs to execute
*&---------------------------------------------------------------------*
FORM frm_fore_select_config.
DATA: lt_config TYPE TABLE OF Zdemoat1,
lo_alv TYPE REF TO cl_salv_table.
" 1. Get all unfrozen configurations
SELECT * FROM Zdemoat1
INTO TABLE @lt_config
WHERE zblock_push = ''.
IF sy-subrc <> 0.
MESSAGE 'No executable configuration records found!' TYPE 'S' DISPLAY LIKE 'E'.
RETURN.
ENDIF.
" 2. Display configuration table in pop-up
TRY.
cl_salv_table=>factory(
IMPORTING
r_salv_table = lo_alv
CHANGING
t_table = lt_config ).
" Enable multi-selection and all toolbars
lo_alv->get_selections( )->set_selection_mode( if_salv_c_selection_mode=>multiple ).
lo_alv->get_functions( )->set_all( abap_true ).
" Set to pop-up mode (size can be adjusted according to actual columns)
lo_alv->set_screen_popup( start_column = 10 end_column = 120 start_line = 5 end_line = 20 ).
WRITE: / '>> Waiting for user to select the configuration to execute in the main console...'.
lo_alv->display( ).
" 3. Get rows selected by the user
DATA(lt_sel_rows) = lo_alv->get_selections( )->get_selected_rows( ).
IF lt_sel_rows IS INITIAL.
WRITE: / '>> User did not select any configuration, operation canceled.'.
RETURN.
ENDIF.
" 4. Write selected data to global table gt_Zdemoat1 (core driver table of the golden version)
CLEAR gt_Zdemoat1.
LOOP AT lt_sel_rows INTO DATA(lv_row_idx).
READ TABLE lt_config INTO DATA(ls_config) INDEX lv_row_idx.
IF sy-subrc = 0.
APPEND ls_config TO gt_Zdemoat1.
ENDIF.
ENDLOOP.
WRITE: / '>> User selected ', lines( gt_Zdemoat1 ), ' configurations, starting execution...'.
" 5. Seamlessly connect to the main logic of the original golden version
PERFORM frm_getdata_push.
CATCH cx_salv_msg.
WRITE: / 'Configuration table pop-up generation failed'.
ENDTRY.
ENDFORM.
gemini评价:
这是一段**非常成熟、硬核且具备较高实用价值**的 ABAP 代码。它不仅仅是一个简单的报表,更像是一个小型的"数据集成中间件"。
从代码的逻辑和使用的技术栈来看,作者对 ABAP 的底层机制(特别是动态编程和跨系统通信)有非常深刻的理解。以下是对这段代码的详细 Review,包含值得称赞的亮点以及一些可以进一步打磨的优化建议。
🌟 值得称赞的亮点(Strengths)
- **核心技术的巧妙结合:**
* **ALV 数据拦截:** 使用 `cl_salv_bs_runtime_info` 强制拦截后台 SUBMIT 的 ALV 数据(不展示直接获取引用),这是 ABAP 中非常经典的"黑科技",极大复用了现有标准或自建报表的逻辑,避免了重复造轮子。
* **RTTS(运行时类型服务):** 熟练运用 `cl_abap_tabledescr` 和 `cl_abap_structdescr` 进行动态结构重构。特别是在获取 ALV 数据后,还能根据配置表动态裁剪字段、拼接 UUID 和年月,这部分逻辑非常清晰。
* **ADBC 直连外部库:** 使用 `cl_sql_connection` 替代传统的 DBCO 函数或 PI/PO 接口,实现了高效的轻量级跨库 CRUD。
- **极强的防御性编程意识(安全底线):**
* **防止灾难性删库:** 在执行 `DELETE` 操作前,明确写了 `IF lv_target_tbname IS NOT INITIAL AND lv_where IS NOT INITIAL.`。这是一个极其重要的保底逻辑,防止因为 WHERE 条件为空导致外部数据库被全表清空。
* **异常回滚兜底:** 在捕获 `cx_sql_exception` 时,立刻执行了 `go_db->rollback( )`,并且将 rollback 本身也用 `TRY...CATCH` 包裹,防止二次 Dump。
* **UUID 容错处理:** 生成 UUID 时考虑到可能发生 `cx_uuid_error`,直接提供了 `sy-sysid && sy-datum && sy-uzeit` 的降级方案,保证程序不会因为基础类报错而中断。
- **用户体验与架构设计:**
* 前后台双模式切换(`p_back` / `p_fore`)设计得很人性化。前台模式利用 `cl_salv_table` 弹出配置表供用户勾选,不仅直观,而且直接将选中的数据回写到全局内表 `gt_Zdemoat1`,无缝衔接了原有的后台核心逻辑,解耦做得很好。
🛠️ 可以进一步优化的空间(Areas for Improvement)
尽管业务逻辑已经非常健壮,但从现代 ABAP 编程规范和极致性能的角度来看,还有以下优化空间:
1. 面向对象化 (OO ABAP) 的重构
代码目前使用的是传统的结构化编程(`FORM ... ENDFORM`)。由于涉及到大量的动态处理、数据库连接和异常捕获,将其重构为 **ABAP OO(面向对象)** 会更易于维护和扩展。
* **建议:** 可以将 `frm_getdata_push` 封装为主控类的一个方法,将 `frm_crud_targetdata` 封装为数据同步类的方法。这样可以避免大量传递 `TABLES` 参数,并且更容易利用接口(Interfaces)来实现多态(比如未来除了直连 DB,还要支持 OData 推送)。
2. 大数据量下的性能隐患(嵌套的动态分配)
在 `frm_crud_targetdata` 中,进行源数据到目标数据映射时使用了双重循环和动态分配:
```abap
LOOP AT pt_table ASSIGNING FIELD-SYMBOL(<ls_pt_row>).
LOOP AT lt_components INTO DATA(ls_comp_map).
ASSIGN COMPONENT ls_comp_map-name OF STRUCTURE <ls_pt_row> ...
```
* **风险点:** 如果 `pt_table` 有 10 万行数据,结构有 100 个字段,这里将执行 1000 万次 `ASSIGN COMPONENT`,性能损耗会非常明显。
* **建议:** 既然已经在前面用 RTTS 生成了完全匹配的目标结构 `<ls_db_row>`,如果是同名字段映射,可以直接使用 `MOVE-CORRESPONDING <ls_pt_row> TO <ls_db_row>`,ABAP 底层对 `MOVE-CORRESPONDING` 的优化远比手动循环 `ASSIGN COMPONENT` 要高效得多。
3. 硬编码 (Hardcoding) 问题
代码中多次出现了硬编码的外部数据库连接名:
```abap
go_db = cl_sql_connection=>get_connection( 'YOUR DBCO CONF' ).
```
* **建议:** 这违背了动态配置的初衷。建议将这个 DBCO 的连接名配置在 `Zdemoat1`(配置主表)中,作为变量传入,实现真正的"全动态"适配不同环境和库。
4. 后台作业的日志记录机制
当程序在后台(`p_back = 'X'`)运行时,代码主要通过 `WRITE` 输出日志:
* **风险点:** 这种做法只能在 SM37 的 Spool(假脱机)中查看,一旦出错,排查起来不够直观,且无法告警。
* **建议:** 考虑到这是一个核心的数据同步中间件,建议引入 **Application Log (SLG1)** 机制。使用 `cl_bal_*` 系列函数或封装好的日志类,将警告(如"未返回 ALV 数据")和报错(如 SQL 异常)写入 SLG1,方便运维监控。
5. 现代语法 (ABAP 7.40+) 的进一步应用
代码中已经用到了内联声明 `DATA(...)`,但在某些地方还可以更进一步。例如:
```abap
READ TABLE lt_config INTO DATA(ls_config) INDEX lv_row_idx.
IF sy-subrc = 0.
APPEND ls_config TO gt_Zdemoat1.
ENDIF.
```
可以简化为:
```abap
APPEND lt_config[ lv_row_idx ] TO gt_Zdemoat1. " 前提是用 TRY...CATCH cx_sy_itab_line_not_found 包裹
```
总结
这绝对是一段**"高阶开发者"**写出的代码。它解决了 SAP 中最痛点的问题之一:如何不写死代码地提取不同报表的数据并推送到外部。你的设计思路非常清晰,核心代码的鲁棒性极高。
如果你打算上线这套程序,重点只需关注一下**大量数据下的嵌套 ASSIGN 性能**,并将**数据库连接字符串参数化**,它将是一个堪称完美的通用接口中间件。