ABAP OO - Data Access Object (DAO)
In diesem Artikel schauen wir uns einmal die DAOs an, was du mit ihnen machen kannst und wobei sie dich unterstützen.
Inhaltsverzeichnis
Data Access Objects oder auch DAOs sollen uns das Leben etwas leichter machen, vor allem wenn wir uns die Entkopplung zur Datenbank anschauen. Wie du so ein Objekte aufbaust, es nutzen kannst und welche Vorteile du damit erlangst, dass zeigen wir dir in den folgenden Abschnitten des Artikels.
Szenario
Wenn du in deinen Quellcode Zugriffe auf Daten implementierst, dann machst du dies meist direkt, da du hier deinen SELECT beeinflussen kannst und die Daten für die Einschränkung zur Verfügung hast. Für einen kleinen Report mit statischen Daten ist das kein Problem. Doch was passiert im nächsten Report, wenn du auf die gleiche Datenbank zugreifen möchtest? Richtig, du warst wahrscheinlich noch einen Zugriff implementieren, der dann vielleicht etwas anders aussieht, aber die gleichen Daten nutzt.
Stell dir vor du musst nun diese verschiedenen Reports auf ein neues System migrieren und dort existieren die Tabellen vielleicht nur in einer anderen Form oder du musst im schlimmsten Fall per RFC die Daten aus einem dritten System nachlesen und zur Verfügung stellen.
An dieser Stelle wirst du vor die Herausforderung gestellt, an jeder Stelle den Zugriff auf die Daten zu tauschen und neue Zugriff zu implementieren. Dies betrifft auch nicht nur ein Objekt, sondern vielleicht noch ein paar mehr Reports und Klassen, wo du auf die selben Daten zugreifst.
DAO
An dieser Stelle kann ein Data Access Object oder kurz DAO zum Einsatz kommen. Das DAO stellt zur Implementierungszeit eine saubere Schnittstelle zur Verfügung, die die Daten bereitstellt und in den meisten Fällen für den Zugriff auf ein Datenobjekt zuständig ist. In unserem Beispielszenario deckt das DAO genau eine Tabelle ab, die wir verschiedenen Anwendungen zur Verfügung stellen wollen.
Aufbau
Wie ist so ein DAO nun eigentlich aufgebaut? Hier hast du relativ viel Freiheit um die Klasse nach deinem Wunsch zu formen. Du solltest allerdings als Grundlage ein Interface nehmen, womit auch die Testbarkeit gewährleistet wird, wenn du direkt mit dem Interface und nicht der eigentlichen Implementierung arbeitest. Dazu eine Beispielimplementierung eines solchen Interfaces:
INTERFACE zif_bs_demo_account_dao PUBLIC.
TYPES:
ts_data TYPE zbs_dy_account,
tt_data TYPE STANDARD TABLE OF ts_data WITH EMPTY KEY,
tt_r_identifier TYPE RANGE OF zbs_dy_account-identifier,
tt_r_currency TYPE RANGE OF zbs_dy_account-currency.
METHODS:
read
IMPORTING
id_identifier TYPE ts_data-identifier
RETURNING VALUE(rs_result) TYPE ts_data,
read_query
IMPORTING
it_r_identifier TYPE tt_r_identifier OPTIONAL
it_r_currency TYPE tt_r_currency OPTIONAL
RETURNING VALUE(rt_result) TYPE tt_data,
has_currency
IMPORTING
id_currency TYPE ts_data-currency
RETURNING VALUE(rd_result) TYPE abap_bool,
create
IMPORTING
is_data TYPE ts_data
RETURNING VALUE(rd_result) TYPE abap_bool.
ENDINTERFACE.
Der Name des Interfaces bezieht sich bereits auf die Tabelle oder den Kontext, damit können die eigentlichen Methodennamen sehr kurz ausfallen. Bei den Namen orientieren wir uns gern an den CRUD Operationen (Create, Read, Update und Delete) die auf so einem Objekt möglich sind. Die Implementierung weiterer Hilfmethoden ist jederzeit möglich. Dazu legen wir eine Klasse an, die das Interface implementiert. Die einzelnen Methoden könnten daher so aussehen:
CLASS zcl_bs_demo_account_dao DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_bs_demo_account_dao.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_bs_demo_account_dao IMPLEMENTATION.
METHOD zif_bs_demo_account_dao~read.
SELECT SINGLE *
FROM zbs_dy_account
WHERE identifier = @id_identifier
INTO @rs_result.
ENDMETHOD.
METHOD zif_bs_demo_account_dao~read_query.
SELECT *
FROM zbs_dy_account
WHERE identifier IN @it_r_identifier
AND currency IN @it_r_currency
INTO TABLE @rt_result.
ENDMETHOD.
METHOD zif_bs_demo_account_dao~create.
INSERT zbs_dy_account FROM @is_data.
rd_result = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD zif_bs_demo_account_dao~has_currency.
DATA(lt_data) = zif_bs_demo_account_dao~read_query(
it_r_currency = VALUE #( ( sign = 'I' option = 'EQ' low = id_currency ) )
).
rd_result = xsdbool( lt_data IS NOT INITIAL ).
ENDMETHOD.
ENDCLASS.
Du siehst an der Methode HAS_CURRENCY, dass wir Zugriff auch innerhalb des DAO wiederverwenden können, um so einfache und wiederverwendbare Funktionen zur Verfügung zu stellen, ohne uns bei den Datenzugriffen zu wiederholen.
Verwendung
Schauen wir uns im nächsten Schritt einmal an, wie du die Klasse im Anschluss am Effektivsten verwenden kannst. Dazu solltest du wissen, dass wir das Interface implementiert haben, um die Funktionen bei Nutzung von der eigentlichen Klasse zu entkoppeln. Somit haben wir die Möglichkeit zur Ausführung den DAO gegen ein anderes Objekt zu vertauschen. Wie sieht so ein entkoppeltes Szenario aus, dazu ein Beispiel einer ausführbaren Klasse mit entsprechender Implementierung:
CLASS zcl_bs_demo_use_dao DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_dao TYPE REF TO zif_bs_demo_account_dao.
METHODS:
get_dao
RETURNING VALUE(ro_result) TYPE REF TO zif_bs_demo_account_dao.
ENDCLASS.
CLASS zcl_bs_demo_use_dao IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
ENDMETHOD.
METHOD get_dao.
IF mo_dao IS INITIAL.
mo_dao = NEW zcl_bs_demo_account_dao( ).
ENDIF.
ro_result = mo_dao.
ENDMETHOD.
ENDCLASS.
Die Referenz zum DAO befindet sich als Attribut in der Klasse und basiert auf dem Interface. Wir definieren eine Methode die uns den DAO zurückgibt, wenn wir diesen anfordern. In dieser Methode wird das Objekt erzeugt, aber erst dann, wenn wir es benötigen. Damit ist unser Objekt auch schon einsetzbar und kann für die Zugriffe verwendet werden.
Injection
Schauen wir uns in einem kleinen Beispiel einmal an, wie wir die Instanz des DAO gegen eine andere Implementierung austauschen können. Dazu entwickeln wir nun eine einfache Verwendung und rufen zweimal die CREATE Methode mit dem selben Schlüssel, aber unterschiedlichen Inhalten auf.
DATA(lo_dao) = get_dao( ).
DATA(ld_created_1) = lo_dao->create( VALUE #( identifier = '0817' amount = '15' currency = 'EUR' ) ).
out->write( |Data 1 was created: { ld_created_1 }| ).
DATA(ld_created_2) = lo_dao->create( VALUE #( identifier = '0817' amount = '35' currency = 'USD' ) ).
out->write( |Data 2 was created: { ld_created_2 }| ).
Da wir versuchen den Schlüssel zweimal anzulegen, wird der zweite Aufruf der Methode ein ABAP_FALSE liefern, da die Anlage nicht möglich ist. Nun legen wir für den DAO eine zweite Implementierung in einer lokalen Klasse, direkt in unserer Testklasse an und implementieren die CREATE Methode neu, dieses Mal wird sie immer ein ABAP_TRUE zurückgeben, aber dafür keinen Datensatz anlegen:
CLASS lcl_empty_dao DEFINITION.
PUBLIC SECTION.
INTERFACES: zif_bs_demo_account_dao.
ENDCLASS.
CLASS lcl_empty_dao IMPLEMENTATION.
METHOD zif_bs_demo_account_dao~create.
rd_result = abap_true.
ENDMETHOD.
METHOD zif_bs_demo_account_dao~has_currency.
ENDMETHOD.
METHOD zif_bs_demo_account_dao~read.
ENDMETHOD.
METHOD zif_bs_demo_account_dao~read_query.
ENDMETHOD.
ENDCLASS.
Das vollständige Beispiel sieht nun wie folgt aus. Die erzeugte Ausgabe in der Konsole wird immer zwei ABAP_TRUE Werte anzeigen und es wird kein Datensatz mehr auf der Datenbank angelegt.
CLASS zcl_bs_demo_use_dao DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
PRIVATE SECTION.
DATA:
mo_dao TYPE REF TO zif_bs_demo_account_dao.
METHODS:
get_dao
RETURNING VALUE(ro_result) TYPE REF TO zif_bs_demo_account_dao.
ENDCLASS.
CLASS zcl_bs_demo_use_dao IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
mo_dao = NEW lcl_empty_dao( ).
DATA(lo_dao) = get_dao( ).
DATA(ld_created_1) = lo_dao->create( VALUE #( identifier = '0818' amount = '15' currency = 'EUR' ) ).
out->write( |Data 1 was created: { ld_created_1 }| ).
DATA(ld_created_2) = lo_dao->create( VALUE #( identifier = '0818' amount = '35' currency = 'USD' ) ).
out->write( |Data 2 was created: { ld_created_2 }| ).
ENDMETHOD.
METHOD get_dao.
IF mo_dao IS INITIAL.
mo_dao = NEW zcl_bs_demo_account_dao( ).
ENDIF.
ro_result = mo_dao.
ENDMETHOD.
ENDCLASS.
Damit solltest du eine ungefähre Vorstellung davon bekommen, wie du das DAO zur Laufzeit austauschen kannst und so eine andere Implementierung nutzen kannst. Das gezeigte Beispiel ist aber eher fiktiver Natur und soll nur veranschaulichen, wie du die Funktion umsetzen kannst.
Vorteile
Die Verwendung des DAO bringt in der Entwicklung einige Vorteile für dich mit, die wir dir an dieser Stelle noch einmal auflisten wollen:
- Single Point of Access - Ein Objekt das die Daten für eine Datenquelle zur Verfügung stellt und immer wieder verwendet werden kann.
- Erweiterbarkeit - Die Erweiterbarkeit um neue Funktionen, Felder oder Logiken kann leicht an einer Stelle implementiert werden.
- Kapselung - Entkopplung des Datenzugriffs von der Business Logik und damit mehr Stabilität in den aufrufenden Reports und Klassen.
- Datentrennung - Welche Datenquelle hinter der Datenbereitstellung steckt ist nun völlig entkoppelt. Die Daten können aus einer Tabelle, einem CDS View, per RFC oder OData zur Verfügung gestellt werden.
- Testbarkeit - Die testende Anwendung kann nun auch den Datenzugriff auf Dummy-Daten simulieren, auch wenn kein Testdouble-Framework vorhanden ist (ältere Releases).
Fazit
Du solltest dem DAO eine Chance geben und in deinem nächsten Projekt verwenden. Der initiale Aufwand zur Bereitstellung ist relativ groß, doch bereits bei den Unit Tests kannst du den ersten Benefit mitnehmen und spätestens bei Erweiterungen und/oder einer Migration wirst du den Vorteil erkennen.