1 6 7 8 9 10 57 Previous Next

ABAP Development

846 Posts

Why & when to use a Builder Class

A Test Method is divided in the 4 Parts: Setup, Exercise, Verify and Teardown (Meszaros).

 

Often you see that a huge and complicated part is the Setup, where you replace Dependencies with Test-Doubles to test a Class in Isolation. If you have complex Dependencies it is very easy to pollute your Test-Class with hundreds of lines of setup Code. If you want to test different scenarios, with different Input e.g. from Stubs, it get's even more worst.

 

One way to get around it would be the use of a mocking Framework that generates Code. I prefer self-written Stubs, Fakes or Mocks for different reasons.


To make Test-Classes shorter and more expressive I use often of Helper-Classes that use the "Builder Pattern". They can be a local Class if it is used for a single Test, or a global Class if it can be reused in several other Test's.

 

In the simplest case you just call the build Method, that creates a bound Instance to satisfy another Method to call - here with a Customer.

lcl_customer_builder=>new(

     )->build( ).

In simple human readable Text you can add Method's that set data for the instance to build.

lcl_customer_builder=>new(

     )->with_name(    

          'Max Mustermann'

     )->with_customer_id(

          '1234'

     )->build( ).


Also it is very comfortable if Class to build has own Dependencies you have to satisfy. Inside the Builder you can simply set them to Default or Null Implementations.


Without the Builder Pattern the Code may have looked like this:

DATA: lo_customer             TYPE REF TO lif_customer,    

           lo_fake_company     TYPE REF TO lif_company.

lo_customer =
     zcl_customer_factory=>get_instance(

          )->get_customer( '1234' ).

lo_customer->set_name( 'Max Mustermann' ).

 

lo_customer->set_company(

          zcl_company_factory=>get_company_null( )

     ).

 

An Example - Build a complex Fake Class

 

Test Class under Test and it's dependency


In our Example the Class under Test ZCL_EXAMPLE_CLASS_UNDER_TEST uses the Interface ZIF_DATE_OBJ_LOOKUP and it's Implementation ZCL_DB_DATE_OBJ_LOOKUP. For demonstration purposes it takes a Date and returns a generic Object Reference.

To test the Class ZCL_EXAMPLE_CLASS_UNDER_TEST in isolation we have to replace the use of ZCL_DB_DATE_OBJ_LOOKUP - that would access the Database.

 

Definition of ZIF_DATE_OBJ_LOOKUP

INTERFACE zif_date_obj_lookup
   PUBLIC .

   METHODS get_obj_for_date
     IMPORTING
       !i_date TYPE dats
     RETURNING
       value(r_obj) TYPE REF TO object .

ENDINTERFACE.

 

Our Class under Test ZCL_EXAMPLE_CLASS_UNDER_TEST

CLASS zcl_example_class_under_test DEFINITION
  
PUBLIC
   FINAL
  
CREATE PUBLIC .

  
PUBLIC SECTION.

    
METHODS:

          constructor,

          do_stuff

               RETURNING value(r_value) TYPE i.

   PRIVATE SECTION.

     DATA mo_date_obj_lookup TYPE REF TO zif_date_obj_lookup .

ENDCLASS.                    "ZCL_EXAMPLE_CLASS_UNDER_TEST DEFINITION

CLASS zcl_example_class_under_test IMPLEMENTATION.

  METHOD constructor.
    
CREATE OBJECT me->mo_date_obj_lookup
      
TYPE zcl_db_date_obj_lookup.
  
ENDMETHOD.                   

   METHOD do_stuff.
     " Uses the dependency ZIF_DATE_OBJ_LOOKUP
     me->mo_date_obj_lookup->get_obj_for_date( ... ).

  
ENDMETHOD.

ENDCLASS.                

 

The Fake Date Object Lookup Class

 

The Class lcl_fake_date_obj_lookup is the fake that will replace the Dependency for the class ZCL_DB_DATE_OBJ_LOOKUP. Instead the Original that use the Database, the Fake uses a Dictionary Class that returns an Object per Date.

CLASS lcl_fake_date_obj_lookup DEFINITION FINAL.

  
PUBLIC SECTION.

    
INTERFACES:
       zif_date_obj_lookup
.

    
METHODS:
       constructor
        
IMPORTING
           io_dictionary
TYPE REF TO zif_dictionary.

  
PRIVATE SECTION.

    
DATA:
       mo_fake_data_dictionary
TYPE REF TO zif_dictionary.

ENDCLASS.                   

CLASS lcl_fake_date_obj_lookup IMPLEMENTATION.

  
METHOD constructor.
     me
->mo_fake_data_dictionary = io_dictionary.
  
ENDMETHOD.                   

  
METHOD zif_date_obj_lookup~get_obj_for_date.
     r_obj
=
       me
->mo_fake_data_dictionary->get( |{ i_date }| ).
  
ENDMETHOD.                   

ENDCLASS.                   


The Fake Date Object Lookup - Builder


The Fake is build with the helper Class lcl_fake_date_lookup_bldr. This class contains logic to simplify the creation of Fakes with different Scenarios.  

CLASS lcl_fake_date_obj_lookup_bldr DEFINITION FINAL.

  
PUBLIC SECTION.

    
CLASS-METHODS:
      
new
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr.

    
METHODS:
       constructor
,

       start_at_date
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       ends_at_date
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       returns_in_default
        
IMPORTING
           io_object
TYPE REF TO object
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       returns_at_date
        
IMPORTING
           i_date   
TYPE        dats
           io_object
TYPE REF TO object
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       build
         RETURNING
value(ro_fake_date_obj_bldr) TYPE REF TO zif_date_obj_lookup.

  
PRIVATE SECTION.

    
DATA:
           m_date_start                
TYPE dats,
           m_date_end                  
TYPE dats,
           mo_defalt_obj               
TYPE REF TO object,
           mo_dictionary_for_fake 
TYPE REF TO zif_dictionary.

    
METHODS:
       get_value_for_date
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_obj) TYPE REF TO object,
       set_value_for_date
        
IMPORTING
           i_date   
TYPE        dats
           io_object
TYPE REF TO object
         RETURNING
value(ro_obj) TYPE REF TO object,
       is_value_for_date_set
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(r_is_set) TYPE abap_bool,
       get_dict_with_default_ret_obj
         RETURNING
value(ro_fake_dictionary) TYPE REF TO zif_dictionary.

ENDCLASS.                   

CLASS lcl_fake_date_obj_lookup_bldr IMPLEMENTATION.

  
METHOD new.
    
CREATE OBJECT ro_self.
  
ENDMETHOD.                   

  
METHOD constructor.
    
CREATE OBJECT me->mo_dictionary_for_fake
      
TYPE zcl_dictionary.
  
ENDMETHOD.                   

  
METHOD start_at_date.
     me
->m_date_start = i_date.
     ro_self
= me.
  
ENDMETHOD.                   

  
METHOD ends_at_date.
     me
->m_date_end = i_date.
     ro_self
= me.
  
ENDMETHOD.                   

  
METHOD returns_in_default.
     me
->mo_defalt_obj = io_object.
     ro_self
= me.
  
ENDMETHOD.                   

  
METHOD returns_at_date.
     me
->set_value_for_date(
         i_date   
= i_date
         io_object
= io_object
      
).
     ro_self
= me.
  
ENDMETHOD.                  

  
METHOD build.
    
ASSERT me->mo_defalt_obj  IS BOUND.
    
ASSERT me->m_date_end > me->m_date_start.

    
CREATE OBJECT ro_fake_date_obj_bldr
      
TYPE lcl_fake_date_obj_lookup
      
EXPORTING
         io_dictionary
= me->get_dict_with_default_ret_obj( ).
  
ENDMETHOD.                   

  
METHOD get_value_for_date.
     ro_obj
=
       me
->mo_dictionary_for_fake->get( |{ i_date }| ).
  
ENDMETHOD.                   

  
METHOD set_value_for_date.
     me
->mo_dictionary_for_fake->add(
        i_key  
= |{ i_date }|
        i_value
= io_object
     
).
  
ENDMETHOD.                   

  
METHOD is_value_for_date_set.
     r_is_set
=
       me
->mo_dictionary_for_fake->exists( |{ i_date }| ).
  
ENDMETHOD.                   

  
METHOD get_dict_with_default_ret_obj.
    
DATA: lv_current_date TYPE dats.

     lv_current_date
= me->m_date_start.

    
WHILE lv_current_date <= me->m_date_end.

      
IF me->is_value_for_date_set( lv_current_date ) = abap_false.
         me
->set_value_for_date(
             i_date   
= me->m_date_start
             io_object
= me->mo_defalt_obj
          
).
      
ENDIF.

       lv_current_date
= lv_current_date + 1.
    
ENDWHILE.

     ro_fake_dictionary
= me->mo_dictionary_for_fake.
  
ENDMETHOD.                   

ENDCLASS.                   



The Test Test-Class

