Application Development and Automation Discussions
Join the discussions or start your own on all things application development, including tools and APIs, programming models, and keeping your skills sharp.
cancel
Showing results for 
Search instead for 
Did you mean: 

Using constructor expression to populate multiple table-type fields based on the single source

Volodymyr_S
Explorer
1,198

Hi all,

I have a peculiar question. Can VALUE #( ) be used to populate multiple fields based on the same source? Before you point me to the documentation, let me clarify what I need. Basically, I want to have only one FOR loop which populates, at least, 2 tables: original records (for deletion) and modified records (for insertion). Using FOR is nice, but it doesn't allow, as it seems, use of a single work area to be assigned to multiple table components of the target structure...

types: begin of ty_db_updates,
         headers_old type some_db_header_table_type,
         headers_new type some_db_header_table_type,
       end of ty_db_updates.

*Here I have 2 tables: kind of driver table (or filter) that drives
*the selection from the HEADERS and the ITEMS tables. For simplicity sake,
*let's limit the example to only 2 tables. Let's call the tables DRIVER and HEADERS,
*both having the same single-field key KEY.
*What i am trying to achieve here is avoiding multiple LOOPS.
data(updates) = value ty_db_updates( for <driver> in driver
                                     for <header> in headers where ( key = <driver>-key )
                                     ( headers_old = value #( base headers_old value #( ( <header> ) ) )  "original header
                                       headers_new = value #( base headers_new value #( ( some transformation of the original <header> ) )  )
) ).

At this point, i'm getting error:

No component exists with the name "FOR". "FOR".

I know that I can use traditional LOOPs here, but a LOOP can't be passed as a parameter. LOOP is easy:

types: begin of ty_db_updates,
         headers_old type some_db_header_table_type,
         headers_new type some_db_header_table_type,
       end of ty_db_updates.
data(updates) = value ty_db_updates( ).
loop at driver assigning field-symbol(<driver>).
  loop at headers assigning field-symbol(<header>).
    "Insert original header into the OLD table
    insert <header> into udpates-headers_old.
    "Here goes some transformation of the header record
    "Now we insert modified header record into the NEW table
    insert <header> into the updates-headers_new.
  endloop.
endloop.

So, is something like the above at all possible using FOR? Or am I looking in vain? Maybe, horst.keller can clarify? I couldn't find any relevant examples - the terms FOR, LET, table comprehensions are too generic...

Thanks.

P.S. LOOP can be wrapped into a functional method though, so not a major show stopper. However, understanding the mechanics of nested tables assignment in constructor expressions will be beneficial. It would be nice to be able to replace that LOOP with a constructor expression...

1 ACCEPTED SOLUTION

Volodymyr_S
Explorer
0 Kudos
969

It is not feasible to process multiple tables (e.g., with parent-child relationships) and populate them in the fields of the same target structure using REDUCE. It's impossible to do so with VALUE either as the latter is not intended for such purpose. Nested LOOPs should be used wrapped in a method call.

12 REPLIES 12

Sandra_Rossi
Active Contributor
0 Kudos
969

You can't use VALUE ... ( FOR ... if the target is a structure.

That would be nice if you could provide a minimal reproducible example that we can try on our system, and we'll tell you why you have the error "No component exists with the name "FOR".".

Probably you must use REDUCE.

Volodymyr_S
Explorer
0 Kudos
969

Thanks sandra.rossi . This is a more-or-less correct representation of the logic that needs to be implemented. Please disregard potential functional issues with incomplete key access - this is not something i'm responsible for. However, the basic idea should be true for any case that deals with DB updates where one of the key fields must be changed - in this case, old records must be deleted and new records must be inserted.

report zztest1 message-id zfi_vendor_msg.

types: ty_pp_headers type standard table of reguh with empty key,
       ty_pp_items   type standard table of regup with empty key.
types: begin of ty_response,
         supplier_code  type reguh-lifnr,
         account_code   type reguh-zbnkl,
         account_number type reguh-zbnkn,
       end of ty_response,
       ty_responses type standard table of ty_response with empty key.
types: begin of ty_updates,
         headers_old type ty_pp_headers,
         headers_new type ty_pp_headers,
         items_old   type ty_pp_items,
         items_new   type ty_pp_items,
       end of ty_updates.

parameters: p_date   type reguh-laufd,
            p_id     type reguh-laufi,
            p_prponl type reguh-xvorl.


select * from reguh
where laufd = @p_date
and laufi = @p_id
and xvorl = @p_prponl
and vblnr is not initial
and zbnks = 'AU' "you can remove it or change to your country
into table @data(pp_headers).

select * from regup
for all entries in @pp_headers
where laufd = @pp_headers-laufd
and laufi = @pp_headers-laufi
and xvorl = @pp_headers-xvorl
and lifnr = @pp_headers-lifnr
into table @data(pp_items).

