
ABAP in der Praxis - Objekt Generator
In diesem Beispiel schauen wir uns an, wie wir mit der XCO Bibliothek einen wiederverwendbaren Generator erstellen, um uns für unsere Tutorials etwas Arbeit zu sparen und automatisiert DDIC Objekte zu generieren.
Inhaltsverzeichnis
In diesem Artikel gehen wir in die Praxis und erstellen uns einen Generator zur Anlage von neuen Objekten im DDIC Bereich. Dies soll uns in Zukunft bei Tutorials die manuelle Arbeit abnehmen.
Einleitung
Manchmal möchtest du etwas Neues probieren, bist aber eigentlich schon wieder bei der Anlage der Tabelle und der Erstellung ordentlicher Datenelemente genervt? Dann hilft dir ein Generator
Aufgabe
Die Aufgabe klingt erst einmal sehr einfach, wir benötigen einen Generator, der uns Datenelemente und Domänen anlegt und mit einer minimalen Konfiguration arbeitet. Damit wollen wir etwas Arbeit sparen, wenn wir beim nächsten Mal Modelle und Datenbanken anlegen wollen. Ist er wiederverwendbar gebaut, können wir damit auch in Schulungen Massendaten für jeden Teilnehmer generieren.
Hinweis: Im nächsten Abschnitt werden wir auf die Lösung eingehen, wenn du die Aufgabe erst einmal selbstständig machen möchtest, solltest du hier pausieren.
Lösung
Gehen wir Schritt für Schritt durch die verschiedenen Teile der Entwicklung.
Grundstruktur
Im ersten Schritt stellen wir eine saubere Grundstruktur der Objekte zur Verfügung. Wir wollen mit ABAP OO ein kleines Framework zur Verfügung stellen und dabei die Bestandteile der Testbarkeit und Wiederverwendbarkeit einhalten. Deshalb generieren wir neben der Klasse auch eine Factory und einen Injector, womit wir fast jede Entwicklung starten. Damit die Anlage einfach funktioniert, verwenden wir unsere IDE Actions, um uns die Klassen generieren zu lassen.
Nach der Formatierung der Objekte, können wir dann in die eigentliche Implementierung gehen.
Strukturen
Damit wir die Objekte anlegen können, sollten wir zuerst eine Konfiguration in Form von Strukturen und Tabellen anlegen. Hier möchten wir in den meisten Fällen eine Struktur, Tabelle oder Objekt in die Methode übergeben, um die Schnittstelle, klein, sauber und leicht erweiterbar zu halten. In diesem Fall legen wir eine Struktur an und beginnen mit den internen Objekten. Beginnen wir mit der Konfiguration für Domänen und Datenelemente.
TYPES: BEGIN OF domain,
name TYPE sxco_ad_object_name,
description TYPE if_xco_cp_gen_doma_s_form=>tv_short_description,
base_type TYPE string,
length TYPE i,
decimals TYPE i,
case_sensitive TYPE abap_boolean,
END OF domain.
TYPES domains TYPE STANDARD TABLE OF domain WITH EMPTY KEY.
TYPES: BEGIN OF data_element,
name TYPE sxco_ad_object_name,
description TYPE if_xco_cp_gen_dtel_s_form=>tv_short_description,
label TYPE string,
domain TYPE sxco_ad_object_name,
END OF data_element.
TYPES data_elements TYPE STANDARD TABLE OF data_element WITH EMPTY KEY.
Später wollen wir dann noch abstrakte Entitäten mit den Datenelementen anlegen können, deshalb definieren wir noch zusätzlich Konfigurationen für die Feldliste der Entität und die eigentliche Entität.
TYPES: BEGIN OF field,
name TYPE sxco_cds_field_name,
data_element TYPE sxco_ad_object_name,
currency TYPE string,
unit TYPE string,
END OF field.
TYPES fields TYPE STANDARD TABLE OF field WITH EMPTY KEY.
TYPES: BEGIN OF abstract_entity,
name TYPE sxco_cds_object_name,
description TYPE if_xco_cp_gen_dtel_s_form=>tv_short_description,
fields TYPE fields,
END OF abstract_entity.
TYPES abstract_entities TYPE STANDARD TABLE OF abstract_entity WITH EMPTY KEY.
Damit haben wir alle untergeordneten Typen definiert und können nun die eigentliche Konfigurationsstruktur definieren. Dazu wollen wir auch das Paket zur Anlage, den Transport und ein Präfix haben. Die ersten zwei Werte benötigen wir je nach System. Über die anderen Felder geben wir die Konfiguration in den Generator.
TYPES: BEGIN OF ddic_configuration,
package TYPE sxco_package,
transport TYPE sxco_transport,
prefix TYPE string,
domains TYPE domains,
data_elements TYPE data_elements,
abstract_entities TYPE abstract_entities,
END OF ddic_configuration.
Hinweis: Bereits mit dem Aufbau haben wir unsere Grundstruktur für die Anlage gelegt. Somit hat bei uns eigentlich jedes Datenelement immer eine Domäne, die den Typen und die technischen Details definiert.
Konfiguration
Da wir viele Teile der Konfiguration dem Aufrufer abnehmen wollen, können wir einige Informationen auch einfach ableiten. Über das XCO Framework stehen uns hier einige Methoden zur Verfügung. Zum Beispiel können wir über das aufrufende Objekt ableiten in welchem Paket wir unterwegs sind und ob es einen offenen Transportauftrag dazu gibt. Dazu könnten wir den Callstack abrufen und das aufrufende Objekt ermitteln. In diesem Fall halten wir es relativ einfach und übergeben über die Factory das Feld SY-REPID. Allerdings erhalten wir den Namen der Klasse als "ZMY_CLASS====CP" übergeben. Dafür schreiben wir eine einfache Funktion, die den Namen extrahiert.
calling_object = substring( val = calling_object
len = find( val = calling_object
sub = '=' ) ).
Wir können uns nun die Klasse einlesen, zumindest gehen wir davon aus, dass wir von einer Klasse aus gerufen werden, und holen uns über das Interface IF_XCO_AR_OBJECT das Paket und dort den Namen. Grundsätzlich können wir auch über das Interface IF_XCO_CTS_CHANGEABLE an einen offenen Transport gelangen, wenn das Objekt einem zugeordnet ist.
xco_cp_abap=>class( CONV #( calling_object ) )->if_xco_ar_object~get_package( )->name.
Ist das Präfix befüllt, dann wollen wir alle Objekte mit diesem ergänzen, da diese meist im gleichen Namensraum sind. Spart bei der Eingabe Zeit und Aufwand und wir könnten damit auch verschiedene Objekte für verschiedene Teilnehmer generieren.
LOOP AT ddic_configuration-domains REFERENCE INTO DATA(domain).
domain->name = ddic_configuration-prefix && domain->name.
ENDLOOP.
LOOP AT ddic_configuration-data_elements REFERENCE INTO DATA(data_element).
data_element->name = ddic_configuration-prefix && data_element->name.
data_element->domain = ddic_configuration-prefix && data_element->domain.
ENDLOOP.
Operation
Damit wir Objekte generieren können, benötigen wir eine OPERATION vom XCO Framework. Da wir neue Objekte anlegen wollen, verwenden wir die Methode CREATE_PUT_OPERATION. Diese übergeben wir dann von Methode zu Methode, um die verschiedenen Typen anzulegen.
DATA(put_operation) = xco_cp_generation=>environment->dev_system( me->ddic_configuration-transport )->create_put_operation( ).
Bereits in einem älteren Artikel haben wir die Generierung von Artefakten mit XCO beschrieben. Im Artikel findest du weitere Informationen zu den Möglichkeiten und dem Framework.
Domänen
Was macht eigentlich eine Domäne aus und welche Typen benötigen wir? Dazu benötigen wir noch eine Struktur von Konstanten, um dem Nutzer die Möglichkeit zu geben unterstützte Typen anzugeben. Dabei verwenden die Typen, die wir in der Entwicklung normalerweise am häufigsten verwenden.
CONSTANTS:
BEGIN OF domain_types,
character TYPE string VALUE `CHAR`,
date TYPE string VALUE `DATN`,
time TYPE string VALUE `TIMN`,
integer TYPE string VALUE `INT4`,
integer_long TYPE string VALUE `INT8`,
timestamp TYPE string VALUE `UTCLONG`,
currency_code TYPE string VALUE `CUKY`,
currency TYPE string VALUE `CURR`,
quantity TYPE string VALUE `QUAN`,
unit TYPE string VALUE `UNIT`,
decimals TYPE string VALUE `DEC`,
raw TYPE string VALUE `RAW`,
string TYPE string VALUE `STRING`,
short_string TYPE string VALUE `SSTRING`,
END OF domain_types.
Der eigentliche Typ der Struktur, ergibt sich aus den Anforderungen, die wir haben und den Pflichtfeldern des Standards. In diesem Fall benötigen wir immer eine Beschreibung, das Paket für die Anlage, einen Namen und den Typen. Über die Operation fügen wir eine neue Domäne ein, setzen das Zielpaket und lassen uns die Spezifikation geben. Über die Spezifikation können wir dann die Eigenschaften, wie Beschreibung, den Typen und Case-Sensitive setzen.
LOOP AT ddic_configuration-domains INTO DATA(domain).
DATA(specification) = operation->for-doma->add_object( domain-name
)->set_package( ddic_configuration-package
)->create_form_specification( ).
specification->set_short_description( domain-description ).
CASE domain-base_type.
WHEN zif_gen_objects=>domain_types-character.
format = xco_cp_abap_dictionary=>built_in_type->char( CONV #( domain-length ) ).
WHEN zif_gen_objects=>domain_types-date.
format = xco_cp_abap_dictionary=>built_in_type->datn.
WHEN zif_gen_objects=>domain_types-integer.
format = xco_cp_abap_dictionary=>built_in_type->int4.
WHEN OTHERS.
CONTINUE.
ENDCASE.
specification->set_format( format ).
specification->output_characteristics->set_case_sensitive( domain-case_sensitive ).
ENDLOOP.
Datenelemente
Die Datenelemente sehen dann recht ähnlich aus, wie die Domänen. Hier haben wir Pflichtfelder wie die Texte, die wir befüllen sollten. Damit bleibt die Logik hier recht überschaubar.
LOOP AT ddic_configuration-data_elements INTO DATA(data_element).
DATA(specification) = operation->for-dtel->add_object( data_element-name
)->set_package( ddic_configuration-package
)->create_form_specification( ).
specification->set_short_description( data_element-description ).
specification->set_data_type( xco_cp_abap_dictionary=>domain( data_element-domain ) ).
DATA(label) = COND #( WHEN data_element-label IS INITIAL
THEN data_element-name
ELSE data_element-label ).
specification->field_label-short->set_text( CONV #( label ) ).
specification->field_label-medium->set_text( CONV #( label ) ).
specification->field_label-long->set_text( CONV #( label ) ).
specification->field_label-heading->set_text( CONV #( label ) ).
ENDLOOP.
Abstrakte Entitäten
Kommen wir zur ersten CDS Entität, die durchaus etwas anspruchsvoller sein können. Der Anfang ist erst einmal gleich, wir definieren eine Spezifikation und setzen die Beschreibung. Dann haben wir aber unterschiedliche Typen, die wir anlegen können. Daher benötigen wir über die Spezifikation unseren Typen, um diesen weiter auszuprägen.
DATA(abstract_entity) = specification->add_abstract_entity( ).
Dann übernehmen wir unsere Felder in die Entität. Da wir für jedes Feld ein Datenelement definieren wollen, können wir hier eine einfache Logik verwenden. Die komplexere Typbestimmung bleibt damit in der Domäne. Zuerst fügen wir über ADD_FIELD das Feld hinzu und setzen über SET_TYPE dann den Typen als Datenelement. Der schwierige Teil dabei ist, den passenden Beispielcode zu finden, wie das Feld hinzugefügt wird und auch der Typ. Im ersten Schritt hatten wir es über eine Expression versucht, die aber hinter jedem Datensatz dann ein Komma macht, was bei einer abstrakten Entität zu Fehlern führt.
DATA(cds_field) = abstract_entity->add_field( xco_cp_ddl=>field( field-name )
)->set_type( xco_cp_abap_dictionary=>data_element( field-data_element ) ).
Was fehlt nun eigentlich noch? Die Entität wird uns einen Fehler schmeißen, wenn wir ein AMOUNT oder QUANTITY Feld verwenden und die Einheiten nicht miteinander verlinken. Daher müssen wir im Nachgang Annotationen an den Feldern ergänzen. Dabei werden die Annotationen ohne das @ angegeben und der Wert über den Builder zusammengefügt. Diese Recherche hat auch etwas Zeit gekostet, da die Verwendung nicht gleich offensichtlich war und die ABAP Docs keine Informationen enthielten, wie die Methode und die Werte verwendet werden.
IF field-currency IS NOT INITIAL.
cds_field->add_annotation( 'Semantics.amount.currencyCode' )->value->build( )->add_string( field-currency ).
ENDIF.
IF field-unit IS NOT INITIAL.
cds_field->add_annotation( 'Semantics.quantity.unitOfMeasure' )->value->build( )->add_string( field-unit ).
ENDIF.
Generierung
Am Ende nicht vergessen auch die Objekte zu generieren. Hier generieren und aktivieren sie in einem Schritt und legen sie im System an. Du kannst sie aber auch erst einmal in einem inaktiven Zustand anlegen lassen, wenn du sowieso noch nacharbeiten hast oder dich um die Fehler kümmern willst.
RETURN put_operation->execute( ).
Möglichkeiten
Um noch weitere Optimierungen in den Code zu bekommen, können wir noch einige Dinge optimieren, die dem Verwender weitere Eingaben erleichtern:
- Defaults - Automatische Defaults für Label, Beschreibungen und Grundtypen, um die Eingaben zu minimieren.
- Grundtypen - Verwendung von Grundtypen und Datenelemente aus dem Standard (SPRAS, ABAP_BOOLEAN)
- Fehlerbehandlung - Das Objekt kann nicht angelegt werden, dafür wird ein Protokoll zurückgegeben. Es gibt das Objekt schon oder Pflichteingaben fehlen?
- JSON - Aktuell bezieht der Generator eine Struktur für die Generierung, allerdings ist es auch recht einfach möglich ein JSON im System oder aus einem Git Repository als Konfiguration zu lesen und zu verwenden.
Test
Nachdem wir nun die Logik definiert haben, sollten wir auch einmal die Logik testen und ein paar Objekte anlegen. Dazu verwenden wir einen Unit Test, was aber in diesem Fall nicht Best Practice ist, da ein Test keine Objekte im System ändern sollte bzw. dann auch die entsprechende Einstufung benötigt. Befüllen wir als unsere Struktur für die Konfiguration.
DATA(abstract_entities) = VALUE zif_gen_objects=>abstract_entities(
( name = 'ZGEN_S_TestGeneratedStructure'
description = 'Automatic generated'
fields = VALUE #( ( name = 'KeyField' data_element = 'KEY_FIELD' )
( name = 'SalesVolume' data_element = 'AMOUNT' currency = 'SalesCurrency' )
( name = 'SalesCurrency' data_element = 'CURRENCY' ) ) ) ).
DATA(data_elements) = VALUE zif_gen_objects=>data_elements(
( name = 'KEY_FIELD' domain = 'KEY_FIELD' description = 'Key' )
( name = 'AMOUNT' domain = 'AMOUNT' description = 'Amount' )
( name = 'CURRENCY' domain = 'CURRENCY' description = 'Currency' ) ).
DATA(domains) = VALUE zif_gen_objects=>domains(
( name = 'KEY_FIELD' base_type = zif_gen_objects=>domain_types-character length = 7 )
( name = 'AMOUNT' base_type = zif_gen_objects=>domain_types-currency )
( name = 'CURRENCY' base_type = zif_gen_objects=>domain_types-currency_code ) ).
DATA(config) = VALUE zif_gen_objects=>ddic_configuration( prefix = 'ZGEN_DEMO_'
domains = domains
data_elements = data_elements
abstract_entities = abstract_entities ).
Dann erzeugen wir uns über die Factory eine Instanz, übergeben die Daten und lassen uns die Objekte im System erzeugen.
DATA(generator) = zcl_gen_objects_factory=>create_generator( sy-repid ).
DATA(result) = generator->generate_ddic( config ).
Nachdem die Logik hoffentlich ohne größere Fehler durchlaufen ist, sollten wir nach einem Refresh im aktuellen Paket unsere Objekte finden.
Im Core Data Service wurden die Felder wie gewünscht definiert, die Beziehung zur Währung ist eingetragen und alle Objekte wurden aktiviert. Damit ist der Test für uns erfolgreich.
define abstract entity ZGEN_S_TestGeneratedStructure
{
KeyField : zgen_demo_key_field;
@Semantics.amount.currencyCode: 'SalesCurrency'
SalesVolume : zgen_demo_amount;
SalesCurrency : zgen_demo_currency;
}
Vollständiges Beispiel
Die aktuelle Version des Generators findest du in diesem GitHub Repository. Das Repository ist auch Grundlage für den nächsten Artikel, wo wir die ersten Objekte per Konfiguration anlegen wollen, um so einen schnellen Einstieg zu erhalten.
Fazit
Die Arbeit mit der XCO Bibliothek ist in einigen Fällen nicht wirklich einfach und wenn wir die API noch nicht so gut kennen, dauert die Suche nach Beispielen und der richtigen Methode schon einmal etwas länger. Am Ende erhalten wir einen ABAP Cloud kompatiblen Generator, der uns in Zukunft etwas Arbeit abnehmen wird.