CLASS ltcl_example_class_under_test DEFINITION
    
FOR TESTING
     DURATION SHORT
     RISK LEVEL HARMLESS
     FINAL
.

  
PRIVATE SECTION.

    
METHODS:
       get_test_instance
        
IMPORTING
           io_date_obj_lookup
TYPE REF TO zif_date_obj_lookup
         RETURNING
value(ro_fcut) TYPE REF TO zcl_example_class_under_test,

       test_3_days_with_one_problem
FOR TESTING,
       test_10_days_with_2_problems
FOR TESTING.

     "... more complex tests

ENDCLASS.                   

I decided to inject the Dependency by overwriting the Member Attribute after Construction. This way allows you to replace also local dependencies e.g. if you use a local Interface LIF_DB to extract SQL Commands to a local Class. And it is easy to create an Instance of the Class. To get access to the private Member Attribute I made the Test-Class to a Friend of the Class under Test.
But this has the Disadvantage that the Test knows internals of the Class Implementation and that the Constructor cannot contain Logic that uses the to replaced Dependency. A Clean way is to add the Dependency to the Constructor Parameters.

CLASS zcl_example_class_under_test DEFINITION LOCAL FRIENDS
            ltcl_example_class_under_test
.

The Test Implementation is reduced into the simple creation of the Instance - and the Assertions. Every Assert Statement contains the Builder Class - which tells the reader in simple readable Text the preconditions. I always try to reduce local Variables to a minimum to keep Methods short & clean.

CLASS ltcl_example_class_under_test IMPLEMENTATION.

  
METHOD get_test_instance.

    
CREATE OBJECT ro_fcut.
     ro_fcut
->mo_date_obj_lookup = io_date_obj_lookup.

  
ENDMETHOD.                   

  
METHOD test_3_days_with_one_problem.

     cl_aunit_assert
=>assert_equals(
        
exp = 3
         act
= me->get_test_instance(
                   lcl_fake_date_obj_lookup_bldr
=>new(
                    
)->start_at_date(
                      
'20140101'
                    
)->ends_at_date(
                      
'20140103'
                    
)->returns_in_default(
                       zcl_dummy2
=>new( )
                    
)->returns_at_date(
                       i_date   
= '20140102'
                       io_object
= zcl_dummy1=>new( )
                    
)->build( )
                
)->do_stuff( )
      
).

  
ENDMETHOD.                   

  
METHOD test_10_days_with_2_problems.

     cl_aunit_assert
=>assert_equals(
      
exp = 5
       act
= me->get_test_instance(
                   lcl_fake_date_obj_lookup_bldr
=>new(
                    
)->start_at_date(
                      
'20140201'
                    
)->ends_at_date(
                      
'20140210'
                    
)->returns_in_default(
                       zcl_dummy2
=>new( )
                    
)->returns_at_date(
                       i_date   
= '20140202'
                       io_object
= zcl_dummy1=>new( )
                    
)->returns_at_date(
                       i_date   
= '20140209'
                       io_object
= zcl_dummy1=>new( )
                    
)->build( )
                
)->do_stuff( )
    
).

  
ENDMETHOD.                   

ENDCLASS.                   

 

Conclusion

 

The Builder Pattern is another Tool in my Toolkit for Test-Driven Development. Complex Dependencies are often a signal that there's something wrong with your Class Design, but you often have to test Classes that must be tested with varying values - and a builder can help you with that.

The Builder Class in the Example above contains quite a bit of logic - and untested logic as dependency for your class under Test may result in brittle Test's. So you may have to test that logic as precondition before the actual Test's. If you use a complex global Builder Class, it would contain own Test's in it's local Test-Class Include.


