ABAP Meshes – when and how to use them
Most knew „mesh“ as a surface material used for sneakers: A mesh is made of fibers woven together. In an ABAP Mesh, the fibers are the records of internal tables, and these are woven together by relations. An ABAP Mesh makes it possible to easily navigate through these relations, e.g. get all items of a header or the header of a specific item.
In this blog post, I want to show how ABAP Meshes can be used in practice, and want to lift the fog which may lying on this topic. It is not intended as a replacement for the very detailed and precise ABAP online help but for everybody who just wants to get started.
Why using Meshes?
As a consultant, I haven‘t seen many Meshes in customer code. To be honest: not even one. Most developers I‘ve spoken to are not sure why they should use a Mesh. Two reasons I’ve heard:
- Meshes look obscure to me and the syntax is confusing. I’ve started to work my way into it but didn‘t finish.
- Meshes don‘t provide groundbreaking new functionality. Anything I can do with a Mesh I can also do with READ TABLEs and LOOP ATs.
Both arguments are not wrong but Meshes are still worth the learning time for the following reasons:
- A Mesh separates the definition of a relation from its use: If e.g. an item record has a field “parent_key” which contains the key of its header record, this is defined only once inside the Mesh. Without a Mesh, this information is repeated in every LOOP where we iterate over the items of a header and in every READ TABLE where we read the header of an item. If there is a change in the data model, in best case only the Mesh definition has to be adjusted but not all the LOOPs and READ TABLEs.
- A Mesh allows often much shorter code, is faster to write, easier to read and less error-prone. We no longer need a bunch of nested LOOPs to muddle through hierarchies. If you rewrite the examples shown in part 2 using the traditional way, and compare the results with the Mesh solution where it is just a one-liner, the difference becomes obvious.
In a way it can be compared to regular expressions: They need some learning effort and everything can be done with a combination of basic string operations but at the end is faster and easier to use a REGEX.
A Mesh type is defined like a structure with the additional keyword MESH, so a Mesh definition always looks like this:
TYPES: BEGIN OF MESH ts_mesh, ... END OF MESH ts_mesh.
As components of this structure type only internal tables and references to internal tables are allowed. In the Mesh context, these internal tables are called „nodes“, but they are still just internal tables, and besides the special Mesh operations we will see in the next sections, we can do anything with them as we can do with an internal table declared outside of a Mesh.
However, the following restrictions must be considered:
- It is only possible to define a Mesh inside the coding, but not in the Data Dictionary. Nevertheless, if it is defined as public member of an ABAP OO Interface, it can be used in a global way.
- A Mesh must be defined as a type first, then a variable can be declared using this type. Therefore, unlike normal structures, it‘s not possible to directly define a Mesh via „DATA: BEGIN OF MESH ls_my_mesh…“.
- The line types of these table types must be structures and they must not contain other internal tables or references (besides strings), i.e. embedded tables are forbidden. Therefore, all tables/references which occur inside a Mesh are direct components of it.
The ability to use references to internal tables is very useful if a Mesh shall be introduced into an existing program but not all the code shall be adapted. So the existing table variables can still be used, but additionally they can be used via the Mesh. For example, if we have a variable “lt_table TYPE tt_table” and a Mesh “ls_mesh” with a component “comp TYPE REF TO tt_table”, via “ls_mesh-comp = REF #( lt_table )” the table is available as a Mesh component.
Of course, we need an example to demonstrate the power of Meshes. My first choice was the „Flight Data Model“ because it is well-known. After I started to build some examples it came out that it was not the best choice, and I decided to create a model which is simple but not so simple that only the standard cases are covered. It‘s a three-level-hierarchy where a head record has 0-to-n item records and an item record has 0-to-n part records. Additionally, an item has a status, and all possible statuses are contained in table status. So, 0-to-n items has 1 status.
TYPES: BEGIN OF ts_head, db_key TYPE string, END OF ts_head, BEGIN OF ts_item, db_key TYPE string, parent_key TYPE string, "relates to a head record status TYPE string, "relates to a status record END OF ts_item, BEGIN OF ts_part, db_key TYPE string, parent_key TYPE string, "relates to an item record amount TYPE i, END OF ts_part, BEGIN OF ts_status, stat_key TYPE string, description TYPE string, END OF ts_status, tt_head TYPE SORTED TABLE OF ts_head WITH UNIQUE KEY db_key, tt_item TYPE SORTED TABLE OF ts_item WITH UNIQUE KEY db_key WITH NON-UNIQUE SORTED KEY key_par COMPONENTS parent_key, tt_part TYPE SORTED TABLE OF ts_part WITH UNIQUE KEY db_key WITH NON-UNIQUE SORTED KEY key_par COMPONENTS parent_key, tt_status TYPE SORTED TABLE OF ts_status WITH UNIQUE KEY stat_key.
Like in a real scenario, we use sorted tables and an additional secondary sorted key (of course non-unique) on the parent_key fields for performance reasons.
One side note concerning the way the hierarchy was defined:
- As the flight data model was designed, it was standard that database tables use its semantic key as database key. So the table SCARR – which contains the carriers – has a key field CARRID (with values like „LH“ for Lufthansa), and the table SFPLI – which contains the connections of the carriers and is a direct child of SCARR – inherits the key of its parent (i.e. CARRID) and adds an own key field (CONNID). This scheme – take the key fields of the parent and add an own one – is also used for the next hierarchy levels.
- In data models designed at a later time all tables use just a single field as database key (typically a GUID), and a child references its parent via an attribute typically named parent_key.
The example above uses the second variant because it‘s the newer one and because it allows to deal with secondary keys in Meshes (the „old“ model does not require them).
To be useful, a Mesh usually must contain at least two internal tables (two nodes), and there must be relation between them. If we take the two nodes head and item of the example, the relation is obviously „head-db_key = item-parent_key“.
Such a relation R is always symmetric: Using R, we can navigate from a head record to the related item record(s), and we can navigate from an item record to the related head record.
In the Mesh world, relations are named „Associations“. They are not defined as stand-alone entities but bound to a node. Each node can have 0 to n associations, and each association defines how the records in its own node (the source node) are related to the records in another node (the target node) of this Mesh. Therefore an association is not symmetric as it goes from a source to a target.
Associations consists of three (optional four) parts:
TYPES: BEGIN OF MESH ts_mesh, node0 TYPE tt_tab_type ASSOCIATION assoc1 TO node1 ON … [USING KEY …] ASSOCIATION assoc2 TO node2 ON … [USING KEY …], ... END OF MESH ts_mesh.
These parts are:
- By convention associations should start with an underscore and contains the name of the target node, so typical names are „_to_target“ or just „_target“.
- If a node has several associations, of course these must have different names, but it is not required that the association names are unique in the entire Mesh.
- Target Node: Name of a node in the same Mesh
- Relation condition: A relation is made of AND-combined pairs “fieldname in target node = fieldname in source node”. Special attention must be paid to the field order: Though the source node is denoted first and the target node is second, the field order in the relation condition is vice-versa. This doesn‘t seem intuitive but the advantage is that the target fieldnames are listed directly after the target node name.
- Secondary key (optional): If the target node has a secondary key which contains of at least one field of the relation condition, for performance reason is recommended to use it.
Please keep in mind that secondary keys are not used automatically but require an explicit designation. Though this is sometimes tiresome, it guarantees that adding a new secondary key to an existing table type has no side effects: Making a READ TABLE with or without a secondary key results in general to a different SY-TABIX value, and if this index is used at a later time to read the record again and here the secondary key is not specified – and the compiler has no reason to use a certain secondary key for an index access – a different record is read.
Of course, it is possible to define two or more associations which all go from the same source node to the same target node, but have different conditions and/or use different secondary keys.
So, the example above can be written in three ways:
- We define the relation at both nodes:
TYPES: BEGIN OF MESH ts_mesh, head TYPE tt_head ASSOCIATION _item TO item ON parent_key = db_key USING KEY key_par, item TYPE tt_item ASSOCIATION _head TO head ON db_key = parent_key, END OF MESH ts_mesh.
- We just define the relation at head, pointing to item:
TYPES: BEGIN OF MESH ts_mesh, head TYPE tt_head ASSOCIATION _item TO item ON parent_key = db_key USING KEY key_par, item TYPE tt_item, END OF MESH ts_mesh.
- We just define the relation at item, pointing to head:
TYPES: BEGIN OF MESH ts_mesh, head TYPE tt_head, item TYPE tt_item ASSOCIATION _head TO head ON db_key = parent_key, END OF MESH ts_mesh.
Only Option (1) seems complete, but usually it‘s only necessary to define a relation at one node, and the reason is the existence of reverse associations.
For each association defined in the Mesh, the runtime automatically generates the reverse association, i.e. it swaps source and target node and correspondingly the field order in the relation condition. Such a „mirrored“ association has the same name as the original, but „^“ as prefix and „~“ followed by the new target node (the original source node) as suffix.
This suffix is required to guarantee the uniqueness of the association names: For example, if the nodes „head“ and „item“ both have a status field and both an association named „_to_status“, the status node will get two reverse associations:
Without the suffix, there would be a naming conflict.
For the sake of completeness, it should be mentioned that also the „normal“ associations have the name of its target node as suffix. E.g. in option (1), the full name of the association “_item” is “_item~item”. Because this suffix is not needed, neither technically (the association names of one node are always unique) nor for readability (by convention the association name contains the target name) it is in general not used.
If we virtually complete the options (2) and (3) with the generated reverse associations, it will look like this:
BEGIN OF MESH ts_mesh, head TYPE tt_head ASSOCIATION _item TO item ON parent_key = db_key USING KEY key_par, item TYPE tt_item ASSOCIATION ^_item~head TO head ON db_key = parent_key, END OF MESH ts_mesh.
BEGIN OF MESH ts_mesh, head TYPE tt_head ASSOCIATION ^_head~item TO item ON parent_key = db_key, item TYPE tt_item ASSOCIATION _head TO head ON db_key = parent_key, END OF MESH ts_mesh.
So option (2) provides the same functionality as option (1), only the names are different (“_head” vs. “^_item~head”), therefore there is no need for option (1).
Option (3) is not exactly the same as option (1) because the USING KEY is missing. Unfortunately, there is no way to specify it inside the Mesh because there is no addition like USING KEY … FOR REVERSE. Though it is possible to specify a secondary key during the usage of the Mesh, it‘s better to have it centrally in the Mesh definition.
This leads to the following rule of thumb:
- If a relation requires a secondary key for one node but not for the other, define the association where the key is required, and use the reverse association for the other direction
- If both associations require a secondary key, define both and don‘t use the reverse associations.
Of course, an additional association does not cost extra: If you find reverse associations hard to read, feel free to define a relation at both nodes even if this is technically not required.
Now we have everything we need to define a Mesh (using the type definitions from above):
TYPES: BEGIN OF MESH ts_mesh, head TYPE tt_head ASSOCIATION _item TO item ON parent_key = db_key USING KEY key_par, item TYPE tt_item ASSOCIATION _part TO part ON parent_key = db_key USING KEY key_par ASSOCIATION _status TO status ON stat_key = status, part TYPE tt_part, status TYPE tt_status, END OF MESH ts_mesh. DATA ls_mesh TYPE ts_mesh.
If you want to execute the examples shown in the next sections in your system you can fill some data in:
ls_mesh = VALUE #( head = VALUE #( ( db_key = 'H1' ) ( db_key = 'H2' ) ) item = VALUE #( ( db_key = 'I1' parent_key = 'H1' status = 'O' ) ( db_key = 'I2' parent_key = 'H1' status = 'D' ) ( db_key = 'I3' parent_key = 'H2' status = 'C' ) ( db_key = 'I4' parent_key = 'H1' status = 'O' ) ) part = VALUE #( ( db_key = 'P1' parent_key = 'I1' amount = 10 ) ( db_key = 'P2' parent_key = 'I3' amount = 20 ) ( db_key = 'P3' parent_key = 'I3' amount = 30 ) ( db_key = 'P4' parent_key = 'I4' amount = 40 ) ) status = VALUE #( ( stat_key = 'O' descript = 'open' ) ( stat_key = 'C' descript = 'canceled' ) ( stat_key = 'D' descript = 'done' ) ) ).
How to use a Mesh
After we have now defined a Mesh, it’s time to use it.
Paths vs. Path Expressions
Roughly said, a path is a node followed by a sequence of associations and is the central element to use a Mesh. The most confusing thing about paths is that their behavior depends on where they are used:
- If a path is used in a LOOP AT, within the functional equivalent FOR … IN or within a few changing statements listed in part 3, it behaves like an internal table (as expected), but
- if a path is moved into a variable, passed as parameter to a method or function module, or used in any other operand position, it behaves like a structure.
If a path is used in the second way, it is called a „path expression“. Path expressions are a variant of table expressions and they don‘t allow all options which can be used for paths. We will start with paths and then continue with path expressions.
An initial path is the simplest form of a path and written as “Mesh-StartNode\Association[ StartStructure ]”. It is built from three parts:
- Mesh-StartNode: The name of a variable with a Mesh type and name of the node we want to start. It can be any node of the Mesh. Especially, if we have a multi-level-hierarchy, it is not required that it is the root node of this hierarchy.
- Start Structure: A structure variable which has the line type of the start node
- Association: The name of an association which is defined for the start node
Such a path evaluates into an internal table which contains all entries of the target node which fulfill the association condition regarding the values contained in the start structure.
For example, if ls_head is a variable of type ts_head, the result of ls_mesh-head\_item[ ls_head ] are all records contained in table ls_mesh-item which parent_key equals ls_head-db_key, so in short: All items of ls_head.
This simple statement has some confusion potential:
- There is only one reason why the start node has to be specified: so that the system can uniquely identify the association. The system does not access the content of the start node, i.e. the internal table ls_mesh-head is not read. It is not required that the record ls_head exists in table ls_mesh-head, and the system does not check this. In fact, the content of ls_mesh-head is fully irrelevant for this path.
- In reality, of course, the content of the start structure isn‘t arising from nothing but typically taken from a mesh node, and then no helper variable like ls_head is used, but it is read via a table expression and directly passed to the path. Therefore, in programs using Meshes, you usually find statements like ls_mesh-head\_item[ ls_mesh-head[ field1 = value1 … ] ].
- It may irritate that the structure variable has to placed behind the association and not behind the start node, i.e. that this path is not written as ls_mesh-head[ ls_head ]\_item[ ]. This would make it clearer that the start is ls_head and then the association _item is evaluated. However, this syntax has already a different meaning: It is an index access to table ls_mesh-head where ls_head is expected to contain an integer.
Though only those fields of ls_head are needed which are defined in the association (here the field db_key) always the complete structure has to be supplied. This is only consequent because if the Mesh definition is changed and other fields from ls_head are required, passing the complete structure guarantees that all these values are available.
If not all target records are needed an optional WHERE condition can be specified, exactly like if it is done in a LOOP statement. Adding the WHERE clause to the LOOP statement itself is not allowed.
Example: Get all items below ls_head with status “O”:
LOOP AT ls_mesh-head\_item[ ls_head WHERE status = 'O' ] ASSIGNING FIELD-SYMBOL(<ls_item>). WRITE: / <ls_item>-db_key. ENDLOOP.
Additionally, the secondary key defined in the Mesh can be overwritten by adding a WITH KEY keyname, e.g.
LOOP AT ls_mesh-head\_item[ ls_head USING KEY primary_key WHERE status = 'O' ] ...
In this case, the syntax check warns us: „The secondary key “KEY_PAR” is specified in full. However, the primary key is used to access the table. Check whether access using “KEY_PAR” is more efficient“. Ignoring performance, this can be useful if the output is required in the order of the key keyname.
An initial path always starts with a single structure and evaluates to a subset of the records contained in the target node. If the target node also has at least one association, the path can be extended by adding it. Here no start structure can be specified because these are taken from the result of the initial path. Therefore we have a syntactical difference between the first association and the others: The first needs a structure as input and the others neither require nor allow it.
For example, “ls_mesh-head\_item[ ls_head ]\_part[ ]” returns all parts belonging to all items which themselves belong to ls_head.
This example could easily be misconstrued to mean that the system evaluates Mesh paths by creating nested LOOPs. Indeed, it is quite easier: A Mesh Path evaluation is a sequence of filter steps, where a subset of source node records goes in and – by applying the relation condition – a duplicate-free subset of target node records comes out, and this sequence starts with a single structure variable coming from outside as first source record. After the last association was evaluated, the subset of records in its target node is called the „result“ of the path.
As a consequence, the result can never have duplicates. For example, if ls_head has 3 items, 2 with status „open“, 1 with status „done“ and none with status „canceled“, the path “ls_mesh-head\_item[ ls_head ]\_status[ ]” delivers two status records: „open“ and „done“:
Like the initial path, inside the brackets an optional WHERE clause can be specified, e.g.
“ls_mesh-head\_item[ ls_head ]\_part[ WHERE amount > 10 ]”.
Reverse paths can be used in exactly the same way. For example, for an item ls_item, we want to get
- the header:
ls_mesh-item\^_item~head[ ls_item ]
- all its siblings, including the item itself:
LOOP AT ls_mesh-item\^_item~head[ ls_item ]\_item[ ] INTO …
- all its siblings, but without the item itself:
LOOP AT ls_mesh-item\^_item~head[ ls_item ]\_item[ WHERE db_key <> ls_item-db_key ] INTO …
Our example used so far is a three-level-hierarchy where each hierarchy level is stored in a separate table. If we need a hierarchy with an unrestricted number of levels, records of all levels must be stored in the same table. Typically this table contains an attribute which is of the same type as its key and this contains the parent record. Examples for such tables are
- TDEVC (table of development packages): Key field is DEVCLASS, and attribute PARENTCL (which is also a DEVCLASS) contains the parent package name
- USR02 (table of users): Key field is BNAME, and attribute ANAME (which is also a BNAME) contains the user who has created this user
To traverse such hierarchies, we need an association where source and target node are identical. Such an association is called a self- or reflexive association.
Example using table USR02:
TYPES: tt_user TYPE SORTED TABLE OF usr02 WITH UNIQUE KEY bname WITH NON-UNIQUE SORTED KEY key_aname COMPONENTS aname, BEGIN OF MESH ts_mesh, user TYPE tt_user ASSOCIATION _creator TO user ON bname = aname ASSOCIATION _has_created TO user ON aname = bname USING KEY key_aname, END OF MESH ts_mesh. DATA ls_mesh TYPE ts_mesh.
As described in „Associations“, it‘s only necessary to create one association because the other is already available as the reverse: Instead of “_creator”, we could use “^_has_created~user”. Here we defined both because of better readability (by the way: associations for the same node are not separated by a comma!)
Next, we fill the Mesh node with some test data:
ls_mesh-user = VALUE #( ( bname = 'ROOT' aname = 'ROOT' ) ( bname = 'USER-A-L1' aname = 'ROOT' ) ( bname = 'USER-B-L1' aname = 'ROOT' ) ( bname = 'USER-C-L2' aname = 'USER-A-L1' ) ( bname = 'USER-D-L2' aname = 'USER-A-L1' ) ( bname = 'USER-E-L3' aname = 'USER-C-L2' ) ( bname = 'USER-F-L3' aname = 'USER-C-L2' ) ( bname = 'USER-G-L3' aname = 'USER-D-L2' ) ( bname = 'USER-H-L4' aname = 'USER-E-L3' ) ( bname = 'USER-I-L4' aname = 'USER-E-L3' ) ).
For e.g. user USER-C-L2, we want to get
- the user who has created him
- all the users who are created by USER-C-L2
FIELD-SYMBOLS <ls_user> TYPE usr02. DATA(ls_myuser) = ls_mesh-user[ bname = 'USER-C-L2' ]. WRITE: / 'Source user:', ls_myuser-bname. WRITE: / 'Creator....:'. WRITE ls_mesh-user\_creator[ ls_myuser ]-bname. WRITE: / 'Has created:'. LOOP AT ls_mesh-user\_has_created[ ls_myuser ] ASSIGNING <ls_user>. WRITE <ls_user>-bname. ENDLOOP.
Source user: USER-C-L2 Creator....: USER-A-L1 Has created: USER-F-L3 USER-E-L3
Please note that “ls_mesh-user\_creator[ ls_myuser ]” is a path expression (a path not used with LOOP or FOR). This make sense here because a user can only have one creator (see next section for details).
Now we don‘t only want the direct creator, but also the user who has created the creator and so on (the complete path up to the root), and accordingly the complete sub-tree of created users.
With Meshes, this is extremely simple: We don‘t have to write a recursion but just had to put a „*“ or „+“ behind the association name. „+“ excludes the start record, „*“ includes it (in this case the record must be contained in the table, otherwise the result is empty).
This leads to an iterative evaluation which stops automatically if no more records are found. So we slightly change our program to:
WRITE: / 'Creator....:'. LOOP AT ls_mesh-user\_creator+[ ls_myuser ] ASSIGNING <ls_user>. WRITE <ls_user>-bname. ENDLOOP. WRITE: / 'Has created:'. LOOP AT ls_mesh-user\_has_created+[ ls_myuser ] ASSIGNING <ls_user>. WRITE <ls_user>-bname. ENDLOOP.
Source user: USER-C-L2 Creator....: USER-A-L1 ROOT Has created: USER-F-L3 USER-E-L3 USER-I-L4 USER-H-L4
We see: Though the creator of ROOT is ROOT himself, we don‘t run into an endless loop.
You may implement this without a Mesh and compare the amount of code lines.
If we execute
DATA(ls_head) = ls_mesh-head[ db_key = 'H1' ]. LOOP AT ls_mesh-head\_item[ ls_head ]\_part[ ] ASSIGNING FIELD-SYMBOL(<ls_part>). WRITE: / <ls_part>-db_key. ENDLOOP.
we get a list of all parts connected to the header record (via its items). But if we execute
ASSIGN ls_mesh-head\_item[ ls_head ]\_part[ ] TO FIELD-SYMBOL(<var>).
and check the type of “<var>” in the debugger, it is surprisingly only a structure (of type ts_part), and it just contains the first record written by the LOOP.
The reason is that a path not used in LOOP or FOR is a „Path Expression“ (not to be confused with a „Path Extension“), and these are evaluated in a different manner: After an association was applied, from the resulting table only the first record is used as the source record for the next association. This reduction to a single record does not only apply at the end but for every result in the middle if the path has more than one association.
This behavior limits the practical use of path expressions: In general, only if because of the data model the result sets cannot have more than one record, they make sense.
Path expressions are a variant of table expressions. They cannot have a WHERE clause inside the brackets but only an optional condition like „fieldname1 = value1 fieldname2 = value2 ….“. Then – instead of the first – the record is taken which matches this condition. This is the same already known from LOOP and READ TABLE: A LOOP can have a condition like “fieldname > value”, but a READ TABLE only “fieldname = value”.
Be aware that if a result set is constructed using a secondary key, e.g. like “ls_mesh-head\_item[ ls_head ]” which uses “key_par” of the item table according to the Mesh definition, it‘s not possible to add such an additional „fieldname = value“ condition. Reason is that it is not allowed to „overspecify“ the secondary key: Expressions like
- ASSIGN ls_mesh-head\_item[ ls_head status = ‘O’ ] TO FIELD-SYMBOL(<var1>) or
- ASSIGN ls_mesh-head\_item[ ls_head ]\_part[ value = 10 ] TO FIELD-SYMBOL(<var2>)
leads to a syntax error because the component (status resp. value) is not contained in the secondary key.
It‘s possible that the internal table from which the record should be taken is empty. In this case the same happens as if we try to access an non-existent entry via a table expression, e.g. “lt_itab[ db_key = ‘not-existing’ ]”: The exception CX_SY_ITAB_LINE_NOT_FOUND occurs. We can handle this via a TRY … CATCH or we put a VALUE #( … OPTIONAL) around the path expression.
Example Mesh for a n-to-m relation
Let’s look at a more complex example: We have materials, plants and an n-to-m-relation between these two entities. Our task is, for a specified material X, to get all materials which are produced in a plant where also X is produced.
According to the example data in the following demo report, material M1 is produced in plant P1 and P3. So we want all materials produced in P1 or P3, i.e. M2 and M3, but not M4.
REPORT zmeshdemo. TYPES: tt_mara TYPE SORTED TABLE OF mara WITH UNIQUE KEY matnr, tt_plant TYPE SORTED TABLE OF t001w WITH UNIQUE KEY werks, tt_marc TYPE SORTED TABLE OF marc WITH UNIQUE KEY matnr werks, BEGIN OF MESH ts_mesh, mara TYPE tt_mara ASSOCIATION _marc TO marc ON matnr = matnr, plant TYPE tt_plant ASSOCIATION _marc TO marc ON werks = werks, marc TYPE tt_marc, END OF MESH ts_mesh. DATA ls_mesh TYPE ts_mesh. ls_mesh = VALUE #( mara = VALUE #( ( matnr = 'M1' ) ( matnr = 'M2' ) ( matnr = 'M3' ) ( matnr = 'M4' ) ) plant = VALUE #( ( werks = 'P1' ) ( werks = 'P2' ) ( werks = 'P3' ) ( werks = 'P4' ) ) marc = VALUE #( ( matnr = 'M1' werks = 'P1' ) ( matnr = 'M1' werks = 'P3' ) ( matnr = 'M2' werks = 'P1' ) ( matnr = 'M2' werks = 'P2' ) ( matnr = 'M3' werks = 'P1' ) ( matnr = 'M3' werks = 'P3' ) ( matnr = 'M4' werks = 'P2' ) ( matnr = 'M4' werks = 'P4' ) ) ). LOOP AT ls_mesh-mara INTO DATA(ls_mara). WRITE: / 'Materials produced in same plant as', ls_mara-matnr. LOOP AT ls_mesh-mara\_marc[ ls_mara ]\^_marc~plant[ ]\_marc[ ]\^_marc~mara[ WHERE matnr <> ls_mara-matnr ] ASSIGNING FIELD-SYMBOL(<ls_mara>). WRITE: / <ls_mara>-matnr. ENDLOOP. ENDLOOP.
Materials produced in same plant as M1 M2 M3 Materials produced in same plant as M2 M1 M3 M4 Materials produced in same plant as M3 M1 M2 Materials produced in same plant as M4 M2
The core of this program is the path
ls_mesh-mara\_marc[ ls_mara ]\^_marc~plant[ ]\_marc[ ]\^_marc~mara[ WHERE matnr <> ls_mara-matnr ]
which looks difficult at first, but if we break it down into pieces it becomes clearer:
- We start with the material in ls_mara (filled in the LOOP with each material), and retrieve all <plant, material> combinations for this single material:
ls_mesh-mara\_marc[ ls_mara ]
- We strip the material and take only its plants by navigating to the plant node:
- For these plants, we retrieve all <material, plant> combinations:
- From these <material, plants> combinations, we take only the materials by navigating to the material node:
- The source material itself shall be excluded from the list, so we add a WHERE clause:
…\^_marc~mara[ WHERE matnr <> ls_mara-matnr ]
If e.g. we only want plants located in „Walldorf“ and only materials created by our user (this of course requires that these fields are filled) we can simply write:
ls_mesh-mara\_marc[ ls_mara ]\^_marc~plant[ WHERE ort01 = ‘Walldorf’ ]\_marc[ ]\^_marc~mara[ WHERE ernam = sy-uname ]
You may write this last statement without a Mesh and compare the amount of source code needed.
Modifying the content of Mesh nodes
Paths behave similarly to internal tables, but they are not identical. For example, it is unfortunately not possible to LOOP over a path and use the GROUP BY addition. Therefore, we cannot replace an internal table with a path in every statement. For INSERT, DELETE and MODIFY this is possible, but with a different logic.
As already mentioned, besides these Mesh-specific variants, it’s also possible to use the well-known variants of INSERT, DELETE and MODIFY for changing the content of Mesh nodes because these are regular internal tables.
Inserting into Mesh nodes
The well-known statement “INSERT [record|LINES OF itab1] INTO TABLE itab” inserts one record or the lines of itab1 into the table itab. Both variants can be used in combination with paths, but for simplicity we only use the record variant.
This statement translates into “INSERT record INTO TABLE path“, which inserts record into the last node of the path. The rules concerning duplicate key handling are unchanged.
Inserting using an initial path
If the path is an initial path, i.e. a node with a start structure and only one association, the specified record is inserted into the target node of this association. The only difference to a „normal“ INSERT is that those fields with are defined by the ON condition are taken from the start structure.
Example: After executing the following statements, the table ls_mesh-item has a new entry with the values db_key = ‘NEW’ and parent_key = ‘H1’. The value specified for parent_key in the inserted row is ignored because the ON condition of the association “_item” defines that item-parent_key must be head-db_key.
DATA(ls_head) = VALUE ts_head( db_key = 'H1' ). DATA(ls_item) = VALUE ts_item( db_key = 'NEW' parent_key = 'IGNORED' ). INSERT ls_item INTO TABLE ls_mesh-head\_item[ ls_head ].
It‘s not allowed to place a WHERE clause into the brackets of “_item[ ]” just because is makes no sense to restrict the table content: We are only adding a record to this table but don‘t read it.
Inserting using a Path Extension
If an extended path is used, the logic is a bit different because in general we have more than just the single start record:
- The system evaluates the path up to the node S before the last node L (so S is the second last node), and therefore determines a subset of records in node S
- For every record s in this subset, a new record is inserted into node L:
- The fields defined by the ON-condition of the association which leads from S to L are filled from record s,
- all other fields are taken from the record specified in the INSERT statement.
Consequently inserting a record into an extended path adds in general more than one record into the last node. This can be useful for hierarchies like the “Flight Data Model” where a table on hierarchy level n has n key fields (see section „Example“). Because in our example the key is a single field, inserting more than one record will always lead to a duplicate-key-error. To execute the statement shown in the following picture, it‘s necessary to change the definition in tt_part to a non-unique sorted key.
Like in the initial path scenario, it does not make sense to use a WHERE clause in the last association, but in all before. This can reduce the selected records in the node before the last node and therefore the number of records inserted into the last node.
Deleting from Mesh nodes
For deleting there are two variants: DELETE path and DELETE TABLE path [table_key].
This is in general the more useful variant. It deletes all lines in the last node which path specifies (the “result” of the path), so all rows are deleted over which a “LOOP AT path” would run. All options for paths (WHERE and USING KEY) can be used.
A typical task is to delete a record and all its direct and indirect children. If we have a self association this is easy because the path generates all records to be deleted. Referring to the example in section „Self-Associations“, the statement “DELETE ls_mesh-user\_has_created+[ ls_myuser ]” will delete all users which are directly or indirectly created by user ls_myuser.
In our three-level-hierarchy, we need three DELETEs (the top record itself has to deleted with the non-mesh-variant). For example, to delete all items and parts of head record “H1” (including himself) we can write:
DATA(ls_head) = ls_mesh-head[ db_key = 'H1' ]. DELETE ls_mesh-head\_item[ ls_head ]\_part[ ]. DELETE ls_mesh-head\_item[ ls_head ]. DELETE TABLE ls_mesh-head FROM ls_head.
DELETE TABLE path [table_key]
This variant is only useful in very special cases. It is only allowed for initial paths (path with only one association) and WHERE and USING KEY are not allowed inside the association. It deletes one (or no) record in the target node of the initial association. Which record is deleted is constructed by the start structure, the ON condition of the association and the table_key.
For evaluating the ON condition, the system always uses a key. For example, “ls_mesh\_item[ ls_head ]” selects the items by using the secondary key “key_par” of the item table because this is specified in the Mesh definition. If the ON condition covers all fields of this key (which is fulfilled here), the “table_key” is not used and it is not necessary to specify it. Then the first found record is deleted. Even if the key is non-unique (like in our example), still only one record is deleted.
DELETE TABLE ls_mesh-head\_item[ ls_head ].
The system deletes the first record in ls_mesh-item which a LOOP over this path will process.
DATA(ls_item) = VALUE ts_item( db_key = 'I1' ). DELETE TABLE ls_mesh-head\_item[ ls_head ] FROM ls_item.
The same record is deleted as in example 1. The system uses the key “key_par” to access the item node, this key consists only of field “item-parent_key”, and this is filled from “ls_head-db_key”. No field of ls_item is used, so it‘s completely ignored.
If the ON condition does not cover all fields of the used key, the missing fields are taken from table_key. In our example we can force this by explicitly specify a key which overrides the key defined in the Mesh.
DELETE TABLE ls_mesh-head\_item[ ls_head ] FROM ls_item USING KEY primary_key.
Now the item node is accessed via its primary key which is a sorted key on field db_key. No fields of this key is supplied via the ON condition, so the content of ls_head is irrelevant, and the item with db_key = ls_item-db_key is deleted. Of course, the same can be done quite easier via DELETE TABLE ls_mesh-item FROM ls_item.
Modifying the content of Mesh nodes
For MODIFY we also have two variants: MODIFY path FROM wa and MODIFY TABLE path FROM wa. They behave the same way as the DELETE variants, just changing the selected records using the values in structure wa instead of deleting them.
MODIFY path FROM wa [TRANSPORTING field1 field2 … ]
It modifies all lines in the last node which the path specifies, so all rows over which a “LOOP AT path” would run are changed. All options for paths (WHERE and USING KEY) can be used.
The following fields of these lines are not changed (the values contained in wa for these fields are ignored):
- fields of the primary key if it is a sorted or hashed key
- fields of the secondary key which is used for access via the ON condition (only if a secondary key is used)
MODIFY ls_mesh-head\_item[ ls_head ] FROM ls_item.
All items belonging to ls_head, i.e. all item records with parent_key = ls_head-db_key, are changed from the fields of ls_item besides item-db_key (because of 1) and item-parent_key (because of 2). If these fields are explicitly specified behind TRANSPORTING a syntax error occurs.
MODIFY TABLE path FROM wa [TRANSPORTING field1 field2 … ]
Like in DELETE TABLE, this is a quite special statement: It is only allowed for initial paths (path with only one association) and WHERE and USING KEY are not allowed. It modifies one (or no) record in the target node of the initial association. Which record is changed is constructed by the start structure, the ON condition of the association and the structure wa.
If the ON condition covers all fields of the key used to access the target node, no field from structure wa is used to identify the record which will be changed. If the ON condition does not cover all fields of the used key, the missing fields are taken from wa. The unchangeable fields are the same as in the MODIFY path variant.
SET ASSOCIATION path = wa
This statement is exclusive for Meshes. As always, the behavior depends if path is an initial or an extended path.
If path is an initial path, it sets the fields in the start structure to the values from structure wa according to the ON condition in the mapping.
Example: After execution of
SET ASSOCIATION ls_mesh-head\_item[ ls_head ] = ls_item.
the value of field ls_head-db_key is filled from ls_item-parent_key (the content of the Mesh node “head” is not changed).
It does not make much sense that a child is changing the key of its parent. More useful is the opposite:
- We use the reverse association “^_item~head”, so we can set the parent_key of a (newly inserted) child to the key of its parent node
- To change the mesh table and not only a local variable, we use a field-symbol <ls_item> which points to the (newly inserted) record contained in ls_mesh-item.
SET ASSOCIATION ls_mesh-item\^_item~head[ <ls_item> ] = ls_head
provides what is described above. However, the same can be achieved easier using the “INSERT … INTO TABLE path” statement.
If path is an extended path, the system evaluates the path until the node S before the last node L, so we have a subset of records of node S. In all these records, using the mapping of the ON condition in the last association, those fields are changed to the values contained in wa. The content of node L doesn‘t matter.
Example: After execution of
SET ASSOCIATION ls_mesh-head\_item[ ls_head ]\_status[ ] = ls_status.
in all items which belong to ls_head the field “status” is set to the value ls_status-stat_key (all other fields are unchanged).
It‘s possible to use a WHERE clause besides in the last association, for example to change only the status only of those items where it has value ‘O’
SET ASSOCIATION ls_mesh-head\_item[ ls_head WHERE status = 'O' ]\_status[ ] = ls_status.
Instead of using a structure which has the type of the last node L, it‘s also possible to use one with the type of the second last node S. Then the statement must be changed to “SET ASSOCIATION path LIKE wa“.
When working with hierarchical data or in general with tables whose contents have relationships, it is always advisable to take a Mesh into account. It may result in shorter code with fewer possibilities to make errors. Since the internal tables inside the Mesh can be read and modified like any other internal table, there is no danger of running into a dead end.
Very well summarized and demonstrated. Thank you
I had always wondered about how and where meshes should be used.
Initially I just could nit see the point.
Now I realise that there is meshes in your madness.
A very nice article. Well explained and demonstrated. Thank you!
Another one to bookmark.
The examples in the paragraph Associations at the end with "ts_mesh" are missing the MESH keyword:
Thanks, you're right - I've added it.
Quite a detailed article !!
While doing hands on for some reason I am getting read failed with sy-subrc eq 4. Could you please help here?
BEGIN OF ts_head,
db_key TYPE string,
END OF ts_head,
BEGIN OF ts_item,
db_key TYPE string,
parent_key TYPE string, "relates to a head record
status TYPE string, "relates to a status record
END OF ts_item,
BEGIN OF ts_part,
db_key TYPE string,
parent_key TYPE string, "relates to an item record
amount TYPE i,
END OF ts_part,
BEGIN OF ts_status,
stat_key TYPE string,
descript TYPE string,
END OF ts_status,
tt_head TYPE SORTED TABLE OF ts_head WITH UNIQUE KEY db_key,
tt_item TYPE SORTED TABLE OF ts_item WITH UNIQUE KEY db_key
WITH NON-UNIQUE SORTED KEY key_par COMPONENTS parent_key,
tt_part TYPE SORTED TABLE OF ts_part WITH UNIQUE KEY db_key
WITH NON-UNIQUE SORTED KEY key_par COMPONENTS parent_key,
tt_status TYPE SORTED TABLE OF ts_status WITH UNIQUE KEY stat_key.
BEGIN OF MESH ts_mesh,
head TYPE tt_head ASSOCIATION _item TO item ON parent_key = db_key
USING KEY key_par,
item TYPE tt_item ASSOCIATION _part TO part ON parent_key = db_key
USING KEY key_par
ASSOCIATION _status TO status ON stat_key = status,
part TYPE tt_part,
status TYPE tt_status,
END OF MESH ts_mesh.
DATA: ls_mesh TYPE ts_mesh,
ls_part TYPE tt_part,
ls_status TYPE tt_status,
ls_head TYPE ts_head,
ls_item TYPE ts_item.
ls_mesh = VALUE #(
head = VALUE #(
( db_key = 'H1' ) ( db_key = 'H2' )
item = VALUE #(
( db_key = 'I1' parent_key = 'H1' status = 'O' )
( db_key = 'I2' parent_key = 'H1' status = 'D' )
( db_key = 'I3' parent_key = 'H2' status = 'C' )
( db_key = 'I4' parent_key = 'H1' status = 'O' )
part = VALUE #( ( db_key = 'P1' parent_key = 'I1' amount = 10 )
( db_key = 'P2' parent_key = 'I3' amount = 20 )
( db_key = 'P3' parent_key = 'I3' amount = 30 )
( db_key = 'P4' parent_key = 'I4' amount = 40 )
status = VALUE #( ( stat_key = 'O' descript = 'open' )
( stat_key = 'C' descript = 'canceled' )
( stat_key = 'D' descript = 'done' )
LOOP AT ls_mesh-head\_item[ ls_head WHERE status = 'O' ] "Getting sy-subrc EQ 4 here. Not " "reading aything. Same happening witrh other loops below
WRITE: / <ls_item>-db_key.
LOOP AT ls_mesh-item\_part[ ls_item WHERE amount GT 20 ]
WRITE: / <ls_item1>-db_key.
LOOP AT ls_mesh-item\_status[ ls_item WHERE descript CS 'o' ]
WRITE: / <ls_item2>-stat_key.
You have to specify ls_head first.
Thanks Nazil for prompt reply. It is working now and returning values.
But using this approach we are calling out the parent row in ls_head explicitly. Doesn't the loop over ls_mesh-head should feed into ls_head directly for each row implictly ?
So if i have to check for complete header, i have to write code like this:
LOOP AT ls_mesh-head INTO ls_head.
LOOP AT ls_mesh-head\_item[ ls_head WHERE status = 'O' ]
*LOOP AT ls_mesh-head\_item[ ls_head ]
WRITE: / <ls_item>-db_key.
So how could it be better than combination of loop at header and then at item, the usual approach? In this manner, mesh provides a way to maintain the relationship only but not the transactional gain. I sure must be missing something here.