*Here data is prepared for sending to a vendor verification platform.
*For simplicity sake, let's make it look like none of the vendors was
*successfully verified.

*Generating a mock-up response from the vendor verification platform.
data(responses) = corresponding ty_responses( pp_headers mapping supplier_code = lifnr
                                                                 account_code = zbnkl
                                                                 account_number = zbnkn ).
*To be more life-like, this example needs some of the responses removed
*which means that one needs a multi-vendor payment proposal for testing
delete responses index 1.
check lines( responses ) > 0.

*LOOP version of what is needed
data updates_loop type ty_updates.
loop at responses assigning field-symbol(<resp>).
  loop at pp_headers assigning field-symbol(<header>)
  where lifnr = <resp>-supplier_code
  and zbnkl = <resp>-account_code
  and zbnkn = <resp>-account_number.
    "Old header - added unchanged for further deletion from the DB
    updates_loop-headers_old = value #( base updates_loop-headers_old ( <header> ) ).
    "New header should not rever to a valid document
    updates_loop-headers_new = value #( base updates_loop-headers_new
                                        ( value #( base <header> vblnr = value #( ) ) ) ).
    loop at pp_items assigning field-symbol(<item>)
    where lifnr = <header>-lifnr
    and vblnr = <header>-vblnr
    and zlspr is initial.
      "Old item - added unchanged for further deletion from the DB
      updates_loop-items_old = value #( base updates_loop-items_old ( <item> ) ).
      "New item should be blocked for payment
      updates_loop-items_new = value #( base updates_loop-items_new
                                        ( value #( base <item> vblnr = value #( )
                                                        zlspr = 'E' ) ) ).
    endloop.
  endloop.
endloop.

Can the same be achieved in the FOR version? Preparing this example I started seeing why VALUE (FOR) can't be used in this case... But can REDUCE be used in this case? That is, to populate 4 table fields of a structure?

Volodymyr_S
Explorer
0 Kudos
969

I managed to process headers with REDUCE, but I don't understand yet how to process items in the same REDUCE statement:

data(updates_reduce) = reduce ty_updates( init upd type ty_updates
                                          for <resp1> in responses
                                          for <header1> in pp_headers
                                          where ( lifnr = <resp1>-supplier_code
                                                  and zbnkl = <resp1>-account_code
                                                  and zbnkn = <resp1>-account_number )
                                          next upd-headers_old = value #( base upd-headers_old ( <header1> ) )
                                               upd-headers_new = value #( base upd-headers_new
                                                                          ( value #( base <header1> vblnr = value #( ) ) ) ) ).
Complexity here lies in the header-item relationships, that is, I need a kind of another FOR loop after the NEXT clause...

Sandra_Rossi
Active Contributor
0 Kudos
969

I think you are over-engineering here. It's good to go to constructor expressions only when it simplifies the reading. In your case, you could keep your existing code with LOOP AT, but just refactor/extract the method.

data(updates_reduce) = extract( responses = responses headers = headers ).

That makes it as clear as a constructor expression.

Of course, you could still use REDUCE, but it's much less legible (and we should also optimize because there are 2 loops for items that we should merge into 1 loop):

data(updates_reduce) = reduce ty_updates( init upd type ty_updates
                                          for <resp1> in responses
                                          for <header1> in pp_headers
                                          where ( lifnr = <resp1>-supplier_code
                                              and zbnkl = <resp1>-account_code
                                              and zbnkn = <resp1>-account_number )
                                          next upd-headers_old = value #( base upd-headers_old ( <header1> ) )
                                               upd-headers_new = value #( base upd-headers_new
                                                                          ( value #( base <header1> vblnr = value #( ) ) ) )
                                               upd-items_old = VALUE #(
                                                        BASE upd-items_old
                                                        FOR <item1> IN pp_items
                                                        WHERE ( lifnr = <header>-lifnr
                                                            AND vblnr = <header>-vblnr
                                                            AND zlspr IS INITIAL )
                                                        ( <item1> ) )
                                               upd-items_new = VALUE #(
                                                        BASE upd-items_new
                                                        FOR <item1> IN pp_items
                                                        WHERE ( lifnr = <header>-lifnr
                                                            AND vblnr = <header>-vblnr
                                                            AND zlspr IS INITIAL )
                                                        ( VALUE #( BASE <item1> vblnr = VALUE #( )
                                                                        zlspr = 'E' ) ) ) ).

Volodymyr_S
Explorer
0 Kudos
969

I agree. While it may work with REDUCE for 1 table for which we are generating DB entries (e.g., for INSERT, UPDATE and DELETE as is usually done in SAP FMs), we are hitting an dramatical increase in complexity as soon as we introduce a dependent table. Even worse, we may have several top-level tables, that is, tables that have 1:1 relationships with the header, and their corresponding dependent tables up to a few levels deep... It becomes a nightmare. Not impossible, but not worth it either.