This will be my shortest blog ever (but c'mon its only 10 points worth anyway).

 

You can make the implementation of interface methods optional now.

 

METHODS meth DEFAULT IGNORE|FAIL ...

 

All you need to know is documented, what should I write more  .

Recap

 

For code push down, meaning pushing analytics from the ABAP application server to the database server, you can use open ABAP features that run on any database. Those are Open SQL that was enhanced widely in ABAP 7.40 with analytic capabilities like SQL expressions or by removing restrictions from joins and the ABAP Core Data Services (ABAP CDS) for advanced view building in the ABAP Dictionary.

 

If the functionality offered by the open features is not sufficicient to cover your needs, you have to switch to Native SQL (sounds familiar?). This is especially true for SAP HANA, that offers analytical features not covered in standard SQL, as e.g. calculation views. If your ABAP programs should run on HANA  only (or if you want to offer special implementations for HANA and alternative implementations for other databases), database procedures written in SQLScript offer a convenient access to the capabilites of HANA. In order to facilitate this Native access to HANA, in 7.40, SP05 the concept of ABAP Managed Database Procedures (AMDP) was introduced, that allows you to code SQLScript in special methods of normal ABAP classes and the runtime environment does the rest for you. (But please note that a single SQL statement does not become faster if you code it in an AMDP, how should it? As long as you can stay open, stay open. There is no drawback in performance if you can express the logics in single statements or in CDS views. You use AMDP to avoid unnecessary data transfers between database and application server when using several statements or when you need access to fuctionality not covered by SQL).

 

 

News for ABAP 7.40, SP08

 

 

With ABAP 7.40, SP08, AMDP was enhanced as follows:

 

Tabular Changing Parameters

 

Although Native SQLScript procedures do not offer INOUT parameters, you can define CHANGING parameters for AMDP methods now. The trick is, that the database procedure generated from AMDP gets an additional IN parameter of the same name as the chaniging parameter with the pstfix __IN__ while the CHANGING parameter becomes an OUT parameter. When calling the procedure, the OUT parameter is filled implicitly with the CHANGING value passed to the IN parameter. Normally, you don't have to care about the implicit IN parameter. Only if you want to call such a procedure from another database procedure, you have to supply it.

 

AMDP method definition

 

METHODS

  get_carriers

     CHANGING

       VALUE(carriers) TYPE t_carriers

     RAISING  cx_amdp_error.

 

AMDP method implementation

 

 

  METHOD get_carriers BY DATABASE PROCEDURE FOR HDB

                         LANGUAGE SQLSCRIPT

                      USING scarr.

    carriers  = select s.*

                     from scarr as s

                     inner join :carriers as c

                       on s.mandt  = c.mandt and

                          s.carrid = c.carrid;

  ENDMETHOD.

 

A good example where AMDP is not needed at all, but KISS here.


Call from ABAP

 

TRY.   

    NEW cl_demo_amdp_changing(

      )->get_carriers( CHANGING carriers = carriers ).

  CATCH cx_amdp_error INTO DATA(amdp_error).

    ...

ENDTRY.

 

Class Based Exceptions

 

As shown above, you can handle exceptions that occur during processing an AMDP now. Before you got runtime errors only.

 

AMDP BAdIs

 

Special AMDP BAdIs allow you to implement BAdI methods as AMDP methods. These transfer the effect of the switches from Switch Framework to the implementation of database procedures in the current database. When an AMDP procedure calls another AMDP procedure managed by an AMDP BAdI, the implementation is executed that is prescribed by the current switch setting. (for calling AMDP procedures from ABAP no special BAdIs are necessary, because ABAP calls of normal BAdI methods are goverened by the switch framework anyway).

Recap

 

With ABAP 7.40, SP05 the ABAP Core Data Services, short ABAP CDS,  were introduced. The ABAP Core Data Services implement the general CDS concept of SAP (that is also available for HANA as HANA CDS) for AS ABAP. They use a DDL to define CDS views that implement a semantic data model in the ABAP Dictionary.

 

The power of ABAP CDS is, that it is open, meaning database independent (with some exceptions if the databases do not yet cover all the functionalities as e.g. views with parameters). You can use the DDL of ABAP CDS in Eclipse based ADT to create rather complex views that exceed the capabilities of the classical database views created in SE11 by far. Technically, from the source code of a CDS view, a classical database view is generated. We call this the CDS database view. You can display the fields of this view in SE11 but not change them. Especially, you can see the real database object - the SQL view - that is created from the view definition on the database in SE11. But this CDS database view serves mainly the internal technical purpose to realize the view in the dictionary. You can, but you should not use it in ABAP programs. Instead, you work with the CDS entitiy, whose name is defined behind DEFINE VIEW. Only the entity carries the full capabilities of the CDS view, like semantical information, client handling, connection to authority checks (planned), and so on. You can use the CDS entity behind TYPE for declaring work areas and in Open SQL in ABAP programs.

 

You use these views for the famous code push down in order to put more analytic work to the database. If the functionality of a view is only needed once, of course you can still use Joins, SQL expressions, subqueries, ... in Open SQL for this code push down. But if you want to reuse a view, need semantical or technical capabilities of CDS that exceed those of Open SQL (but we try to keep the technical capabilities on the same level, e.g., CDS knows UNION, Open SQL will know UNION with SP12) or you just want to push down the full data model to the database, you use CDS. And in fact I've seen very, very extensive sophisticated views already now (oh boy ...).

 

But I'm a simple mind, not a business modelling guru. In order to understand the basics, I work with simple examples. Just to give you two of those:

 

@AbapCatalog.sqlViewName: 'DEMO_CDS_JOIN'

define view demo_cds_scarr_spfli

  (id, carrier, flight, departure, destination)

  as select from spfli

            join scarr on scarr.carrid = spfli.carrid

     { key spfli.carrid,

       key scarr.carrname,

       key spfli.connid,

       spfli.cityfrom, 

       spfli.cityto }

 

This is a simple join as you could also define it in classical SE11. The name of the CDS entity is demo_cds_scarr_spfli and the name of the generated CDS database view is DEMO_CDS_JOIN. One difference to a classical view is the fact, that a CDS entity that uses client dependent data sources is client dependent by default but never has a client field. The client handling is done implicitly without shining through here.

 

Another view, that does exactly the same as the above one but shows a new kind of modelling capability:


@AbapCatalog.sqlViewName: 'DEMO_CDS_ASSOC'

define view demo_cds_association

  (id, carrier, flight, departure, destination )

  as select from spfli

            association [1..1] to scarr as spfli_scarr

              on $projection.carrid = spfli_scarr.carrid

     { key spfli.carrid,

       key spfli_scarr.carrname,

       key spfli.connid,

       spfli.cityfrom, 

       spfli.cityto }

 

The join is replaced by an association, which is a more elegant way to code reusable joins. Associations can be adressed inside the view using path expressions (spfli_scarr.carrname is the most simple one) and can be published (not shown here) for usage in other views and in Open SQL (planned for SP12).

 

In an ABAP program you can read the views with Open SQL:

 

SELECT *
       FROM demo_cds_association
       ORDER BY id, carrier, flight
       INTO TABLE @DATA(result1).

SELECT *
       FROM demo_cds_scarr_spfli
       ORDER BY id, carrier, flight
       INTO TABLE @DATA(result2).

ASSERT result1 = result2.

 

To find out more, see the CDS documentation (it contains an authorization concept already, but the release of that was postponed to SP12).

 

After this recap, now the most important

 

News for CDS with 7.40, SP08

 

Besides many smaller enhancements, the most important news are:

 

Many new built-in functions

 

Besides the function already introduced with SP05, you can use standard functions as COALESCE, CONCAT, REPLACE, ABS, DIV, DIVISION, FLOOR, MOD and ROUND now and as in Open SQL the searched CASE was introduced.

 

Special functions  CURRENCY_CONVERSION, UNIT_CONVERSION und DECIMAL_SHIFT allow you to deal with data of the (in)famous SAP specific dictionary types CURR, CUKY, QUAN and UNIT.

 

 

@AbapCatalog.sqlViewName: 'DEMO_CDS_CRRCNV'

  define view demo_cds_currency_conversion

   with parameters to_currency:abap.cuky(5),

                   exc_date:abap.dats

   as select from demo_prices

   { id,

     currency_conversion( amount => amount,

                          source_currency => currency,

                          round => 'X',

                          target_currency => :to_currency,

                          exchange_rate_date => :exc_date,

                          error_handling => 'SET_TO_NULL' ) as amount,

     :to_currency as currency }

 

 

Parameter views

 

On databases that support it (not all up to now, but SAP HANA does) you can add importing parameters to a view that can be used in the SELECT statement of the view and that can be supplied when using the view in Open SQL.

 

Example for such a view:

 

@AbapCatalog.sqlViewName: 'DEMO_CDS_PARA'

define view demo_cds_parameters

  with parameters p_distance_l:S_DISTANCE,

                  p_distance_o:S_DISTANCE,

                  p_unit:S_DISTID

  as select from spfli         

            { key carrid,

              key connid,

                  cityfrom,

                  cityto,

                  distance,

                  distid }

            where distid = :p_unit and

                           distance between :p_distance_l

                                        and :p_distance_o;

 

Usage in Open SQL:

 

IF cl_abap_dbfeatures=>use_features(

     EXPORTING

        requested_features =

          VALUE #( ( cl_abap_dbfeatures=>views_with_parameters ) ) ).

  SELECT *

         FROM demo_cds_parameters( p_distance_l = @from_distance,

                                   p_distance_o = @to_distance,

                                   p_unit       = @unit )

         ORDER BY carrid, connid

         INTO TABLE @DATA(result).

      cl_demo_output=>display( result ).

ELSE.

  cl_demo_output=>display(

    'Database system does not support views with parameters' ).

ENDIF.

 

 

Extending views

 

Classical views delivered by SAP or partners can be extended with the well known clasical append views. In order to achieve the same for CDS views a new DDL statement is introduced:

 

@AbapCatalog.sqlViewAppendName: 'DEMO_CDS_EXTENS'

extend view demo_cds_original_view with demo_cds_view_extension

  { spfli.distance,

    spfli.distid as unit };

 

It adds new fields to the existing view fields. Technically, a classical append view - its name defined behind the annotation @AbapCatalog.sqlViewAppendName - is generated in the ABAP Dictionary for that purpose.

 

 

 

 

 

 

 

 

 

 

 


The most important news for Open SQL in ABAP 7.40, SP08 are as follows:

 

Inline Declarations behind INTO


You might like this one. From 7.40, SP08 on you can place inline declarations with the declaration operator DATA( ... ) that was introduced with 7.40, SP02  behind INTO.

 

DATA id TYPE scarr-carrid.
cl_demo_input=>request( CHANGING field = id ).

SELECT SINGLE carrname AS name, carrid AS id
       FROM   scarr
       WHERE  carrid = @id
       INTO @DATA(result).

cl_demo_output=>display( result ).

 

Or for a table

 

SELECT carrname AS name, carrid AS id
       FROM   scarr
       INTO TABLE @DATA(result).


cl_demo_output=>display( result ).

 

Either an elementary data object, a structure, or an internal table is declared depending on the results set defined in the SELECT list. See the documentation for details of the type construction.

 

SQL Expressions


The SQL expressions introduced with 7.40, SP05 into the SELECT list were enhanced with 7.40, SP08 as follows:

 

  • You can use SQL expressions behind GROUP BY
  • You can use SQL expressions together with aggregates
  • You can use SQL expressions as argument of aggregates
  • You can use a seached CASE expression besides the simple CASE

 

Example for a searched case:

 

SELECT num1, num2,

       CASE WHEN col1 <  50 AND col2 <  50 THEN @both_l

            WHEN col1 >= 50 AND col2 >= 50 THEN @both_gt

            ELSE @others

       END AS group

       FROM demo_expressions

       ORDER BY group

       INTO TABLE @DATA(results).

 

Column Specification


In the SELECT list, you can specify all columns of a data source using the syntax data_source~* from  7.40, SP08 on. This can be handy when working with joins.

 

SELECT scarr~carrname, spfli~*, scarr~url

       FROM scarr INNER JOIN spfli ON scarr~carrid = spfli~carrid

       INTO TABLE @DATA(result).

 

Position of INTO


Did you realize the position of INTO in the above examples? I positioned it behind the other clauses. This was not possible before. From 7.40, SP08 on, the INTO clause can and should (haha) be used behind the other clauses of a SELECT statement. The additions UP TO n ROWS, BYPASSING BUFFER, and CONNECTION that are not treated as clauses must then be placed behind the INTO clause.

 

The rationale behind this change is, that the INTO clause is not part of standard SQL but defines the data interface between SQL and ABAP. In order to enable future enhancements in the SQL part of Open SQL, e.g. UNION, the INTO clause has to be removed from the SQL part.

 

Removal of Restrictions and New Restrictions

 

Some restrictions have been removed. E.g. from 7.40, SP08 on you can place a minus sign in front of an operator of an arithmetic expression in the SELECT list, you can carry out a simple CASE for aggregates, you can use LIKE and IN in join conditions behind ON, you can specify a subquery behind WHERE dynamically.

 

But if you use any of the new things listed here, as already for SP05, the syntax check for Open SQL is carried out in a strict mode, where stricter syntax rules apply. E.g. you must use comma separated lists and the escape symbol @ in front of host variables. By this, at least in Open SQL ABAP enforces a release dependent  deprecation concept in a downward compatible way. Now what do you think about that?

Hi,

 

Recently I am involve in a very big project and we are discussing ways to code in old fashion user exit (form exit ,functions).

 

One of the options is to use BADI within the enhancement framework .

 

I became aware of this idea from  A framework for local/region specific requirements in user-exits using BAdIs and the new enhancement framework (revisited). by Bruno Esperança

 

This small blog post will present an idea how to sort the BADI implementation prior to execution.

 

For code See attache program Y_R_EITAN_TEST_51_18

 

The basis of the idea is based on the fact that internal table IMPS that contains the implementations is public.

 

2014-10-07_06h41_39.png

2014-10-07_06h41_56.png

 

In the BADI interface we can create a method that will give the implementation the option to return its sorting value .

2014-10-07_08h36_29.png

 

FORM test_01 is "regular" call .

And the result:

 

2014-10-07_08h44_20.png


FORM test_02 is "sorted" call .

 

From each implementation we get the result of get_sort_value to create the a sorting table .

2014-10-07_08h59_08.png

 

We sort the implementation in descending order .

 

And the result:

 

2014-10-07_08h46_03.png


It seems to be working .

 

Mind you that this sorting code was not tested in actual production environment .


Regards.


Besides the already mentioned REDUCE operator, the conditional FOR and  LINES OF inside VALUE & Co. and the grouping of internal tables, there are some other ABAP enhancements for internal tables in 7.40, SP08:

 

 

FILTER expressions

 

The new FILTER operator enables two kinds of filtering an internal table.

 

FILTER with single values

 

In this variant, you simply extrract the lines from an internal table into a tabular result, that fulfill a simple value condition.

 

DATA(extract) =

  FILTER #( spfli_tab USING KEY carr_city

              WHERE carrid   = CONV #( to_upper( carrid ) ) AND

                    cityfrom = CONV #( to_upper( cityfrom ) ) ).

 

As a prerequisite, the filtered table must have a sorted or a hash key (primary or secondary), that is evaluated behind WHERE. You can also extract the lines that do not fulfill the WHERE condition by using an EXCEPT addition.  Of course, you can achieve the same result by using a FOR inside a VALUE ror a REDUCE expression but FILTER allows you to write it shorter and it should be faster.

 

FILTER with filter table

 

In this variant, you compare the lines of one table with the contents of another table, the filter table, and you extract those lines, where at least one match is found (say hello to FOR ALL ENTRIES).

 

TYPES: BEGIN OF filter,
         cityfrom TYPE spfli-cityfrom,
         cityto   TYPE spfli-cityto,
       END OF filter,
       filter_tab TYPE HASHED TABLE OF filter
                  WITH UNIQUE KEY cityfrom cityto.

DATA(filter_tab) = ...        


DATA(extract) =
  FILTER #( spfli_tab IN filter_tab
              WHERE cityfrom = cityfrom  AND cityto = cityto ).

 

Here, the filter table - that can be specified also as a functional method call - must have a sorted or a hashed key (primary or secondary) that is evaluated.

 

Standard Value for Table Expressions

 

Table expressions itab[ ...] cannot support sy-subrc. Up to now, an exception was raised anytime if a table line specified in the square brackets could not be found. Not everybody liked this behavior.

 

As a workaround, you can place a table expression inside a VALUE or REF expression, that contains a OPTIONAL or DEFAULT addition. If a line is not found, the OPTIONAL addition returns an initiial line while the DEFAULT addition returns a given value, that can be specified as an expression, especially another table expression.

 

TYPES:

  BEGIN OF line,

    id    TYPE i,

    value TYPE string,

  END OF line,

  itab TYPE SORTED TABLE OF line WITH UNIQUE KEY id.

 

DATA(def) = VALUE line( id = 0 value = `not found` ).

 

 

...

 

DATA(result) = VALUE #( itab[ id = ... ] DEFAULT def ).

SAP Australia User Group Conference 2014 – Hilton Hotel, Sydney

 

image001.jpg

 

One thing that Australia has in abundance, apart from deserts and deadly wildlife, is SAP conferences. I was lucky enough to be presenting at the Australian SAP User Group conference in Sydney the other week, and I thought I would share some of the content from this event with you. In the past I have moaned and groaned and waffled on for ages about my dislike for blogs which purport to talk about SAP conference but which in reality talk about the food, and show pictures of dustbins, and list all the exciting cool people that the author met. Naturally this blog is going to be nothing like that.

 

Monday 8th of September

 

Upon arrival at the conference the first thing one’s eyes are drawn to is the exceptional quality of the dustbins. Set in a stunning silver style, with a crisp black bin bag, these beauties are capable of going from zero to sixty miles an hour in four seconds, if you were to throw one off a cliff.

 

Breakfast

 

To kick off the day we are served pigeon with warm foie gras, foie gras sorbet, beetroot, cocoa beans with a protruding pigeon leg immersed in Banyuls sauce. The unique pigeon taste and flavor blends well with the silken foie gras. For after’s we have a doughnut. This lavish fried treat is filled with Dom Perignon champagne jelly, and is topped with a 24-carat gold leaf, white chocolate flowers dusted in gold and edible diamonds.

 

International SAP Executive Update – Paul Young, GVP Customer Strategy, SAP AG

 

After having assorted top ten singles in the eighties such as “Love of the Common People” Paul Young then went on to become a high ranking member of software company SAP and became Scottish also. In this talk he brought to our attention that SAP have a product called HANA. To be serious for a second the main message here, which was to be repeated throughout the conference was that just doing the same thing as before really fast, whilst good, is not what HANA is all about. The point is to try and do things that you could not do before because they were in the “too hard” basket.

One interesting – and horrifying – theme that kept coming up throughout this conference is about how big companies want to get as much information about you as possible. The example given here was Disney theme parks where you buy some sort of wrist bracelet and if you wear it you get assorted benefits but in return the Disney corporation track you in real time and analyse everywhere you go and everything you buy, how long you stand in queues for certain rides etc.. A full scale 1984 type of thing and people are willing to pay to have this done to them.

The aim of the so called “big data” then seems to be to (a) predict what someone is going to do next with an aim to making money out of them and the next stage would be (b) to influence/control what someone is going to do next with the aim of making money out of them.

 

Next cab off the rank was Ray Wang, founder of Constellation Research. The previous keynote was naturally SAP focused, Ray talked in much broader terms about where technology is taking the world.

 

Morning Tea

 

To calm us down after the keynote speeches out comes an eel dish that reflects the so-called evolution technique, where the fish is cooked in two ways: as a slow-cooked technique requiring six and a half hours at 65 degrees, and the convention grilling. The dish is served with herbs, garlic and reduction sauce of eel juices and citrus, with potatoes coated with pepper powder on the side, and pan fried kangaroo loins.

There were a large number of SAP mentors in attendance at this conference, such as Graham Robinson, Jocelyn Dart, Professor Paul Hawking, Tony de Thomasis, and Greedy Smith from the pop group “Mentor as Anything”. Morning tea is a good opportunity to swap business cards with them and talk about HANA and UI5.

 

Next was a forty minute advert for IBM disguised as a keynote speech. The good thing about SAP conferences these days is that there is very little of this now, it used to be almost every speech was an advert at SAPPHIRE type events.

 

Then it was breakout time where you decide what you want to see. There were no less than five tracks, so you were spoilt for choice – you can always tell when a conference is going to be good when it is difficult to decide which presentation you want to see. I plumped for solution manager to start off with, and David Noack from SAP talked about how to best manage two major releases a year. I cannot talk for other companies, but where I work we put changes into production every week, so change management is more of an ongoing process than managing two great big big bangs each year.

 

Lunch

 

image002.png

 

The first day’s lunch consists of a sensational beef dish that comes from a 7-year-old dairy cow, which is slaughtered in front of our eyes and then cooked right on a table in the middle of the exhibitor hall on a hot stone. Once done, it is served with potato crisps, mushrooms and buttery bone marrow, with an accompaniment of tandoori crocodile, all washed down with port which has been guarded by eunuchs on top of a mountain in deepest Transylvania for 100 years.

 

I am a big fan of BRF+ so next I went to see Jocelyn Dart talk about why SAP has felt the need to have no less than three different business rules engines, and which one should you use. SAP is always doing this sort of thing, offering you multiple ways to achieve the same thing. In this case we have the ABAP based BRF+ which started life in Business One and is now part of the ECC 6.0 system, we have a Java based equivalent which uses technology acquired by SAP and naturally there is a HANA rules engine as well.

 

Then came what was for me the highlight of the conference which was Graham Robinson doing a session on UI5. He had no slides at all, he spent the whole time coding live, as he changed the code you saw what the result was in the UI5 application being played with. UI5  is not as scary as all that, once you get over the fact that you have to program the front end in JavaScript (a concept which makes traditional ABAP people shake with fear) things become a lot more straightforward.

 

Afternoon Tea

 

To complement the bowls of sweets we have chocolate made from Venezuela's rare Porcelana Criollo bean, topped with Tahitian Gold Vanilla Caviar and edible gold flakes together with vegemite on toast. Also we have Louis XIII de Remy Martin Cognac which comes in a hand blown sugar Fleur-de-Lis.

There were a large number of fictional characters from eth middle ages in attendance at this conference, such as Bill Brewer, Jan Stewer, Peter Gurney, Peter Davy, Dan'l Whiddon, Harry Hawke, Old Uncle Tom Cobley and all. Afternoon tea is a good opportunity to swap business cards with them and talk about HANA and UI5.

 

Then it was my turn to wow the audience with a talk on “secret” things you never knew were inside your SAP system. SAP used to have a split between new functionality delivered via enhancement packs, and bug fixes delivered via support stacks. Recently this has changed such that enhancements packs only come out every two years instead of annually, but in the meantime new functionality gets sneaked in via support stacks. Two examples are the ABAP Test Cockpit which came along as part of a support stack, and every support pack adds more tests to the code inspector, and the BOPF (Business Object Processing Framework) which is a major chunk of functionality which arrived via support stacks are opposed to being part of an enhancement pack.

 

To end the day we have another keynote, this time by David Roberts who is a top dog accountant at USA Company “Under Armour”. They have all former sports stars working for them, and so they do not have meetings they have “huddles”. How they work is to start of someone shouts out “Will you protect this house?” and everyone has to shout “I Will!” at the top of their voice. Then someone else shouts out “How much do you want it?” and the correct response is “I want it so much, I want t more than I want to breathe”. And this is the accountants. I shudder to think what sales and marketing do in their meeting equivalents.

 

Network Drinks

 

image003.png

 

To wash down the glasses of Henri Jayer Richebourg Grand Cru wine from Cote de Nuits in France are some fresh lobsters with select shellfish in summer vegetables, heads of fresh bulbs mixed with herbs in bubbly, light vinaigrette and a meat pie. This meat pie contains Wagyu beef fillet, Chinese matsutake mushrooms, winter black truffles, and French bluefoot mushrooms. Two bottles of vintage 1982 Chateau Mouton Rothschild wine are used in the gravy and the crust is covered in edible gold leaf.

 

Tuesday 9th

 

Breakfast

 

Todays breakfast includes bacon from rare pig breeds, truffle, watercress, saffron and an egg all cooked in truffle oil, sprinkled with real gold dust and served between a bun, along with caviar and a selection Louis Roderer Cristal Brut 2000 champagne sherbet and Madagascar chocolate cake with Moët très fine Champagne No. 7. The breakfast included a croissant covered in edible gold and jewels, Bar le Duc hand-seeded redcurrant jam, Kopi Luwak coffee, and a champagne and Chambord cocktail. All this was served from a bottle covered in gold and encrusted with real pearls and diamonds.

 

After that we staggered into the auditorium to hear the first of two keynotes, starting off with Alan Capes from the Canadian National Railways. That organisation is a gigantic success story, moving from almost going down the gurgler to becoming one of the most profitable railways companies in the world, and a large part of this is attributed to use of IT systems i.e. SAP.

 

Then we had a non SAP related keynote, Professor Hugh Durrant-Whyte from the Australian ICTA Research Centre of Excellence talked about how to use the nebulous concept of “big data” to actually make a difference in the real world – one example was the mapping that was ben done (somehow) of the entire layout of Australia many kilometers under the surface. What is being looked for are rocks that glow naturally hot, so a hole can be drilled and the heat transformed into renewable power. The same principle is naturally even more important to mining companies

 

Morning Tea

 

Fresh langoustines served on the SAP booth table alive, and “seared’ with a knife 15 minutes before the delegate devours it, which is a style that’s beyond a guarantee of freshness.

 

There were a large number of Hollywood movie stars in attendance at this conference, such as Brad Pitt, Matt Damon, Julia Roberts, George Clooney, Harrison Ford and Betty Boop. Morning tea is a good opportunity to swap business cards with them and talk about HANA and UI5.

 

Then there was one of the best attended sessions of the conference, in which Simon Kemp talked about and demonstrated the “Fiori Launchpad” which is new and shiny and everyone liked the look of it. I might be burnt at the stake for saying this, but this is an evolution of the “user menu” concept which has been around a long time in SAP, this time you customize lots of boxes all over the initial screen, each of which launches a fancy UI5 transaction (or something else). I feel sorry for anyone demonstrating this, as they keep getting asked the “is this the end of the SAP portal?” question, along with “is this the end of ESS/MSS?”. No-one wants to answer that question, but if I were a life assurance salesman the the SAP Portal came to me as a customer I would think twice.

 

Lunch

 

image004.png

 

A crunchy-tender-juicy-heady suckling pig served with tomato balls or “bolao” and pools of tomato jelly, with confit garlic to decorate the plate, and a crunchy funnel web spider sauce.

 

The former Prime Minister of Australia, said last year the Australian mining boom is over, but if it is no-one has told mining company Fortescue Metals. The CIO of that company, Vito Forte, gave a keynote speech, starting off with the growth figures for the company over the last few years, which is quite startling. Some of my colleagues went to Western Australia last year and showed me photographs of the sea being filled with ships, all waiting for the tide to change so they could take that days output to China or India or wherever. Fortescue metals is clearly a company which takes advantage of the latest SAP innovations, and the CIO talked about making sure the IT department is not the “department of no”.

 

The next keynote was from the CIO of Fairfax Media, a company which is being forced to reinvent itself for the digital age, as traditional media sectors like printed newspapers go into terminal decline. He showed a chart of some recent research which suggested that just 4% of people looked at adverts in printed newspapers, and yet companies still spend 18% pf their advertising budget on such things. He is all too horribly aware that this is just because “we have always done it this way” and one day companies will wake up.

 

Afternoon Tea

 

The final meal of the day is Duck Consommé served with what they call corn silk, with chocolate, vanilla purée, rice and apple, and deep fried emu in a beer batter.

 

There were a large number of rock starts in attendance at this conference such as Jimmi Hendrix, Freddie Mercury, Michael Jackson, John Lennon and Buddy Holly. Afternoon tea is a good opportunity to swap business cards with them and talk about HANA and UI5.

 

SAP Mentor Tony de Thomasis talked about the various tools available to you when preparing for an SAP upgrade. He is not a big fan of “technical” upgrades, and subscribes to the view that you should take advantage of new features as soon as they become available, a message that was repeated by the various CIOs of forward looking companies who spoke at this conference.

This was a very hands on type of presentation, with live demonstrations as opposed to power point slides. I noticed when an error message popped up it referred to SAP GUI version 740, which I did not think was out yet, and he considered showing  us the version 5 of the NWBC but decided against it as it is still too flaky. As it turns out these will be released on 8th of October 2015.

 

The final talk of the day was from Katrin Pietsch from SAP talking about the “customer connection program”. SAP provide many free services to their customers, the general idea is that if there is a piece of functionality that is not present in standard SAP then if you band together in a group (like a user group) then chances are you can get SAP to add it to the standard product. As an example think of all the “repairs” you make to the standard system – if a large number of companies are making the exact same change then why not tell SAP. An example I can think of is the ERS transaction MRRL. The transaction date defaults to todays date, which is crazy, so I have seen a large number of companies  make a “repair” to the standard transaction so you can specify the transaction date. Instead of five hundred companies making the same modification and then having to go through the SPAU pain at upgrade time, why not tell SAP and see if they can’t add that field as standard?

 

To end the conference we had a foot long bratwurst infused with hundred-year-old Louis XIII cognac and topped with fresh lobster, picante sauce and Kobe beef seared in olive and truffle oil, washed down with the world's oldest cocktail, created with 1788 Clos de Griffier Vieux Cognac, 1770 Kummel Liqueur, 1860 Dubb Orange Curacao and early 20th century Angostura Bitters.

 

All in all a wonderful conference, packed with fascinating SAP demonstrations, good food, and excellent dustbins.

 

Cheersy Cheers

 

Paul

 

"In computer programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed." (Wikipedia)

 

A very unused yet extremely simple software design technique is the lazy initialization (sometimes referred to as lazy instantiation or lazy loading). This is exceedingly helpful in large applications where a massive amount of data must be loaded and/or calculated at runtime.

 

For a better understanding, the following ABAP scenario is taken into consideration:

A company/client needs an application to display all the information related to a single entity model. Because the amount of that cannot fit into a single database table or a single screen, a decision has been taken to implement the display in the form of a tab strip, where each of the tabs will encapsulate information in different forms with the possibility to integrate different data sources (E.g. entry/entries from the underlying or external database, calculations, aggregates, RFC calls, Web Services, etc.).

 

In this example, several database tables will be used, each will correspond to one of the tabs:

tabs and data sources.png

The database objects will be uniquely identified by a key value structure, which will be referred to as the "common key". When the application is started and the tab strip is instantiated, the only known data is:

  1. The common key
  2. ID of the first displayed tab

(For clarity purposes, each data source will be named according to the corresponding tab: D1, D2 and so on.)

 

Because the business logic must be UI-independent, the application will be structured in two packages:

  1. Business object (BO) layer – encapsulates the data model and functionality
  2. User interface (UI) layer

 

Using a bottom-up approach, the BO layer will be designed first. For each data sources (in this case: ZD1, ZD2, etc.) a corresponding business class must be created.

bo architecture.png

ZCL_FACTORY will act as a bottleneck for the creation of the subordinate objects, which can only be instantiated by the factory method.

As the factory class contains a list of already instantiated BOs, it should also be a Singleton. The factory's constructor method is optional, but it can be used for example to either enqueue existing data in Change-mode or generated the common key in Create-mode.

 

A snippet of the ZCL_FACTORY class with some explanations:


CLASS zcl_factory DEFINITION
                 
PUBLIC

                                       FINAL

                  CREATE PRIVATE.

PUBLIC SECTION.
CLASS-METHODS get_instance
  
IMPORTING
    
value(iv_common_key) TYPE guid OPTIONAL
   RETURNING
    
value(ro_factory) TYPE REF TO zcl_factory.

METHODS constructor
  
IMPORTING
    
value(iv_common_key) TYPE guid OPTIONAL.

METHODS factory
  
IMPORTING
    
value(iv_type) TYPE string
   RETURNING
    
value(ro_bo) TYPE REF TO zcl_abstract_bo.

PRIVATE SECTION.
TYPES:
  
BEGIN OF ts_list,
     classname
TYPE classname,
     bo 
TYPE REF TO zcl_abstract_bo,
  
END OF ts_list,
   tt_list
TYPE STANDARD TABLE OF ts_list.

CLASS-DATA :
   mo_factory
TYPE REF TO zcl_factory.

DATA :
   mt_list      
TYPE tt_list,
   mv_common_key
TYPE guid.
ENDCLASS.


CLASS zcl_factory IMPLEMENTATION.
METHOD constructor.
   mv_common_key
= iv_common_key.
ENDMETHOD.

METHOD factory.
  
DATA :
     lv_classname
TYPE classname.

FIELD-SYMBOLS :
   <fs_list>
TYPE ts_list.

*   Obviously these hard-coded values are not optimal
CASE iv_type.
  
WHEN 'D1'
    
OR 'D2'
    
OR 'Dx'.
       lv_classname
= |ZCL_BO_{ iv_type }|.
  
WHEN OTHERS.
*      RAISE EXCEPTION TYPE ...
ENDCASE.

*   Check if the list already contains an instance
READ TABLE mt_list ASSIGNING <fs_list>
                   
WITH KEY classname = lv_classname.

*   If not, create
IF NOT sy-subrc IS INITIAL.
  
APPEND INITIAL LINE TO mt_list ASSIGNING <fs_list>.
   <fs_list>
-classname = lv_classname.
  
CREATE OBJECT <fs_list>-bo TYPE (lv_classname)
    
EXPORTING
       iv_common_key
= mv_common_key.
ENDIF.

*   Return BO instance
   ro_bo
= <fs_list>-bo.
ENDMETHOD.

METHOD get_instance.
*   Singleton method to retrieve factory instance
IF mo_factory IS INITIAL.
*    Just as an example for Create/Display
  
IF iv_common_key IS SUPPLIED.
    
CREATE OBJECT mo_factory
      
EXPORTING
         iv_common_key
= iv_common_key.
  
ELSE.
    
TRY.
      
CREATE OBJECT mo_factory
        
EXPORTING
           iv_common_key
= cl_uuid_factory=>create_system_uuid( )->create_uuid_x16( ).
    
CATCH cx_uuid_error.
*        ...
    
ENDTRY.
  
ENDIF.
ENDIF.

   ro_factory
= mo_factory.
ENDMETHOD.
ENDCLASS.

 

Note: Attribute MT_LIST can also be used as a mechanism to control methods like: SAVE, CHECK_BEFORE_SAVE, HAS_CHANGES, and so on; but this depends on particular scenarios.

 

At this point, each specific BO can be implemented to match the particular use case.

  1. E.g. ZCL_BO_D1 reads a single entry from the database and displays it as a form. Therefore each field will have corresponding getter and setter methods. Alternatively, ZCL_BO_D2 will select a number of rows from the database and output them as a table. In which case, CRUD functionalities might be more adequate.

The constructor of each BO is responsible to load the data buffer using the given common key.

 

Note: As all the business objects have the property CREATE PRIVATE, each one must declare ZCL_FACTORY as friend.

 

The following statement can be used to create a BO instance for D1:


DATA :
lo_bo
TYPE REF TO zcl_bo_d1.

lo_bo ?= zcl_factory
=>get_instance( )->factory( 'D1' ).


After the business logic is active and secured, it is time to move up towards the UI layer. For this example, the user interface has been implemented both as an ABAP report and a Web Dynpro application. Nevertheless any other UI technology can be used.


1. ABAP Report

The most elementary implementation requires a main screen (100) and two sub screens (101, 102). The latter are embedded into a tab strip control belonging to the main screen. The trick here consists of adding the BO instantiation in the PBO module of each sub screen. As the PAI module of screen 100 controls the tab strip actions, the corresponding PBO modules are only called upon when necessary. This means that data is only loaded when displayed.

 

2. Web Dynpro Application

The application consists of a main WD component with a start view and an embedded TabStrip UI element. Based on the complexity of the application, it is a matter of choice how each tab is implemented:

     2.1. Directly in the same view

     2.2. As a different view inside the same application

     2.3. As a separate WD component

 

Either way, the component controller (from the main component 2.1., 2.2., or the each used component, 2.3.) must implement initialization method(s) – for 2.3., these methods must be defined as interface.

The catch in this situation is that TabStrip UI element has an event called ON_SELECT. For this event, an associated event handler is implemented in the start view of the main component. Similar to the PAI module, this method is used to determine the navigation and therefore control the BO instantiation.

 

METHOD onactionon_select.
 
CASE wdevent->get_string( 'TAB' ).
   
WHEN 'D1'.
   
WHEN 'D2'.
*     2.1., 2.2.
      wd_comp_controller
->init_d2( ).
*     OR

*     2.3.
      wd_this
->wd_cpifc_d2( )->init( ).
   
WHEN 'Dx'.
 
ENDCASE.
ENDMETHOD.

 

Note: This can also be implemented using Floorplan Manager (FPM) as an OIF component by checking the navigated tab in method PROCESS_BEFORE_OUTPUT.

 

Evidently, it is important to set the most used tab as initial or give the possibility for the user to choose.

 

In the end, this is the most rudimentary example I could think of, but it can nevertheless be scaled tremendously.

 

 

Thank you and please feel free to contribute. My only hope is that I didn't compress the content to the extent that it is not understandable.

 

Tudor

You know the GROUP BY clause from SQL. There was not such a clause for internal tables up to now. All we had was that clumsy group level processing with statements AT NEW ... that relied on the order of table columns and contents that is sorted respectively.

 

With release 7.40, SP08 there is a real GROUP BY clause for LOOP AT itab that is much more powerful than the SQL one.

 

DATA flights TYPE TABLE OF spfli WITH EMPTY KEY.

SELECT * FROM  spfli
         WHERE carrid = '...'
         INTO TABLE @flights.

 

DATA members LIKE flights.
LOOP AT flights INTO DATA(flight)
     GROUP BY ( carrier = flight-carrid cityfr = flight-cityfrom )
              ASCENDING
              ASSIGNING FIELD-SYMBOL(<group>).
  CLEAR members.
  LOOP AT GROUP <group> ASSIGNING FIELD-SYMBOL(<flight>).
    members = VALUE #( BASE members ( <flight> ) ).
  ENDLOOP.
  cl_demo_output=>write( members ).
ENDLOOP.
cl_demo_output=>display( ).

 

Looks like dreaded nested LOOPs, but it isn't quite that!  What happens here is that the first LOOP statement is executed over all internal table lines in one go and the new GROUP BY addition groups the lines. Technically, the lines are bound internally  to a group that belongs to a group key that is specified behind GROUP BY.The group key is calculated for each loop pass. And the best is, it need not be as simple as using only column values, but you can use any expressions here that normally depend on the contents of the current line, e.g. comparisons, method calls, .... The LOOP body is not evaluated in this phase!

 

Only after the grouping phase, the LOOP body is evaluated. Now a second (but not nested) loop is carried out over the groups constructed in the first phase. Inside this group loop you can access the group using e.g. the field symbol <group> that is assigned to the group in the above example. If you want to access the members of the group, you can us the new LOOP AT GROUP statement, which enables a member loop within the group loop. In the example, the members are inserted into a member table and displayed.

 

Here another example, where the group key is evaluated from method calls:

 

LOOP AT flights INTO DATA(wa)

     GROUP BY ( tz_from = get_time_zone( wa-airpfrom )

                tz_to   = get_time_zone( wa-airpto ) )

     ASSIGNING FIELD-SYMBOL(<group>).

  ...

ENDLOOP.

 

Of course, there is also expression enabled syntax for grouping internal tables.

 

In a first step, we get rid of LOOP AT GROUP by replacing it with a FOR expression:

 

DATA members LIKE flights.

LOOP AT flights INTO DATA(flight)

     GROUP BY ( carrier = flight-carrid cityfr = flight-cityfrom )

              ASCENDING

              ASSIGNING FIELD-SYMBOL(<group>).

  members = VALUE #( FOR m IN GROUP <group> ( m ) ).

  cl_demo_output=>write( members ).

ENDLOOP.

cl_demo_output=>display( ).

 

The IN GROUP is a new addition to FOR. Second, away with the outer LOOP:

 

TYPES t_flights LIKE flights.

DATA out TYPE REF TO if_demo_output.

out = REDUCE #( INIT o = cl_demo_output=>new( )

                FOR GROUPS <group> OF flight IN flights

                GROUP BY ( carrier = flight-carrid cityfr = flight-cityfrom )

                  ASCENDING

                LET members = VALUE t_flights( FOR m IN GROUP <group> ( m ) ) IN

                NEXT o = o->write( members ) ).

