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

In Object oriented programming our goal is to deal with real world objects and their state (data). This is in stark contrast to the procedural approach, where developers are more focused on procedures operating on data structures. 

To that end SAP created a set of services (the Transaction Service, the Persistence Service and the Query Service) to help you better manage objects in your Object Orientated applications, providing improved integration between the ABAP Runtime and the Database layer.

After a bit of research around the subject, I found the basic implementation straight forward.  I saw some really neat features. But I was still concerned with the generation of the persistence class. In particular the restriction that the tables associated with a persistent class must share the same key.  When working with complex objects, for example a purchase order, the state is inherently persisted across multiple tables in an SAP system with foreign key relationships.  A purchase order has items attached to it, which in turn may have schedule lines attached to them.  So why could I not select multiple tables and the generator create the relevant classes and associations? 

On reflection, I realised how naïve I was to think it would be possible to generate a class that encapsulated the relationships between multiple tables.  I was still stuck, I knew what I wanted to do, but was not sure how could I bridge the gap between my real world object and the persistent classes I generated from my tables. So I sat down and drew out some approaches that I could use to achieve this. 

This blog will ultimately document this approach.

Overview of the services

I’ll begin by briefly summarising what each of these services do: 

  • Persistence service – Allows you to generate a class, whose objects represents specified fields of one or more tables (that must have the same key fields) and providing access via get and set methods (accessor and mutator).  Each instance of the class representing a record within a table/s, which are representative of the persistence objects state.
  • Transaction Service – Is based on the SAP LUW. It records all changes made to the persistence object and registers them against the transaction.  Internally it still works with update function modules, but provides a simpler and tidier interface to do so. 
  • Query Service – Used for searching and loading instances of persistent classes that match the specified query criteria.

For more details see the sap help entry under Object Services.

I do not plan to go into too much detail on how to start developing with these services, as there are many good blogs and documents out there that explain this.  (Admittedly the best thing I ever did was read the book “Object Services in ABAP” from SAP Press, which I would definitely consider as essential reading.) 

My hypothetical real world requirement

I have a requirement from a customer to create or maintain a new class representing shopping carts for purchasing. They need an option to add or remove items from the carts.  The business logic should be designed in such a way that is can be easily be plugged into any application (ABAP, or Web Dynpro or in a proxy class call from PI).  Effectively my code will form the model of a business application following the MVC design concept.   

In terms of data for the shopping carts, it will be persisted across two tables in the SAP DB.

ZSHOP_CART – Representing the header fields

ZSHOP_CART_ITEM – Representing the shopping cart item, with a foreign key relationship to the header, based on the CART_ID field.

Design

Overview

In a nutshell, for each of the two shopping cart tables created, I created persistence class.  When working with persistence classes you have three potential options for customization. You can modify it directly, extend it or wrap it.  These options are clearly explained in the book “Object Services in ABAP” with an explanation on the pros and cons of each. 

I opted to implement my customisation by wrapping my persistence classes.  My main reasons for doing this, is so I can hide some of the coding of how to create instances of the persistence classes from developers working with my wrapper classes.  I will provide access to the persistence class via the wrapper class. 

The only way to access and manipulate the table data will be via the accessor and mutator methods of the persistence classes.  The wrapper classes of the persistent classes should not store any persistent data. This is important, as any changes to the underlying tables, would also require subsequent changes to be made in the wrapper classes.  By doing this, all that should be required is to regenerate the persistence classes (assuming you did not adjust the key of the table, then you will have to update the wrapper classes). 

Another class will be developed to store the shopping cart item objects linked to the cart at runtime.  This class will be an attribute of the shopping cart header class.  Our shopping cart cannot logically exist without its items and this helps to logically encapsulate that.

Each class is responsible for the validation of its own data.  Calling the validation method of the header class, will implicitly trigger validation method of the items as a group and consequently the individual item data validation routine. The results will be included together but can be called individually if required.

