Improved Inside-Out Modelling
What is inside-out modelling?
It is a design paradigm that takes a business component and models it so that it can be exposed as a service. The entities and properties thereof are generally driven by the component interface. The most common form of inside-out driver is the RFC function module, although BOR objects and others like GENIL are available.
This is in contrast to outside-in model design , where the service that is required is modelled and the appropriate backend components are located or built to serve the consumption model.
The important phrase for me is “to serve the consumption model”; conversely, inside-out design can be paraphrased as “a service imposed by the business model”. Gateway consumers should not really be concerned with or know about the business model; it is generally a lot bigger and more complex than a particular service use case would need.
I’ve discussed that preferred approach here: Outside-in modelling (or “rehab” for RFC addicts).
I am not a huge advocate of inside-out modelling but where it is deemed necessary, it can often be improved to work in a Gateway OData service context.
Let’s take the SAP expenses application & data concept as a starting point. “Expenses” is quite a complex beast; it has not been designed with modular access in mind, it is very monolithic in nature. Despite a revamped UI in Web Dynpro, it hasn’t really altered in the backend logic or model.
Rather than tackle the whole of the expenses component, I’m going to focus on one part – “trip options”. These are essentially the lists of options that you can choose for data entry while completing an expenses form. They are typically used to provide context and fill dropdowns or other value choice controls in a UI. What I found interesting about this part is that it mirrors certain aspects of the expenses component on a macro level.
If you wish to obtain the trip options, you can get the data from the BAPI_TRIP_GET_OPTIONS function.
This function returns 20 tables of various data! Here is a prime example of where inside-out design fails for me – how am I going to map 20 outputs to one entity? Typically one RFC is mapped to an entity to satisfy the READ operation.
At this point I would abandon any hope of providing a well-conceived service and look at the outside-in approach – but more on that later.
Back to the modelling exercise : if I have to do it this way, how do I do it well?
One BAPI, twenty collections
Do I need all 20 tables for my service? Those I don’t need to fetch I can remove from the plan.
For example, I’ll use these:
(In reality I’ll probably need more but 25% is a good cut for an example.)
Now, the approach that the majority of beginners in Gateway will take is to try and send all five of these tables back in one read operation, based on the assumption that simple request/responses can return multiple tables (like a BAPI!).
That is not the way to do it. You can’t do it. Fundamental OData is based on flat structures.
Here’s where the promised improvement starts…
What you do is create five entities with corresponding entitysets (build sets only if required; EMP_INFO for example has a cardinality of 1:1).
Each entity/collection is read by a separate GET request.
This has the following advantages.
- The service entities are decoupled from the backend model; after all they are siblings, not part of a dependency set.
- A collection can be read when it is required, rather than obtained as part of another package of collections. One, a few or all of the entities can be accessed as required without any request constraints.
- More of the collections can be exposed if the service evolves, or conversely, deprecation is easier.
It does carry some disadvantages however;
- The BAPI logic obtains all 20 outputs regardless of those that are required; despite the outputs being optional, pretty much all data is returned.
- Using the BAPI still accesses 100% of the data when I only want 25%.
- Each read of an entityset obtains the other four entitysets again!
In performance terms, where I only want to read five collections with one BAPI call, I am reading 100 (5 calls x 20 tables). Not great!
In this inside-out model, there has to be a balance between the unnecessarily complex and unclear means of implementing a method for obtaining the five collections and the enforced constraint of excessive access.
…And the answer is..?
Moving towards a solution, some of you may ask, ”If we read the EMP_INFO entity with this BAPI, it would have read the other four data tables for the other entities. Why don’t we just store these tables in memory then use them to fill the other sets when required?”
Indeed that would be a good idea; except that the request for EMP_INFO is stateless. If we store the other four tables, they are gone when we try a request for DEFAULTS as a second request, so the BAPI will have to go and read them again, plus the EMP_INFO we already have. And so on for the other collections.
Statefulness can be introduced fairly easily. It is possible to get five entitysets in one request with one BAPI call. The key to this is using the $expand option – when a request is “expanded” the required feeds for the expansion are evaluated in the same connection session, therefore the state is maintained for the duration of the request.
One drawback is that the client needs to know how to place the call in the right way to take advantage of this feature, however the feature is at least available!
The final model design
$expand is commonly used to obtain related entities, however it can be used to chain “unrelated” GET’s into one request as well.
In order to place an expand request, there has to be some relation – but there is no clear relation between the entities, they are not hierarchical.
I’ll now take a technique from the world of outside-in modelling in order to help me realise the model. If the entity design is coming from the outside, it does not have to have a direct (or any!) correlation to a data model on the inside. As long as I can devise a way to place meaningful data in the feed or response, that entity is valid.
What I need is a common relation for all of my five (or even the full 20) chosen entities. This is quite obvious – the entities are all BAPI outputs, so it follows that I should look at the input.
In order to provide those 20 outputs, all I require is an employee number. To properly qualify the context I should also add a date and language, which are optional inputs but can make a difference.
Based on this information, I design an entity called TripContext with properties for employee number, trip date and language – I also make sure they are all keys.
I can then provide an association from my TripContext to each entity and collection in the trip option service.
Because I am going to relate my collections to another entity, I do not pay any attention to the input parameters during the RFC wizard steps. Choosing and realising inputs is another feature of this process that creates a lot of confusion if the RFC is not “mapping friendly”.
I can create all five entities with one import.
Ignore the inputs and choose the five collections from outputs.
Mark a key set within each entity block.
Returned entities need to be renamed (they are upper case and refer to multiples in some cases).
Create the sets.
Create associations from TripContext to each of the options collections.
Finally, assign navigation properties to TripContext. Referential constraints are not required and could not be maintained in any case.
With the right implementation, I can now obtain all of the five sets of options with one request.
The primary entity is the TripContext. The values that I use to GET the entity are actually used to establish the context, i.e. these values become known to the data provider as the initial phase of the request. The entity itself does not exist, it is a “state directive”; it is stating “this is the person in question and this is the date and language that may affect the outcome”. I do not need to access any further data in relation to this entity data, and the returned entity is the same as the request key.
The trick here is that I have now established the input for the following entities that I wish to obtain.
The expand option will locate each of the endpoints of the navigation properties that I defined. In the case of Defaults and TripEmployeeInfo, these are single entity feeds (cardinality 1:1) and the corresponding ‘GET_ENTITY’ method will be called. For the remainder, the corresponding ‘GET_ENTITYSET’ methods will be called.
Data Provider logic
I’ll make some assumptions for my DPC class.
- I’ll only ever want to access a single entity of type TripContext; no collection logic is required.
- I’ll only ever want to access TripContext in order to provide the context for an expanded feed. The return form this request is pointless without an expand option.
- None of the trip options feeds will work unless the TripContext has been established.
- The more expanded elements per request, the more efficient the provider is; however the returned feeds must be required for consumption for this to hold true!
Based on the above assumptions, I can introduce some efficiency.
When TripContext is requested, what is really being requested are the trip options that fit into that context. At this point (TRIPCONTEXT_GET_ENTITY) it would be a good idea to call the BAPI, as we have the input values for it.
There is a slight problem here – the returned data from the BAPI isn’t required just now, the return feed is a TripContext. However I do know that the DPC is going to continue running during this RFC connection; the $expand option is going to invoke calls of the other GET methods.
I’ve got the entity data for those feeds already – so I’m going to store them.
In order to separate the concerns somewhat, I create an access layer class to manage the trip options.
The trip options manager object is then attached to my DPC as an attribute. It reads the BAPI and stores the results in its object space.
When I reach the second (implied by $expand) GET in the process, I can now ask the trip options manager to give me the return data.
I repeat this for each expected feed.
I consider this a much better implementation of an inside-out design. A typical implementation of this service, solely based on the BAPI, would not have been very elegant, efficient or as simple to consume.
What could have been a very inefficient and cumbersome implementation is now well-scaled and fairly simple. In traced tests this service can return the full feed in under 100 milliseconds.
However, it still reads more data than required and could be written even more efficiently using the outside-in modelling approach. I intend to tackle this same scenario in an outside-in manner in a future blog.