out->display( ).

 

FOR GROUPS is another new FOR variant. Believe me, it does the same as the variants above. But for reasons of readability, the combination of LOOP AT GROUP with a FOR IN GROUP within might be the preferable one, at least for this example .

 

For a deep dive, read this and this!

 

PS: If you are confused what kind of loops and nesting takes place, have a look at my comment below.

Has this happened to someone you know: in their eagerness to start development for a repair or change, they forgot to confirm if the development version being worked upon was in sync with production? Thus promoting unintended code to production. The consequences of that are never pretty!

 

Let’s consider a simplified scenario (derived from real life ): Person A makes changes to a program, activates it and deletes his transport. Subsequently person B starts a new set of changes on the active version in development.  Person B tests and moves his/her changes to production and has to deal with the not so nice consequences of also moving person A’s changes. (Now, there are those who may argue correctly that there is a process gap, improper quality control and human error but leave that aside for now. Also, I am strong believer in automation to overcome the human element).

 

Here’s how you prevent this from happening again: all it takes is a well placed exit enhanced with the code below. The code compares the local versions with the version stored in the production system. It takes the latest development version and works backwards till it finds a version that matches production. As an example, for the following program –

Capture.PNG

Check if active version matches production

 

if not

 

Check if modif  version matches production

 

if not

 

