Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
matt
Active Contributor

Introduction

This blog has been inspired by bruno.esperanca and his thought provoking The last runtime buffer you'll ever need? The thing is, I wrote such a thing a few years ago, that's widely used by one of my clients. There's a few areas it could be improved

The Interface

interface zif_lookup

   public .

     constants c_dateto type fieldname value 'DATETO'. "#EC NOTEXT

   methods lookup

     exporting

       es_data type any

       eo_type type ref to cl_abap_structdescr

       e_notfound_flag type char1

     exceptions

       sql_error .


   methods set_key_val

     importing

       i_component type clike

       i_value type any .


   methods set_tim_val

     importing

       i_component type clike

       i_value type any .


   methods set_val_component

     importing

       i_component type clike .


   methods get_ref2_lookup

     returning

       value(rp_data) type ref to data

     exceptions

       sql_error .


   methods get_val_struc

     returning

       value(ro_valstruc) type ref to cl_abap_structdescr .


   methods get_notfound_flag

     returning

       value(r_notfound_flag) type flag .


endinterface.


Buffering in use


Well, the constructor is missing, so just showing you the interface doesn't really help! So, imagine a class with this attribute.

DATA: buffer TYPE REF TO zif_lookup.

Then we have a method in some class that reads materials from the MARC table, with i_matnr, i_werks, exporting e_bwtty and e_mmsta.

IF buffer IS NOT bound.

  CREATE OBJECT buffer TYPE zcl_table_lookup EXPORTING

     i_table_name = 'MARC'

     i_whole_tab = abap_false.

" The data I want to get back

  buffer->set_val_component( 'BWTTY' ).

  buffer->set_val_component( 'MMSTA' ).

ENDIF.

" The key data

buffer->set_key_val( i_component = 'MATNR' i_value = i_matnr )

buffer->set_key_val( i_component = 'WERKS' i_value = i_werks ).

" Now look it up.

DATA: BEGIN OF looked_up,

bwtty TYPE bwtty_d,

mmsta TYPE mmsta,

END OF looked up.

buffer->lookup( IMPORTING es_data = looked_up ).

e_bwtty = looked_up-bwtty.

e_mmsta = looed_up-mmsta.

What the class ZCL_TABLE_LOOKUP does is take the supplied components through set_val_component and set_key_val, and constructs two hashed tables (using RTTS) with the key components (in this case, MATNR and WERKS) as key. The first contains data looked up, the second contains data looked up and not found.

After the first lookup, you can't change the component fields.

If the i_whole_tab parameter is set, then the whole table will be buffered, rather than doing line by line buffering.

A couple of other implementations

I created a BW style look up, that uses the table look up above, and the same interface. It specialises into two further classes - one for looking up InfoObject master data, the other reading from a DSO.

Their constructors take an InfoObject name / DSO name, convert that to the underlying transparent tables, and instantiates a table lookup instance for this table. Oh - and for InfoObjects there's a flag on the constructor for whether the data is time dependent.

The component setting methods similarly take InfoObject names (instead of field names). The InfoObject names are then converted to field names, and passed to the table lookup instance.

A bit about internals

I'll concentrate on the TABLE buffer implementation, as it is that which is the engine.

SET_VAL_COMPONENT SET_KEY_VAL

There are two HASHED internal table, both with the same structure of fieldname, value and fieldtype (keyed on fieldname). The field type is simply gained via DD03L, using the table and fieldname.

The val_component (the fields which define which values will be returned) doesn't populate the value field. Values for the key fields are held as strings.

SET_TIM_VAL

This method is used for time dependent data.

LOOKUP

No buffer tables are created until the first time this method runs. Using RTTS, I build a few table types and references to these a few tables based on those types. The types of these variables, I'll leave as a exercise for the readers - they're not difficult to work out

* Add key fields

  LOOP AT me->th_key_fields ASSIGNING <ls_field>.

    ls_component-name = <ls_field>-fieldname.

    ls_component-type ?= cl_abap_tabledescr=>describe_by_name( <ls_field>-fieldtype ).

    APPEND ls_component TO lt_tab_component.

    APPEND ls_component TO lt_ntim_component.

    APPEND <ls_field>-fieldname TO lt_key.

  ENDLOOP.

* Time dependent fields

  IF me->s_tim_field IS NOT INITIAL.

    ls_component-name = me->s_tim_field-fieldname.

    ls_component-type ?= cl_abap_tabledescr=>describe_by_name( me->s_tim_field-fieldtype ).

    APPEND ls_component TO lt_tab_component.

    APPEND me->s_tim_field-fieldname TO lt_key.

    ls_component-name = c_index_fld.

    ls_component-type ?= cl_abap_tabledescr=>describe_by_data( sy-tabix ).

    APPEND ls_component TO lt_ntim_component.

  ENDIF.