Class Specific Details

Shopping Cart Wrapper Header Class (ZCL_SHOPPING_CART)

ZCL_SHOPPING_CART is the main interface for all developers to interact with shopping carts.  The ZCL_SHOPPING_CART class can only be privately instantiated. Two static methods will be available to developers to create new instances or retrieve existing instances of shopping carts.  By only allowing private instantiation, you can control how instances are created and handle some of the complexities of object services, for someone unfamiliar with the APIs.

One of the above mentioned features will be the locking the shopping cart record for change.  The ZCL_SHOPPING_CART class will implicitly set the required pessimistic locks for the record being maintained and the appropriate lock modules will need to be generated for this purpose.  Unfortunately the lock concept is not integrated in object services, so you need to do this yourself.  It’s not hard and a great explanation on how to implement pessimistic and optimistic locking is provided in the book “Object Services in ABAP”.

I also wanted to make sure that the developer does not forget to release the lock that implicitly gets created when creating or retrieving their shopping cart instance. So a method will be created to listen for the transaction service event IF_OS_TRANSACTION~FINISHED at the end of the transaction processing. Once the transaction processing has completed, this method will be called and perform the release of the lock on the cart id. 

Two other class methods will be created for starting and ending the transactionfor the LUW. These wrap up a bit of the logic for the transaction service, making it a bit easier for developers working with the transactions.  I thought it was better to allow the developer to control this, rather than try to implicitly trigger it for the developer during instantiation of the class. I think this probably should be included in separate entity for reusability, but for simplicity I have just added it here. 

A couple of other public class methods will be exposed to provide users with a way to search for shopping carts.

Shopping Cart Items Class (ZCL_SHOPPING_CART_ITEMS_MAP)

Instantiation of ZCL_SHOPPING_CART_ITEMS_MAP is public, but outside of the context of the shopping cart header, does not add much value.  The expectation would be that developers access the instantiated object via the accessor method provided in the header class (ZCL_SHOPPING_CART).

The standard class CL_OBJECT_MAP will be used as the internal storage mechanism of this class. This is OO coding after all and we should be aiming to re-use as much as possible. It is developed for key based access to records and similar to the Java Map Class.  Everybody loves getting stuff for free, so if it does what you need, then you should look to re-use it.  I opted not to inherit from this class as I did not want to developers using this code to access all the methods available. It has been added as a private class attribute.

Validation logic will be encoded here to ensure the integrity of the items being added, modified or deleted from the cart. The validation routine here will also execute the specific validation routine for each of the items in the cart, which will be included in the results.

As this class is the storage holder for items during runtime, it is only logical that it should be the main class that can be used by developers to control the creation, access and deletion of items for the cart.  For this reason a set of methods will be publicly exposed to do this.

Shopping Cart Wrapper Item Class (ZCL_SHOPPING_CART_ITEM)

The shopping cart item class will also be set for private instantiation for the same reasons as the header class.  There will be three public class methods to create an instance, one to create new instance, one to get an existing instance and a third to create an instance from an existing persistence class reference.

This third method is required, because the method in class ZCL_SHOPPING_CART_ITEMS_MAP to retrieve multiple item records does so via the query service.   The query service returns multiple instances of the item persistence class for the same cart id, so it made sense to me to offer the ability to instantiate a wrapper item from the item persistence class, just to make my life easier.

A method will be created for deletion, but as explained earlier this method should not be called by developers, as methods will be available in class ZCL_SHOPPING_CART_ITEMS_MAP for this.

Directly calling these methods is not recommended. There will be methods specifically created in the cart items class to do this, ensuring everything is always done in the context of the cart as a whole.

The only method a developer should be calling in this class directly is the method to retrieve the persistent class to read and manipulate the item record fields.

UML Diagram of classes created and their relationships

These were generated via SE80....

Source Code

I have attached text files for the three classes I created for this.