Check if version 4 matches production

 

if not

 

Check if version 3 matches production....and so on, resulting in

Untitled.png

 

 

The code is attached.

 

 

Right now the code works for the ABAP Editor (SE38): Relevant Enhancement Spot: SEUED001, Component: EXIT_SAPLS38E_001. The exit is triggered when the editor is invoked, so this works automatically for all programs you try to edit.

 

Finally, I believe it should be trivial to adapt this code to Class Editor or Print workbench or whatever else you can think of. That and the task of identifying the proper exits are left as an exercise for the reader .  It would be great if  a common exit could be found for all objects.

 

Please add your improvements and let know if useful. One known limitation is this gets called multiple times if you are jumping around in the edit mode and the object is not locked in any transport. There are ways around it but I haven't gotten around to adding the relevant code.

 

TIP: For SE37, try SEU00004 -> EXIT_SAPMS38L_001

 

P.S: I take a couple of shortcuts in my code, by hardcoding the production system and its RFC path. I am sure one can figure out how to make that part generic by using the SLD. However since the definition is done once per system, the tradeoff is acceptable to me.


A month back, I wrote a short blog about the application log, and how great it would be to have yet another object oriented wrapper around the SBAL function group and friends.  Well, I wasn't kidding.  I just put up a working version on Github, and added some documentation to explain some of the features I thought would make a logger tool great.  It's been done fifty times before, but I haven't seen any of them with the features I describe.  It's all working, so feel free to kick the tires, fork the project, and please, please, let me know what you think!

 

