8.2.2 Accessing instances of a Business Object
As we imagine to enter our application, there are typically two different UI-patterns: After a selection-screen, a list of instances matching the criteria is displayed, one row representing one instance of the entity. On the button-bar, we’d be offered to edit an existing instance or to create a new one. Alternatively, we could also just have to enter an identifier which is a human-readable semantical key. The next screen would be used in order to read the current data and edit it or could be used to create an instance with the corresponding ID (optionally with default values).
Identifying instances
As written before, a business object node is the model-part which corresponds to a UML class and thus carries the data of the actual instances. In BOPF, each of these instances is identified with a – tada – GUID. This technical key does not need to be modeled: While generating the combined structure which includes the persistent as well as the (optional) transient information, BOPF also includes a technical structure, the so-called key-include. It contains not only the instances GUID (KEY), but also the PARENT_KEY which is the KEY of the parent node (is initial for root-nodes) as well as the ROOT_KEY (in case of a root-node-instance, the ROOT_KEY and the KEY carry the same values). This key-include is used by the framework in order to resolve compositions as well as their reverse (TO_PARENT) and TO_ROOT, but of course also can be interpreted in business logic.
All semantical data including identifiers are modeled as attribute. Based on these attributes, two core-services exist in order to get the KEYs for an instance: QUERY and CONVERT_ALTERNATIVE_KEY. Once you know the key, you can feed it to the core-service RETRIEVE in order to get the actual data.
Let’s have a look at QUERY first, as it’s the simpler one. A query is a modeled artifact which resides at a node (the “assigned node”). Based upon (multiple) query parameters, a set of instances of the node at which the query resides is being returned (precisely the corresponding KEYs). The query contract allows to apply the well-known select-options for each attribute (including BT and CP). There are two types queries which do not need to be implemented, but which can be answered by the framework itself: The node-attribute-query SELECT_BY_ELEMENTS is a query where the parameters match the persistent node structure, the SELECT_ALL-query is a query without any parameters. Note that the query-names are not unique within the model, but only within the context of the node: A SELECT_BY_ELEMENTS at the ROOT node will return keys of the ROOT-instances, the SELECT_BY_ELEMENTS at the HEAD node will return HEAD-keys matching the criteria (potentially of multiple monsters). All queries adhere to the implied contract and have to support paging as well as the restriction to a set of instances upon which is queried (see parameter “is_query_options”). I believe that “QUERY” feels very familiar for most ABAP developers as it kind of wraps an SQL-query (like a prepared statement). But there is one pitfall when using it in transactional applications: Just like any select-statement, only persisted data can be returned. The transactional buffer (some internal member table which holds the created and changed instances) is ignored. Therefore, I highly recommend to use “QUERY” only from the consumer at the very beginning at a transaction (e. g. on a selection screen or at the beginning of some batch-report). Especially within service provisioning, queries must not be used! The side effects of reading dirty while applying business logic are tricky to identify and mostly horrible to correct.
The core-service CONVERT_ALTERNATIVE_KEY is much less comfortable with respect how to identify instances of a node and needs more modeling, but it respects the transactional buffer! An alternative key in the sense of BOPF is an attribute of a node (or a combination of multiple attributes) which serves to identify an instance either exactly (usually an ID) or in order to identify a set of instances (usually a foreign key). A node may have one or more alternative keys. The definition in the business object comprises its structure as well as its multiplicity (uniqueness). In our sample, the monster number could be a unique alternative key while the creator could be a non-unique alternative key if there was a need to have business logic based on the selection by creator. If for example monsters have got a rental price and for all monsters of a creator the price shall be adjusted, we’d need an alternative key on the creator: Using a query would not find a monster which has been created within the same transaction.
The alternative key’s uniqueness can also be used for validating that no second instance with the same unique alternative key is getting created. In contrast to what Paul wrote, BOPF offers a re-use-feature which ensures the adequate uniqueness: Once you model an alternative key, you are requested to add an action validation (which we’ll cover in a later chapter) with implementation class /BOBF/CL_LIB_V_ALTERNATIVE_KEY. The SAP-provided implementation also ensures uniqueness across multiple sessions on non-persisted data!
Remark: Alternative keys are also necessary in order to be able to model associations between nodes of different business objects (Cross-BO-associations). In this case, the multiplicity of the association has to match the uniqueness of the alternative key.
Reading data
Alright, now we got a set of technical keys of instances which we’d like to process. There are two core-services for reading data: RETRIEVE gets the data of instances of which we know the KEYs. RETRIEVE_BY_ASSOCIATION– surprise, surprise – can retrieve instances (KEYs) of associated nodes. Optionally (not by default!), RETRIEVE_BY_ASSOCIATION also returns the data of the target instances. Both services allow the consumer to specify in which information of the node to be retrieved he’s interested in by specifying the it_requested_attributes. If one of the requested attributes is a calculated one (from the transient part of the node structure), BOPF will execute the corresponding calculation. If no requested attributes is specified, all node-attributes are considered requested. As your models grow (and they will, be sure) and transient information is added and calculated, the use of the requested attributes is getting more and more important. So even if you’re requesting all attributes of the currently modeled nodes, I recommend to specify the attributes which are relevant. This not only saves you nasty performance analysis in the future, but also helps to make your code more readable. Let me give you a short sample:
Monster_manager->retrieve(
EXPORTING
iv_node = zif_monster_c=>sc_node-root
it_key = relevant_monster_keys
it_requested_attributes = VALUE #( zif_monster_c=>sc_node_atttribute-root-number_of_heads )
IMPORTING
et_data = relevant_mosters
).
The above code implies that the number of heads is relevant for the business logic which is about to follow. Also note that a table of moster-keys is being fed into the method. In BOPF, all commands issued by the consumer are mass-enabled. This is particularly important for the retrieval-methods, as each read might result in a DB access (if the buffer is not being hit for all instances). It can scrutinize your system’s performance if you only feed single keys and read with index 1 and do this in a loop. I highly recommend to mass-read all the relevant data (including the necessary associated data) right in the beginning of the method. If you in addition properly fill the requested attributes, 80% of your performance tuning has already been taken care of.
The command for following an association looks very similar:
Monster_manager->retrieve_by_association(
EXPORTING
iv_node = zif_monster_c=>sc_node-root
it_key = relevant_monster_keys
* iv_fill_data = abap_true
* it_requested_attributes = VALUE #( zif_monster_c=>sc_node_atttribute-head-number_of_eyes )
IMPORTING
et_key_link = link_root_head
* et_data = relevant_mosters_heads
).
A careful observer will see that the data of the target node is not always being returned when following an association. The runtime representation of an association is a link between the source and target node. The data is actually a property of the target node and not always necessary in order to implement the requested behavior. As the retrieval of the target node’s data is comparatively expensive (particularly if transient information is requested), the default is not to request the data (iv_fill_data). If you have managed to implement a real-world usecase without ever running into a short-dump because you forgot to set iv_fill_data = abap_true, you are certainly a more careful programmer than I am.
Modifying instances
After we read the current data of an instance, we might want to manipulate it. /BOBF/IF_TRA_SERVICE_MANAGER offers the core-service MODIFY which is a command to execute all kinds of manipulations (Create, Update, Delete). The modify command gets passed a set of modification instructions which might not only affect multiple instances, but also multiple nodes in one call. This is essential, as there might be business logic which validates whether an instance can be created based on subnode-data. E. g. we could validate that each monster needs to have a least one head. Creating a monster without a head would reject the modifications for the failed monster instance.
I will not go into the details of the command (but I recommend you to read the method documentation on the interface which will really help you, the BOPF documentation team did a great job there), but I’ll point you to some specialties.
When creating instances of multiple nodes of a composition, you need to make sure that the instances of the subnode are created for the proper parent-node-instance. In order to be able to do this, you need to know the KEY of the parent node instance. In this case, you can use /bobf/cl_frw_factory=>get_new_key( ) in order to define with which technical identifier the parent node instance shall be created. Else, as a consumer you don’t need to define the key, the framework will do that for you.
Once you update an instance, you can use the changed attributes in order to inform the framework which part of the instance have changed. This not only increases performance (as BOPF doesn’t have to compare the before- and target-data), but also allows you to have multiple modification instructions per instance affecting different attributes.
When deleting an instance, BOPF will implicitly delete the subnodes (via the compositions) as well. There is no need for an explicit deletion of the subnode-instances.
Change- and message-object
Each core-service returns a message container and a change object. It is crucial to understand that in a BOPF-application (such as it should be in any other well-designed application), messages are exclusively intended to be interpreted by a human. Business logic must never be based upon the existence of a particular message-attribute. For this, BOPF calculates a change-object after each roundtrip. This does not only inform about the difference in the transaction before and after the roundtrip, but also informs about failed changes. It may also be the case that during one roundtrip, multiple modifications are being made out of which some are successful and some fail (because they violated some constraint). Thus, if the has_failed_changes( )-method returns abap_true, you definitely have to analyze which change failed!
<-- Back to the first post about general modeling and the unnecessary model class
--> Next: Implicit services for locking authorization management