Below is the source code for my test application, where I create a new shopping cart, add some items and save the data.  I then go back and retrieve the data and output the results.

*&---------------------------------------------------------------------*

*& Report  ZKP_SHOPPING_CART_APP

*&

*&---------------------------------------------------------------------*

*&

*&

*&---------------------------------------------------------------------*

REPORT  zkp_shopping_cart_test.

*----------------------------------------------------------------------*

*       CLASS zcl_shopping_cart_controller DEFINITION

*----------------------------------------------------------------------*

*

*----------------------------------------------------------------------*

CLASS zcl_shopping_cart_controller DEFINITION ABSTRACT FINAL.

  PUBLIC SECTION.

    CLASS-METHODS: run_program,

                   create_data

                     RETURNING value(re_cart_no) TYPE zcart_no,

                   read_data

                     IMPORTING im_cart_no TYPE zcart_no.

ENDCLASS.                    "zcl_shopping_cart_controller DEFINITION

START-OF-SELECTION.

  zcl_shopping_cart_controller=>run_program( ).

*----------------------------------------------------------------------*

*       CLASS zcl_shopping_cart_controller IMPLEMENTATION

*----------------------------------------------------------------------*

*

*----------------------------------------------------------------------*

CLASS zcl_shopping_cart_controller IMPLEMENTATION.

  METHOD run_program.

    DATA: l_cart_no TYPE zcart_no.

    l_cart_no = create_data( ).

    ULINE.

    read_data( l_cart_no ).

  ENDMETHOD.                    "run_program

  METHOD create_data.

    DATA: lo_shopping_cart TYPE REF TO zcl_shopping_cart,

          lo_cart_hdr_db   TYPE REF TO zcl_shopping_cart_persist,

          lo_cart_items    TYPE REF TO zcl_shopping_cart_items_map,

          lo_cart_item     TYPE REF TO zcl_shopping_cart_item,

          lo_cart_item_db  TYPE REF TO zcl_shopping_cart_item_persist,

          lx_cart_excp     TYPE REF TO zcx_shopping_cart_excp,

          l_error_text     TYPE string,

          l_matnr          TYPE zmatnr,

          l_cart_no        TYPE zcart_no.

    TRY.

        " Let's create a new cart

        zcl_shopping_cart=>start_transaction( 'S' ).

        lo_shopping_cart = zcl_shopping_cart=>get_new_cart( ).

        lo_cart_hdr_db = lo_shopping_cart->get_cart_header( ).

        l_cart_no = lo_cart_hdr_db->get_cart_id( ).

        lo_cart_hdr_db->set_shopper( 'KATAN' ).

        WRITE:/ 'Shopper Set'.

        lo_cart_items = lo_shopping_cart->get_cart_items( ).

        WRITE:/ 'Coffee added to cart'.

        lo_cart_item = lo_cart_items->add_new_item_to_cart( ).

        lo_cart_item_db = lo_cart_item->get_cart_item( ).

        lo_cart_item_db->set_matnr( 'COFFEE' ).

        lo_cart_item_db->set_price( '5.30' ).

        lo_cart_item_db->set_currency( 'AUD' ).

        WRITE:/ 'Bagel added to cart'.

        lo_cart_item = lo_cart_items->add_new_item_to_cart( ).

        lo_cart_item_db = lo_cart_item->get_cart_item( ).

        lo_cart_item_db->set_matnr( 'CHEESE_BAGEL' ).

        lo_cart_item_db->set_price( '9.50' ).

        lo_cart_item_db->set_currency( 'AUD' ).

        WRITE:/ 'Fruit Loaf added to cart'.

        lo_cart_item = lo_cart_items->add_new_item_to_cart( ).

        lo_cart_item_db = lo_cart_item->get_cart_item( ).

        lo_cart_item_db->set_matnr( 'FRUIT_LOAF' ).

        lo_cart_item_db->set_price( '5.50' ).

        lo_cart_item_db->set_currency( 'AUD' ).

        lo_shopping_cart->end_transaction( im_top  = 'X' ).

        WRITE:/ 'Transaction ended'.

        WRITE:/ 'Shopping cart', l_cart_no, 'created'.

        re_cart_no = l_cart_no.

      CATCH zcx_shopping_cart_excp INTO lx_cart_excp.

        l_error_text = lx_cart_excp->get_text( ).

        WRITE:/ l_error_text.

        IF lx_cart_excp->previous IS NOT INITIAL.

          l_error_text = lx_cart_excp->previous->get_text( ).

          WRITE:/ l_error_text.

        ENDIF.

    ENDTRY.

  ENDMETHOD.                    "create_data

  METHOD read_data.

    DATA: lo_shopping_cart TYPE REF TO zcl_shopping_cart,

          lo_cart_hdr_db   TYPE REF TO zcl_shopping_cart_persist,

          lo_cart_items    TYPE REF TO zcl_shopping_cart_items_map,

          lo_cart_item     TYPE REF TO zcl_shopping_cart_item,

          lo_cart_item_db  TYPE REF TO zcl_shopping_cart_item_persist,

          lx_cart_excp     TYPE REF TO zcx_shopping_cart_excp,

          l_error_text     TYPE string,

          lt_item_nos      TYPE string_table,

          l_item_no        TYPE posnr,

          l_matnr          TYPE zmatnr,

          l_cart_no        TYPE zcart_no.

    TRY.

        " Now read the new cart created and output the header and item info

        zcl_shopping_cart=>start_transaction( ).

        l_cart_no = im_cart_no.

        lo_shopping_cart = zcl_shopping_cart=>get_existing_cart( l_cart_no ).

        lo_cart_items = lo_shopping_cart->get_cart_items( ).

        lt_item_nos = lo_cart_items->get_item_no_list( ).

        LOOP AT lt_item_nos INTO l_item_no.

          WRITE:/ l_item_no.

          lo_cart_item = lo_cart_items->get_item_from_cart( l_item_no ).

          lo_cart_item_db = lo_cart_item->get_cart_item( ).

          l_matnr = lo_cart_item_db->get_matnr( ).

          WRITE:/ l_matnr.

        ENDLOOP.

        lo_shopping_cart->end_transaction( im_undo = 'X'

                                           im_top  = 'X' ).

      CATCH zcx_shopping_cart_excp INTO lx_cart_excp.

        l_error_text = lx_cart_excp->get_text( ).

        WRITE:/ l_error_text.

        IF lx_cart_excp->previous IS NOT INITIAL.

          l_error_text = lx_cart_excp->previous->get_text( ).

          WRITE:/ l_error_text.

        ENDIF.

    ENDTRY.

  ENDMETHOD.                    "read_data

ENDCLASS.                    "zcl_shopping_cart_controller IMPLEMENTATION

The results of the report are

Outcomes

The objective of all this was to build a template/concept/method that could be re-used time and time again. The concept of the shopping cart could fundamentally be replaced by any objects persisted in the SAP DB with a foreign key relationship.  Admittedly the time to set this up takes a bit longer than the more traditional approach. However the object services framework provides so much functionality for free. Mix that up with this implementation approach and you have something robust that can cope with change.   

In terms of users working with your application class, I think the user interaction is still quite simple.  Compound that with the added bonus of the automated locking and developers using your code really don’t have much to grumble about.

Once you’ve completed your code don’t forget to share the love.  Tell people what you have done as you should be proud of it.  If people don’t know about it, how do you ever expect anyone to use it?  Or even worse see them re-creating the same or similar logic.

In Summary

Many developers I have worked with are not even aware these services exist and out of those who do know, fewer still would consider using them.  I am not here to debate why they are not using them, merely to express my opinion, as outlined above, as to why I think they should be used. I think you owe it to yourself to at least spend a bit of time to get out there and read a bit around the subject.

3 Comments
Labels in this area