epeterson320/ABAP-Logger · GitHub

 

Here is my original post: What Would a Developer-Friendly Application Log Look Like?

 

Below is most of the readme from Github:

 

ZCL_LOGGER

 

SAP Logging as painless as any other language

 

One of these things is not like the others

 

One of the most effective ways of debugging and inspecting a program's operations is by writing to a log.  Logging is so powerful, just about every language has some way of doing it.  For example, if you want to write to a log in javascript, you just need to write the following line of code:

 

    console.log("Leapin' lizards, something went wrong!");

 

Or how about in Ruby? Ruby has the added benefit of different levels of messages.

 

    require 'logger'

    log = Logger.new('logfile.log')

    log.warn("You're on thin ice, bud.")

    log.info("Things are normal again.")

 

Writing an Android app? You're in luck - you can log and optionally tag messages, all in one line of Java.

 

    Log.e('MAPS_INTERFACE', 'The system is down!!!');

 

So, how does ABAP logging stack up to the other languages? What code is required to log a string to be viewed later?

 

    DATA: header TYPE bal_s_log,

          handle TYPE balloghndl,

          handles_to_save TYPE bal_t_logh.

 

    header-object = 'ZINTERFACES'.

    header-subobject = 'ACCOUNTING'.

    header-extnumber = 'Stuff imported from legacy systems'.

 

    CALL FUNCTION 'BAL_LOG_CREATE'

      EXPORTING

        i_s_log      = header

      IMPORTING

        e_log_handle = handle.

 

    CALL FUNCTION 'BAL_LOG_MSG_ADD_FREE_TEXT'

      EXPORTING

        i_log_handle = handle

        i_msgty = 'E'

        i_text = 'You see, what had happened was...'.

 

    CALL FUNCTION 'BAL_DB_SAVE'

      EXPORTING

        i_t_log_handle = handles_to_save.

 