Well, thank you for answering my question. There still are limitations in ABAP... (:

Volodymyr_S
Explorer
0 Kudos
970

It is not feasible to process multiple tables (e.g., with parent-child relationships) and populate them in the fields of the same target structure using REDUCE. It's impossible to do so with VALUE either as the latter is not intended for such purpose. Nested LOOPs should be used wrapped in a method call.

0 Kudos
969

Why do you say "not feasible"? It's feasible, but you risk the constructor expression to not be legible, and the goal of constructor expression is to bring legibility to the code.

969

"Feasible" also means "practical", that's why I distinguished between non-feasibility of REDUCE vs. impossibility of VALUE. Besides, I explicitly mentioned that complexity grows tremendously with introduction of nested/linked tables. That's why "not feasible" - that is, hard to implement, difficult to understand, nightmare to support and virtually impossible to extend...

Sandra_Rossi
Active Contributor
0 Kudos
969

I wouldn't say like that. People think they must use "new abap" (constructor expressions) to replace all "old abap", and they are completely wrong in the "must use new abap". Constructor expressions are useful for what they have been designed, and developers should learn and understand that.

This is a simplified expression (but still less legible than original LOOP AT code):

TYPES: BEGIN OF ty_updates,
         BEGIN OF headers,
           old TYPE ty_pp_headers,
           new TYPE ty_pp_headers,
         END OF headers,
         BEGIN OF items,
           old TYPE ty_pp_items,
           new TYPE ty_pp_items,
         END OF items,
       END OF ty_updates.

...

DATA(updates_reduce) = REDUCE ty_updates( INIT upd TYPE ty_updates FOR <resp1> IN responses FOR <header1> IN pp_headers WHERE ( lifnr = <resp1>-supplier_code AND zbnkl = <resp1>-account_code AND zbnkn = <resp1>-account_number ) NEXT upd-headers = VALUE #( old = VALUE #( BASE upd-headers-old ( <header1> ) ) new = VALUE #( BASE upd-headers-new ( VALUE #( BASE <header1> vblnr = VALUE #( ) ) ) ) ) upd-items = REDUCE #( INIT it TYPE ty_updates-items FOR <item1> IN pp_items WHERE ( lifnr = <header>-lifnr AND vblnr = <header>-vblnr AND zlspr IS INITIAL ) NEXT it-old = VALUE #( BASE it-new ( <item1> ) ) it-new = VALUE #( BASE it-new ( VALUE #( BASE <item1> vblnr = VALUE #( ) zlspr = 'E' ) ) ) ) ).

Volodymyr_S
Explorer
969

Well, I thought of such a solution, but that's not practical. Not that I have such a requirement, but what if a 3rd table must be introduced? I mean, something like subitems? Wrapper method with a single return parameter is far more convenient in this case. We can add new or remove old fields from the TY_UPDATES type, but it's not that easy with REDUCE. Besides, if we need to do some calculations in the nested table to update the parent table, this solution falls apart. (Actually, I do have to calculate something at the items level to update certain values at the header level as this was done in the original.)

As for the urge to use new syntax per se, this is not the case. I asked a question to better understand the constructor expressions, which I do now. I see that they don't support multiple layers of LET/FOR to evaluate helper variables first and then assign them to the target structure fields. I thought of constructor expressions as of a functional version of the LOOP statement, but they are not exactly that. This is the design constraint I have to keep in mind choosing between "good old LOOPs" and CEs.

By the way, new syntax should be used as it brings optimisations (; For example, LOOPS with secondary keys. That's what I learned working on the problem in this post.

Anyway, thanks a lot - I learned much during these 2 days.

Volodymyr_S
Explorer
969

Speaking of limitations... it would be great to limit the scope of work areas/field symbols to their respective constructor expressions... it's annoying when I can't declare, say, <header> in several consecutive CEs... 😞

Sandra_Rossi
Active Contributor
969
  1. I completely agree for a wrapper method in your case. But I can't agree with general assertions that you should not use constructor expressions starting from 3 levels, adding/removing fields, calculations to update the parent table (not sure what you're talking about), "don't support multiple layers of LET/FOR" (I would say that it's supported, I don't see what you mean), new syntax brings optimization (well, FILTER forces to use a hashed key, but that's all). Key understanding: IT DEPENDS. It depends on the exact context and requirement, and it depends on people/company how much they are expert in constructor expressions or not.
  2. "to limit the scope of work areas/field symbols to their respective constructor expressions..." : I think we all agree, and we understand that SAP did not re-think ABAP to introduce true "block-scoped" variables, but not so much a big deal when you write methods with few lines of code.