* Add value fields for buffer

  LOOP AT me->th_val_fields ASSIGNING <ls_field>.

    ls_component-name = <ls_field>-fieldname.

    ls_component-type ?= cl_abap_tabledescr=>describe_by_name( <ls_field>-fieldtype ).

   READ TABLE lt_tab_component TRANSPORTING NO FIELDS WITH KEY name = ls_component-name.

    IF sy-subrc IS NOT INITIAL.

      APPEND ls_component TO lt_tab_component.

    ENDIF.

    APPEND ls_component TO lt_val_component.

  ENDLOOP.

* Create table structure and types

  lo_tablestruc = cl_abap_structdescr=>create( lt_tab_component ).

  lo_hash_table_type =

           cl_abap_tabledescr=>create( p_line_type  = lo_tablestruc

                                       p_table_kind = cl_abap_tabledescr=>tablekind_hashed

                                       p_unique     = abap_true

                                       p_key        = lt_key ).

  lo_sort_table_type =

           cl_abap_tabledescr=>create( p_line_type  = lo_tablestruc

                                       p_table_kind = cl_abap_tabledescr=>tablekind_sorted

                                       p_unique     = abap_true

                                       p_key        = lt_key ).

* Create "time key" structure and table

  IF me->s_tim_field IS NOT INITIAL.

    DELETE lt_key WHERE name EQ me->s_tim_field-fieldname.

    lo_ntim_struc = cl_abap_structdescr=>create( lt_ntim_component ).

    lo_ntim_table_type =

             cl_abap_tabledescr=>create( p_line_type = lo_ntim_struc

                                         p_table_kind = cl_abap_tabledescr=>tablekind_hashed

                                         p_unique     = abap_true

                                         p_key        = lt_key ).

    CREATE DATA me->ps_ntim_lookup  TYPE HANDLE lo_ntim_struc.

    CREATE DATA me->pth_ntim_lookup TYPE HANDLE lo_ntim_table_type.

ENDIF.

* Create value structure types

  me->o_valstruc  = cl_abap_structdescr=>create( lt_val_component ).

* Create pointers to the lookup table, workarea and values

  CREATE DATA me->pth_lookup    TYPE HANDLE lo_hash_table_type.   " The main buffer

CREATE DATA me->ps_lookup     TYPE HANDLE lo_tablestruc.             " The key fields in a structure

CREATE DATA me->ps_val        TYPE HANDLE me->o_valstruc.            " The values being read, in a structure

  CREATE DATA me->pth_lookup_nf TYPE HANDLE lo_hash_table_type.  " The buffered "not found" values

Then we need to read the data. For a whole table buffer, it's a simple piece of dynamic SQL. Note how pool tables are handled. It doesn't actually matter if we get duplicates selected; it's just less efficient.

     TRY.

          SELECT DISTINCT (me->t_select) FROM (me->table) INTO TABLE <lt_lookup>.

        CATCH cx_sy_dynamic_osql_error cx_sy_open_sql_db INTO lx_error.

         " Handle the fact that you can't use DISTINCT with pool tables

          TRY.

              SELECT (me->t_select) FROM (me->table) INTO TABLE <lt_lookup>.

            CATCH cx_sy_dynamic_osql_error cx_sy_open_sql_db INTO lx_error.

              RAISE sql_error. "<---- this really should be a proper class based exception. I'm sorry...

          ENDTRY.

      ENDTRY.

Now, we can actually get data. <ls_lookup> is constructed from ps_lookup, and contains the key values.

* See if the value is already in the table

  READ TABLE <lth_lookup> INTO <ls_lookup> FROM <ls_lookup>.

If it's there, we're done. <ls_lookup> with contain the key fields and value fields. Just pass the value fields back to the calling program. If it isn't there we have to check the <lth_lookup_nf> table (ok, for whole table buffering we don't need to; we know it's not found if it isn't in <lth_lookup>. With line by line buffering, we have to do a single dynamic SQL read. Here, the time dependent reads may come into play.

lt_where = me->get_where_tab( ).

TRY.

   SELECT (me->t_select) FROM (me->table) INTO xs_lookup WHERE (lt_where) ORDER BY (me->orderby). " Orderby is only set for time dependence

     IF not time dependent OR date GT date in the key.

        EXIT.

      ENDIF.

   ENDSELECT.

CATCH cx_dy_dynamic_osql_error cx_sy_open_sql_db.

   raise SQL_ERROR.

ENDTRY.

If the data isn't found, then it's added to the not found table, otherwise it's added to the found table.

Simple, isn't it?

Improvements

  • With the table name, we can easily get the key fields from DD03L. We still need set_key_val, or we don't know the values to select against.
  • Put in proper exception handling. It's pretty dire. You can tell I built this before I understood exception classes well :oops: . But I do now, honest!
4 Comments