If you're not asleep after writing all that, then you've at least forgot what you were programming before you had to write to a log.  If anything, logging should be QUICK so you can get on with the real programming!

 

Get out of my way

 

A better log would barely interrupt my code, so I can output messages in one line, and you don't lose the big picture as you are reading it. What do you wish the ABAP example above looked like?  How about this:

 

    DATA: log TYPE REF TO zcl_logger.

    log = zcl_logger=>new( object = 'ZINTERFACES'

                           subobject = 'ACCOUNTING'

                           desc = 'Stuff imported from legacy systems' ).

    log->e( 'You see, what had happened was...' ).

 

All the information, none of the boilerplate. This is the goal of ZCL_LOGGER.

 

Log anything

 

Making use of SAP's run-time type services, we can pass almost anything we might want to log to an instance of ZCL_LOGGER, and it will do the heavy lifting.

 

Log a string!

 

    log->s( 'Document 4800095710 created successfully' ).

 

Log a bapi return message!

 

    DATA: rtn TYPE bapiret2.

    log->add( rtn ).

 

Log a...gasp...TABLE of bapi return messages!

 

    DATA: msgs TYPE TABLE OF bapiret2.

    log->add( msgs ).

 

Log an exception? Yep!

 

    TRY.

        rubber_band_powered_spaceship=>fly_to( the_moon ).

      CATCH zcx_not_enough_power INTO err.

        log->e( err ).

    ENDTRY.

 

Log the current system message.

 

    MESSAGE e001(oo) WITH foo bar baz INTO dummy.

    log->add( ). "you don't even need to pass anything in, bro.

 

Log the return of a BDC call.

 

    CALL TRANSACTION 'CO07' USING bdc_tab MESSAGES INTO bdc_messages.

    log->add( bdc_messages ).

 

And that's every scenario I've been able to think of, so far.

 

Don't Ignore SAP's Strengths

 

As frustrating as it can sometimes be, the SAP environment has a lot of power, and it would be good not to ignore it.  Transaction code SLG1 views and filters logs with ease, and it allows for added context variables and parameters. A new logger class should not reinvent the wheel, the wheelbarrow, or the mechanisms for saving and displaying logs.

 

If you have an instance of a log object, you can add context variables and a problem class, two of a few things that SLG1 handles well.

 

    log->w( obj_to_log = 'Document created with errors' "<-- Which document? Needs context.

            context = new_document ). "<-- Here's the context

 

Since this log is designed to be simple, it has compromised a lot of the more exotic function modules in the SBAL family. If your log needs to use one of these function modules, the log header, handle and database id are all read-only members of the class, so you can pass them right along to the function module.

 

    log->i( 'Results of system analysis' ).

 

    CALL FUNCTION 'BAL_LOG_MSG_CUMULATE'

      EXPORTING

        i_log_handle = log->handle

        i_s_msg = l_msg

        i_compare_attributes = abap_true.

 

Chainable

 

It's 2014, so yes, you can chain method calls.

 

    zcl_logger=>new( object = 'foo' )->e( 'Bad things happened: See details' )->e( error ).


With 7.40, SP05 the first version of the iteration operator FOR was introduced. You can use it in constructor expressions with VALUE and NEW for so called table comprehensions, as e.g.

 

DATA(itab2) = VALUE t_itab2( FOR wa IN itab1 WHERE ( col1 < 30 )

                             ( col1 = wa-col2 col2 = wa-col3 ) ).

 

This is an expression enabled version of LOOP AT itab. It didn't take long to ask also for expression enabled versions of DO and WHILE (couldn't stand them in otherwise expression enabled examples any more ...).

 

Therefore, with 7.40, SP08 we also offer conditional iterations with FOR:

 

... FOR i = ... [THEN expr] UNTIL|WHILE log_exp ...

 

