After completing this lesson, you will be able to:
After completing this lesson, you will be able to:
Work with structured data objects
Use structured data objects in ABAP SQL
Work with Structured Data Objects
To access a component of a structure, you have to place a minus-sign (-) between the structure name and the component name.
Note
No blanks are allowed before or after the component selector.
Accessing a structure component that way, you can use it in any operand position in which you can use a variable of the same type. Component airport_from_id of structure connection in the example above is of type /DMO/AIRPORT_FROM_ID. In consequence, you can use this component in any operand position in which you could use a simple variable of type /DMO/AIRPORT_FROM_ID; not only on the left-hand side of a value assignment as in the example, but also on the right-hand side, in the parameter passing of a method call, in the INTO clause or WHERE clause of a SELECT statement, and so on.
If the component of a structure is itself a structure you access the sub-components by using the component selector again after the name of the main component. The first value assignment in the example accesses component MSGTY of MESSAGE, which itself is a component of nested structure CONNECTION_NESTED.
Hint
You can use code-completion to implement access to structure components. Place the cursor immediately after the structure component selector and press CTRL + SPACE to see a list of all available structure components.
The VALUE #( ) expression is an elegant way to assign values to a structured data object.
If you want to fill a whole structure, you can address each component individually as you saw in the previous example.
However, you can also use a VALUE #( ) expression to fill the structure. The expression constructs a structure, fills it with value and assigns the filled structure to a variable, in this case connection. The pound sign (#) tells the ABAP runtime environment to construct a structure with the same type as the target variable connection. In the brackets, you list the components of the structure that you want to fill (it does not have to be all of them) and assign a value to them. The value can be either a literal or the contents of a variable.
When you fill a structure in this way, the runtime system deletes all existing values from the structure before refilling it with the values from your expression.
Note
An assignment in the form connection = VALUE #( ). with just a blank between the brackets, fills all components of the structure with the type-specific initial value. This has the same effect as statement CLEAR connection.
In ABAP, you may only copy the contents of one structure directly into another structure using the notation <target structure> = <source structure> if the two structure types are compatible. This is generally only the case if both structures have the same type. If the structures have different types, two things can happen:
If one of the structures has a non-char-like component at a position where the other structure has a char-like component, direct assignment leads to a syntax error.
If both structures are char-like, or, in other words, both structures consist of char-like components, only, direct assignment is technically possible. But usually, the result will be wrong.
In the example, source structure and target structure are char-like. Therefore, direct assignment is technically possible. But because they are not compatible the result is wrong: The content of component carrier_name is copied to component message in the target structure.
Because there is no syntax error, you have to be extra careful when working with non-compatible char-like structures.
When you copy data between structures, you usually want to copy information from one field into the corresponding field of the target structure - airport_from_id to airport_from_id, airport_to_id to airport_to_id, and so on. To achieve this in ABAP, use the CORRESPONDING expression. This assigns values from <source_structure> to the corresponding, that is, identically-named components of . <target_structure>. You must remember the following points:
The fields must have identical names.
The components do not have to be in the same position or sequence in the two structures.
If the fields have different types, ABAP attempts a type conversion according to the predefined set of rules.
Note
The target structure is initialized before being re-filled with the result of the expression.
Try It Out: Access to Structured Data Objects
Like in the first exercise of this course, create a new global class that implements interface IF_OO_ADT_CLASSRUN.
Copy the following code snippet to the implementation part of method if_oo_adt_classrun~main( ):
Code snippet
TYPES: BEGIN OF st_connection,
airport_from_id TYPE /dmo/airport_from_id,
airport_to_id TYPE /dmo/airport_to_id,
carrier_name TYPE /dmo/carrier_name,
END OF st_connection.
TYPES: BEGIN OF st_connection_nested,
airport_from_id TYPE /dmo/airport_from_id,
airport_to_id TYPE /dmo/airport_to_id,
message TYPE symsg,
carrier_name TYPE /dmo/carrier_name,
END OF st_connection_nested.
DATA connection TYPE st_connection.
DATA connection_nested TYPE st_Connection_nested.
* Example 1: Access to structure components
**********************************************************************
connection-airport_from_id = 'ABC'.
connection-airport_to_id = 'XYZ'.
connection-carrier_name = 'My Airline'.
"Access to sub-components of nested structure
connection_nested-message-msgty = 'E'.
connection_nested-message-msgid = 'ABC'.
connection_nested-message-msgno = '123'.
* Example 2: Filling a structure with VALUE #( ).
**********************************************************************
CLEAR connection.
connection = VALUE #( airport_from_id = 'ABC'
airport_to_id = 'XYZ'
carrier_name = 'My Airline'
).
" Nested VALUE to fill nested structure
connection_nested = VALUE #( airport_from_id = 'ABC'
airport_to_id = 'XYZ'
message = VALUE #( msgty = 'E'
msgid = 'ABC'
msgno = '123' )
carrier_name = 'My Airline'
).
* Example 3: Wrong result after direct assignment
**********************************************************************
connection_nested = connection.
out->write( `-------------------------------------------------------------` ).
out->write( `Example 3: Wrong Result after direct assignment` ).
out->write( data = connection
name = `Source Structure:`).
out->write( |Component connection_nested-message-msgid: { connection_nested-message-msgid }| ).
out->write( |Component connection_nested-carrier_name : { connection_nested-carrier_name }| ).
* Example 4: Assigning Structures using CORRESPONDING #( )
**********************************************************************
CLEAR connection_nested.
connection_nested = CORRESPONDING #( connection ). "
out->write( `-------------------------------------------------------------` ).
out->write( `Example 4: Correct Result after assignment with CORRESPONDING` ).
out->write( data = connection
name = `Source Structure:`).
out->write( |Component connection_nested-message-msgid: { connection_nested-message-msgid }| ).
out->write( |Component connection_nested-carrier_name : { connection_nested-carrier_name }| ).
Press CTRL + F3 to activate the class and F9 to execute the console app.
Analyze the console output. Debug the program, play around with the source code to get familiar with the concepts.
Use Structured Data Object in ABAP SQL
The INTO clause of the SELECT statement will only work correctly if the number and types of the components of the structure correspond to the number and types of the columns specified in the FIELDS clause. In the above example, the statement can only work if the target structure connection has three components with the same type and length as the columns DepartureAirport, DestinationAirport, and \_Airline-Name listed in the FIELDS clause. Note that, in this case, the names do not have to be identical - the system fills the target structure from left to right.
If the field list in the FIELDS clause does not match the structure or table line type in the INTO clause, a runtime error will occur.
The example shows an easy technique to ensure that the target structure matches the field selection:
The target structure is typed with CDS view /DMO/I_Connection, which is the data source in the FROM clause.
The asterisk sign (*) after keyword FIELDS is a short notation to makes sure, that all fields of the view are part of the field selection. Exposed associations are ignored.
Note
This technique is also available when you read directly from a database table. Just like CDS view definitions, database table definitions can also serve as global structure types in ABAP.
The main advantage of this technique is, that the SELECT statement stays syntactically intact even if you or someone else makes changes to CDS view or database table. The most important drawback is, that you always read all fields from the database, whether you actually need them or not.
Note
Only use this technique for views and tables with a small number of fields and if you actually need all the fields. Unnecessary reading of data from the database is a major cause for performance problems.
Another way to avoid syntax errors is variant INTO CORRESPONDING FIELDS. This variant has the same effect as the CORRESPONDING #( ) operator that you learned about earlier. It ensures that data is copied between identically-named components. By defining the structure type according to your needs you can ensure that only the required data is read.
Once again, only the names must be identical. But to avoid problems you should make sure that identically-named components have compatible types. Otherwise the system attempts to convert the contents of the source field into the type of the target field. This can lead to data loss or (catchable) runtime errors.
If the field names in the data source and the component names in the target structure do not match, the combination of FIELDS * and INTO CORRESPONDING FIELDS OF does not work.
If you want to keep variant INTO CORRESPONDING FIELDS OF, you can define alias names for the selected fields in the field list. For this, add addition AS after the field name, followed by the alias name. In the example, the alias name for view field DepartureAirport is airport_from_id and the alias name for path expression \_Airline-Name is carrier_name. Based on this alias names, INTO CORRESPONDING FIELDS OF correctly identifies the structure component in which to store the retrieved data.
The simplest technique to avoid conflicts between the field selection and the target structure is an inline declaration in the INTO clause. The sequence, type and name of the inline declared structure is derived from the FIELD clause. Therefore the target structure always fits the field selection.
Note
Inline declarations are only supported after INTO. You cannot use inline declarations after INTO CORRESPONDING FIELDS OF.
If you use an inline declaration in the INTO clause, you have to provide a name for each element in the FIELDS clause. For fields of the data source, this can be the field name itself or, optionally an alias name. For expressions, the alias name becomes mandatory.
In the example, there is no alias for field DepartureAirport. The name of the field is used as component name in structure connection_inline. Field DestinationAirport has an optional alias ArrivalAirport. In this case the alias is used as component name. The alias for path expression \_Airline-Name is mandatory.
When working with a relational database you often face the problem that you have to read related data from different database tables. We already learned that associations in CDS views are an elegant way to perform this task.
If no CDS View with suitable associations exist you can implement SQL joins, instead. The example above illustrates the principle of joins:
We are interested in flight connections and the airports they connect with each other. We find the 3-letter IDs of the airports in database table /DMO/CONNECTION. The full names of the airports are stored in database table /DMO/AIRPORT.
To retrieve a connection with the departure airport name, in one SELECT statement, we read connection data from DB table /DMO/CONNECTION and join it with DB table /DMO/AIRPORT.
A join consists of the following building blocks:
Data Sources
The Database tables and views to join with each other. A single join always combines a left-hand data source with a right-hand data source. In the example above, table /DMO/CONNECTION is the left-hand data source and table /DMO/AIRPORT the right-hand data source. ABAP SQL also supports joins of joins (nested joins)
Join Condition
The join condition specifies which records of the right-hand data source belong to a record from the left-hand data source. In the example above, the related departure airport is identified by the value in columns CLIENT and AIRPORT_ID. The join condition reads:
The join type has an influence on the result if one of the data sources does not contain a matching records. ABAP SQL currently supports INNER JOIN, LEFT OUTER JOIN, and RIGHT OUTER JOIN. The most common join type is a LEFT OUTER JOIN.
The figure shows the ABAP SQL syntax for a join. In the FROM clause, the join type is specified by keywords LEFT OUTER JOIN between the left-hand data source /dmo/connection and the right-hand data source /dmo/airport. The syntax introduces alias names c and f for the data sources. Alias names for data sources are optional, unless a data source appears more than once in the join.
The join condition follows keyword ON. The separator between the data source or its alias and the field is the tilde sign (~).
Note
In ABAP SQL, it is not necessary to mention the client fields. They are added to the join condition by the database interface before the statement is sent to the database. If the FROM clause defines a join, you can use fields from both data sources in the FIELDS and WHERE clauses.
In this example, the SELECT statement not only read the departure airport name but also the destination airport name. To do so, the FROM clause defines a nested join:
The first join is a left outer join of tables /dmo/connection and /dmo/airport, introducing alias "f" (like "from") for the right-hand data source. This first join is then used as left-hand data source for a second left outer join that has table /dmo/airport as right-hand data source. Note that in this case alias "t" (for "to") is crucial to distinguish this appearance of table /dmo/airport from the previous one.
The FIELDS clause, lists the airport names from both data sources, introducing aliases airport_from_name and airport_to_name to distinguish them from each other.
Hint
The brackets around the first join are optional. If they are omitted, the joins in the from clause are evaluated from left to right.
Try It Out: Structured Data Objects in ABAP SQL
Like in the first exercise of this course, create a new global class that implements interface IF_OO_ADT_CLASSRUN.
Copy the following code snippet to the implementation part of method if_oo_adt_classrun~main( ):
Code snippet
TYPES: BEGIN OF st_connection,
airport_from_id TYPE /dmo/airport_from_id,
airport_to_id TYPE /dmo/airport_to_id,
carrier_name TYPE /dmo/carrier_name,
END OF st_connection.
TYPES: BEGIN OF st_connection_short,
DepartureAirport TYPE /dmo/airport_from_id,
DestinationAirport TYPE /dmo/airport_to_id,
END OF st_connection_short.
DATA connection TYPE st_connection.
DATA connection_short TYPE st_connection_short.
DATA connection_full TYPE /DMO/I_Connection.
* Example 1: Correspondence between FIELDS and INTO
**********************************************************************
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport, DestinationAirport, \_Airline-Name
WHERE AirlineID = 'LH'
AND ConnectionID = '0400'
INTO @connection.
out->write( `------------------------------` ).
out->write( `Example 1: Field List and INTO` ).
out->write( connection ).
* Example 2: FIELDS *
**********************************************************************
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS *
WHERE AirlineID = 'LH'
AND ConnectionID = '0400'
INTO @connection_full.
out->write( `----------------------------` ).
out->write( `Example 2: FIELDS * and INTO` ).
out->write( connection_full ).
* Example 3: INTO CORRESPONDING FIELDS
**********************************************************************
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS *
WHERE AirlineID = 'LH'
AND ConnectionID = '0400'
INTO CORRESPONDING FIELDS OF @connection_short.
out->write( `----------------------------------------------------` ).
out->write( `Example 3: FIELDS * and INTO CORRESPONDING FIELDS OF` ).
out->write( connection_short ).
* Example 4: Alias Names for Fields
**********************************************************************
CLEAR connection.
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport AS airport_from_id,
\_Airline-Name AS carrier_name
WHERE AirlineID = 'LH'
AND ConnectionID = '0400'
INTO CORRESPONDING FIELDS OF @connection.
out->write( `---------------------------------------------------` ).
out->write( `Example 4: Aliases and INTO CORRESPONDING FIELDS OF` ).
out->write( connection ).
* Example 5: Inline Declaration
**********************************************************************
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport,
DestinationAirport AS ArrivalAirport,
\_Airline-Name AS carrier_name
WHERE AirlineID = 'LH'
AND ConnectionID = '0400'
INTO @DATA(connection_inline).
out->write( `-----------------------------------------` ).
out->write( `Example 5: Aliases and Inline Declaration` ).
out->write( connection_inline ).
* Example 6: Joins
**********************************************************************
SELECT SINGLE
FROM ( /dmo/connection AS c
LEFT OUTER JOIN /dmo/airport AS f
ON c~airport_from_id = f~airport_id )
LEFT OUTER JOIN /dmo/airport AS t
ON c~airport_to_id = t~airport_id
FIELDS c~airport_from_id, c~airport_to_id,
f~name AS airport_from_name, t~name AS airport_to_name
WHERE c~carrier_id = 'LH'
AND c~connection_id = '0400'
INTO @DATA(connection_join).
out->write( `------------------------------------------` ).
out->write( `Example 6: Join of Connection and Airports` ).
out->write( connection_join ).
Press CTRL + F3 to activate the class and F9 to execute the console app.
Analyze the console output. Debug the program, play around with the source code to get familiar with the concepts.
Use a Structured Data Object
Task 1: Preparation
Steps
If you finished the previous exercise Analyze and Use a CDS View Entity, create a copy of your global class ZCL_##_CDS, and name the copy ZCL_##_STRUCTURE, where ## is your group number. Then skip the rest of this task.
In the Project Explorer view on the left, expand your package.
Expand node Source Code Library → Classes
Right-click the name of the class you want to copy and choose Duplicate.
In the Name field, enter the name ZCL_##_STRUCTURE, where ## is your group number.
Choose Next.
Select your transport request and choose Finish.
If you did not finish the previous exercise, create a new global class ZCL_##_STRUCTURE, where ## is your group number. Ensure that the class implements the interface IF_OO_ADT_CLASSRUN.
Choose File → New → ABAP Class.
Enter the name of your package in the Package field. In the Name field, enter the name ZCL_##_STRUCTURE, where ## is your group number. Enter a description.
In the Interfaces group box, choose Add.
Enter IF_OO_ADT_CLASSRUN. When the interface appears in the hit list, double-click it to add it to the class definition.
Choose Next.
Select your transport request and choose Finish.
Copy the following code between METHOD if_oo_adt_classrun~main. and ENDMETHOD. on the Global Class tab:
Code snippet
DATA connection TYPE REF TO lcl_connection.
DATA connections TYPE TABLE OF REF TO lcl_connection.
* First Instance
**********************************************************************
TRY.
connection = NEW #(
i_carrier_id = 'LH'
i_connection_id = '0400'
).
APPEND connection TO connections.
CATCH cx_abap_invalid_value.
out->write( `Method call failed` ).
ENDTRY.
* Second instance
**********************************************************************
TRY.
connection = NEW #(
i_carrier_id = 'AA'
i_connection_id = '0017'
).
APPEND connection TO connections.
CATCH cx_abap_invalid_value.
out->write( `Method call failed` ).
ENDTRY.
* Third instance
**********************************************************************
TRY.
connection = NEW #(
i_carrier_id = 'SQ'
i_connection_id = '0001'
).
APPEND connection TO connections.
CATCH cx_abap_invalid_value.
out->write( `Method call failed` ).
ENDTRY.
* Output
**********************************************************************
LOOP AT connections INTO connection.
out->write( connection->get_output( ) ).
ENDLOOP.
Navigate to the Global Class tab and insert the source code between METHOD if_oo_adt_classrun~main. and ENDMETHOD..
Copy the following code to the Local Types tab:
Code snippet
CLASS lcl_connection DEFINITION.
PUBLIC SECTION.
CLASS-DATA conn_counter TYPE i.
METHODS constructor
IMPORTING
i_connection_id TYPE /dmo/connection_id
i_carrier_id TYPE /dmo/carrier_id
RAISING
cx_ABAP_INVALID_VALUE .
METHODS get_output
RETURNING
VALUE(r_output) TYPE string_table.
PROTECTED SECTION.
PRIVATE SECTION.
DATA carrier_id TYPE /dmo/carrier_id.
DATA connection_id TYPE /dmo/connection_id.
DATA airport_from_id TYPE /dmo/airport_from_id.
DATA airport_to_id TYPE /dmo/airport_to_id.
DATA carrier_name TYPE /dmo/carrier_name.
ENDCLASS.
CLASS lcl_connection IMPLEMENTATION.
METHOD constructor.
" ensure non-initial input
IF carrier_id IS INITIAL OR connection_id IS INITIAL.
RAISE EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
" check existence and read additional data
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport, DestinationAirport, \_Airline-Name
WHERE AirlineID = @i_carrier_id
AND ConnectionID = @i_connection_id
INTO ( @airport_from_id, @airport_to_id, @carrier_name ).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
me->connection_id = i_connection_id.
me->carrier_id = i_carrier_id.
conn_counter = conn_counter + 1.
ENDMETHOD.
METHOD get_output.
APPEND |--------------------------------| TO r_output.
APPEND |Carrier: { carrier_id } { carrier_name }| TO r_output.
APPEND |Connection: { connection_id }| TO r_output.
APPEND |Departure: { airport_from_id }| TO r_output.
APPEND |Destination: { airport_to_id }| TO r_output.
ENDMETHOD.
ENDCLASS.
Navigate to the Local Types tab and insert the source code.
Task 2: Declare a Structured Variable
Define a structured attribute details to replace scalar attributes airport_from_id, airport_to_id, and carrier_name.
Steps
Switch to the local class lcl_connection.
In the global class, choose Local Types.
Add a private structure type st_details with the folowing components:
Components of structure type st_details
Component Name
Data Type
DepartureAirport
/DMO/AIRPORT_FROM_ID
DestinationAirport
/DMO/AIRPORT_TO_ID
AirlineName
/DMO/CARRIER_NAME
After line PRIVATE SECTION. , add the following code:
Code snippet
TYPES: BEGIN OF st_details,
DepartureAirport TYPE /dmo/airport_from_id,
DestinationAirport TYPE /dmo/airport_to_id,
AirlineName TYPE /dmo/carrier_name,
END OF st_details.
Comment or remove the declaration of attributes airport_from_id, airport_to_id, and carrier_name.
Task 3: Access Structure Components
Use the components of structured attribute details in method get_output( ).
Steps
Adjust the implementation of method get_output( ). Replace any access to attributes airport_from_id, airport_to_id, and carrier_name with the corresponding component of attribute details.
Hint
Do not type in the component names manually. After typing the structure component selector (-), press Ctrl + Space to get a list of all components.
Navigate to the implementation of method get_output( ).
Adjust the APPEND statements as follows:
Code snippet
* APPEND |--------------------------------| TO r_output.
* APPEND |Carrier: { carrier_id } { carrier_name }| TO r_output.
* APPEND |Connection: { connection_id }| TO r_output.
* APPEND |Departure: { airport_from_id }| TO r_output.
* APPEND |Destination: { airport_to_id }| TO r_output.
APPEND |--------------------------------| TO r_output.
APPEND |Carrier: { carrier_id } { details-airlinename }| TO r_output.
APPEND |Connection: { connection_id }| TO r_output.
APPEND |Departure: { details-departureairport }| TO r_output.
APPEND |Destination: { details-destinationairport }| TO r_output.
Task 4: Fill Structured Attribute in SELECT Statement
Use the structured attribute as target of the SELECT statement in method constructor( ).
Steps
Adjust the SELECT statement in the implementation of method constructur( ). Replace the list of data objects in the INTO clause with the structured attribute details.
Navigate to the implementation of method constructor( ).
Adjust the SELECT statement as follows:
Code snippet
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport, DestinationAirport, \_Airline-Name
WHERE AirlineID = @i_carrier_id
AND ConnectionID = @i_connection_id
* INTO ( @airport_from_id, @airport_to_id, @carrier_name ).
INTO @details.
Add alias name AirlineName for the path expression.
The SELECT statement should now look like this:
Code snippet
SELECT SINGLE
FROM /DMO/I_Connection
FIELDS DepartureAirport, DestinationAirport, \_Airline-Name as AirlineName
WHERE AirlineID = @i_carrier_id
AND ConnectionID = @i_connection_id
INTO CORRESPONDING FIELDS OF @details.