You can use FOR in constructor expressions with VALUE and NEW in order to create new internal tables, e.g.:

 

TYPES:
  BEGIN OF line,
    col1 TYPE i,
    col2 TYPE i,
    col3 TYPE i,
  END OF line,
  itab TYPE STANDARD TABLE OF line WITH EMPTY KEY.

 

DATA(itab) = VALUE itab(
     FOR j = 11 THEN j + 10 UNTIL j > 40
     ( col1 = j col2 = j + 1 col3 = j + 2  ) ).


gives

 

COL1COL2COL3
111213
212223
313233

 

Neat, isn't it?


But we don't want to construct internal tables only. Now that we have all kinds of iterations available with FOR, we want to construct arbitrary types. And that's where the new constructor operator REDUCE comes in.

 

... REDUCE type(

      INIT result = start_value

           ...

      FOR for_exp1

      FOR for_exp2

      ...

      NEXT ...

           result = iterated_value

           ... ) ...

 

While VALUE and NEW expressions can include FOR expressions, REDUCE must include at least one FOR expression. You can use all kinds of FOR expressions in REDUCE:

  • with IN for iterating internal tables
  • with UNTIL or WHILE for conditional iterations.


Let's reduce an internal table:

 

DATA itab TYPE STANDARD TABLE OF i WITH EMPTY KEY.
itab = VALUE #( FOR j = 1 WHILE j <= 10 ( j ) ).


DATA(sum) = REDUCE i( INIT x = 0 FOR wa IN itab NEXT x = x + wa ).

 

First, the table is filled with VALUE and FOR and then it is reduced with REDUCE to the sum of its contents. Note that there is no THEN used to construct the table. If THEN is not specified explicitly, implicitly THEN j = j + 1 is used. Be also aware, that you can place any expression behind THEN, including method calls. You only have to make sure that the end condition is reached within maximal program run time.

 

Now let's reduce the values of a conditional iteration into a string:

 

DATA(result) =

  REDUCE string( INIT text = `Count up:`

                 FOR n = 1 UNTIL n > 10

                 NEXT text = text && | { n }| ).

 

The result is

 

Count up: 1 2 3 4 5 6 7 8 9 10

 

These simple examples show the principle. Now imagine, what you can do by mixing REDUCE with all the other expression enabled capabilities. I only say nested REDUCE and VALUE operators ...

 

To conclude I show a cunning little thing that I use in some of my documentation examples:

 

TYPES outref TYPE REF TO if_demo_output.

 

DATA(output) =
  REDUCE outref( INIT out  = cl_demo_output=>new( )
                      text = `Count up:`
                 FOR n = 1 UNTIL n > 11
                 NEXT out = out->write( text )
                      text = |{ n }| ).

 

output->display( ).

 

I reduced the values of an iteration into the output list of a display object, oh my ...

Moderator notice The techniques described in this blog are NOT recommended by SAP and many independent professionals would also not recommend their use. It is far better to use the customisation techniques provided by SAP.


If you use the technique described in this blog, you do so at your own risk and you should read the comments section carefully. "It works" is simply not a good enough reason when you are dealing with customer systems.


Before I start on the topic of ASSIGN statement in ABAP, I would like to take the opportunity to introduce myself here. I am a 10 years experienced SAP professional with 4 years experience as a technical consultant and rest as functional. Though I am more of a functional now, I still do not mind diving deep into ABAP getting my hands dirty in coding when the situation asks for it.

 

Recently I was working on a problem in which I had to populate the value date for the clearing document which is posted after manually clearing the open items for the customer. The tricky thing was to determine at runtime which are the two selected open items selected by the user to clear.

 

The solution was obvious – FI substitution for value date.

 

However, knowing that the solution was easy, implementing it, turned out to be quite tricky.

 

Obstacle 1: Value date was not present in the list of fields to be substituted in BSEG. How to substitute then?

Solution: After doing sufficient amount of research on the internet and exploring the issue with the seniors I learnt that we can open certain fields from the BSEG table for substitution which are initially not allowed by SAP. Yeah I agree opening certain fields from the table “VWTYGB01” is a modification, but looking the possible options at hand and the dire customer requirement we had limited choices and had to take this path. For more details on this kindly look at the hereby mentioned SAP Notes: 842318, 42615.

Also follow the below mentioned links for more information on this.

 

http://scn.sap.com/thread/3168871

 

http://scn.sap.com/thread/1254973

 

Blog: http://scn.sap.com/community/business-one/add-ons/blog/2013/09/27/adding-of-a-particular-field-in-the-substitution-rules--ggb1

 

Anyways I do not want to digress too much from the core topic here.

 

Obstacle 2: Finding out the two document which are to be cleared manually by the user ?

Solution: With no clear approach in sight we started debugging the standard program “SAPMF05A” for F-32 i.e. “Clear Customer data”. Within 5 min of debugging (I am not exaggerating here) we found out that everything tied to the internal table POSTAB[]. My hunch was to read this table in the substitution to dynamically identify the line items. I created a sample substitution and hooked it in the FI postings. For more details on how to create substitution please refer to the link.

 

The table data was present in the ABAP memory at the time the code control came to the substitution exit sub routine. Please refer to the ABAP memory stack screen shot shown below.

 

Assign01.PNG

 

The sole task remaining was to now to read that data. The magic is was done by sole three statements coming to the rescue.

 

Assign02.PNG

 

The general syntax as mentioned in the F1 help is ASSIGN (memory_area). Now the interpretation of the memory area is quite interesting.

 

Assign (lt_itab[]) to <fs>

This will assign the starting memory address of the internal table to <fs>.

 

Assign (program_name) to <fs>.

This will assign the starting memory address of the complete program ABAP memory to the field symbol.

 

Assign (‘(program_name)lt_itab[]’ ) to <fs>.

This will assign the starting address of the internal table lt_itab[] from the program_name to the field symbol. As a side note we have to make sure that the program_name is loaded into the ABAP memory at runtime for one to access.

 

From the SAP help, we do understand the usage of ASSIGN statement, but this implementation will surely stick to my head for forever after this.

 

Hope this helps other developers as well.


Predicative Method Calls

 

For quite some time the lack of a real boolean type in ABAP led to a lot of discussions inside and outside of SAP and especially in SCN. People kept asking, why can I not write

 

IF meth( ... ).

  ...

ENDIF.

 

The answer always was: Because you have to have a boolean type behind IF and no method can return a boolean type.

 

But in fact, the solution was absolutely simple and - tatarata! - is made availabe in Release 7.40, SP08.

 

You can write the so called predicative method call

 

... meth( ) ...

 

as a short form of the relational expression

 

... meth( ) IS NOT INITIAL ...

 

now and that's already all! Of course,  methods that return a value of type ABAP_BOOL are especially suited for that purpose. We call those predicate methods now, but it's not a prerequisite to use them.

 

So you can place simple functional method calls everywhere, where logical expressions are allowed: behind  IF, CHECK, ASSERT, COND, SWITCH, ...

 

The documentation shows some examples:

 

IF cl_abap_demo_services=>is_production_system( ).

  cl_demo_output=>display(

     'This demo cannot be executed in a production system' ).

  LEAVE PROGRAM.

ENDIF.

 

Here, a predicative method is called that returns ABAP_TRUE or ABAP_FALSE.

 

COND string( WHEN cl_demo_spfli=>get_spfli( to_upper( carrier ) )

               THEN `Filled`

               ELSE `Not filled` )

 

Here, a normal method is called and the result is true, if the table type return value contains lines.

 

Why didn't we enable this long before?

 

 

New Boolean Function

 

Did you ever stumble over that one?

 

IF boolc( 1 = 2 ) = abap_false.
  cl_demo_output=>display_text( 'yes' ).
ELSE.
  cl_demo_output=>display_text( 'no' ).
ENDIF.

 

The relational expression boolc( 1 = 2 ) = abap_false is false! Why? Because boolc despite its name does not return c but string and the comparison rules for c and string blah, blah, blah ...

 

Now, with Release 7.40, SP08, we have an new Boolean function.

 

IF xsdbool( 1 = 2 ) = abap_false.

  cl_demo_output=>display_text( 'yes' ).

ELSE.

  cl_demo_output=>display_text( 'no' ).

ENDIF.

 

The relational expression xsdbool( 1 = 2 ) = abap_false is true, because xsdbool returns type XSDBOOLEAN from the ABAP Dictionary that is - yes, you guess it -  c of length 1. For the experts among you, XSDBOOLEAN is normally used for special mappings in XML-transformations and was reused here for quite another purpose. And that's were the funny name xsdbool comes from.

 

The new Boolean function xsdbool can be used everywhere, where comparisons with type c length 1 are needed, especially comparisons with type ABAP_BOOL.But be aware of the fact, that the space that is returned by xsdbool for a false expression is cut in many operand positions, e.g. if you want to convert it with a builin function like translate.

Actions

Filter Blog

By author:
By date:
By tag: