
RAP - Singleton Pattern
Das Singleton Pattern in RAP gibt es schon länger, doch wie genau funktioniert es und wie kannst du es auch für andere Use-Cases nutzen? Schauen wir uns dazu die Details in diesem Artikel an.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns das Singleton Pattern an, wie es aufgebaut ist und wofür du es noch verwenden kannst. Dazu werden wir in ein Beispiel einsteigen.
Einleitung
Das ABAP RESTful Application Programming Model ist das neue Modell in ABAP, um Cloud Ready und Clean Core Anwendungen zu erstellen. Mit RAP lassen sich neben Anwendungen, auch Schnittstellen für den internen und externen Gebrauch zur Verfügung stellen. Mit den neusten Features ist RAP sehr flexibel was den Aufbau und die Nutzung angeht, weshalb wir die Anwendungen in verschiedene Pattern aufteilen möchten.
Aufbau
Das "Singleton Pattern" wird so genannt, weil es uns genau eine Instanz zurückliefert, die wir dann bearbeiten können. Damit umgehen wir bestimmte Limitations, die zum Beispiel beim Arbeiten mit dem List Report auftreten können. Ein typisches Beispiel dafür ist auch die Business Configuration, die jeweils mit dem Singleton Pattern generiert wird.
Dazu die folgenden Merkmale zur Abgrenzung:
- Root und Child über fixen Schlüssel verbunden
- Es wird genau eine Instanz zurückgeliefert (Root Ebene)
- List Report wird für die Bearbeitung übersprungen
- Bearbeitung der Daten direkt in der Tabelle (Massenbearbeitung)
- Consumption Layer Optional
Beispiel
Das erste Beispiel, welches dir in den Sinn kommen sollte, ist das Singleton Pattern, das verwendet wird, um eine Business Configuration zu generieren. Hintergrund ist dabei, dass wir direkt die Tabelle komplett editieren wollen und dabei nicht jeweils pro Zeile auf die Object Page springen wollen. Dies wird erreicht, indem wir direkt vom List Report über das Singleton auf die Object Page springen und dort im Edit-Modus ankommen. Dies hat den Vorteil, dass wir die Tabelle im Fullscreen pflegen können. Ein weiteres Beispiel, ähnlich dessen, ist auch ein Formular. Deshalb bauen wir uns im nächsten Beispiel ein Formular für die Dateneingabe auf, welches der User am Ende abschicken kann.
Modellierung
In diesem Kapitel generieren wir uns zuerst das Datenmodell bis hoch zum Service, um unserer Anwendung alle nötigen Daten zur Verfügung zu stellen.
Datenmodell
Das Datenmodell gestalten wir relativ einfach. Wir haben auf der obersten Ebene den Singleton. Hier haben wir zusätzlich administrative Felder, die wir für unser Draft-Handling benötigen. Hier können wir aber auch zusätzlich Daten des Eingabeformulars speichern. Diese kann der User später über das Formular eingeben. Direkt darunter befindet sich eine Eingabetabelle für Teilnehmer. Hier wollen wir einfach nur die Namen eingeben lassen und können über das Formular neue Einträge hinzufügen. Hier ist zum Beispiel nicht vorgesehen, eine Object Page anzulegen, da alles auf einer Seite passieren soll. Im Gegensatz zum Singleton verwenden wir hier auch noch eine zusätzliche User-ID, um die Daten der unterschiedlichen User über die verschiedenen Formulare abgrenzen zu können.
Im Datenmodell oben dienen die Tabellen lediglich dazu, Struktur zu geben. Hier wollen wir eigentlich keine Daten speichern, sondern diese sollen später über den Save-Button in einem gesonderten Prozess behandelt werden.
Core Data Service
Den Core Data Service modellieren wir dann auf der Tabelle, um die Struktur zu übernehmen. Den eigentlichen Select ziehen wir hier wieder aus der Language-Tabelle. Dies ist ein Standard, wenn es um das Singleton in der Business Configuration geht. So ist immer sichergestellt, dass wir genau einen Eintrag erhalten. Dadurch, dass wir aber noch die User-ID verwenden, sollten wir sicherstellen, dass hier auch immer unser aktueller User vorhanden ist. Eine weitere wichtige Eigenschaft ist der semantische Key: Diesen definieren wir auf dem Navigation Key als einheitliches Kriterium für die Navigation. Diesen benötigst du später, um in den Editor zu wechseln und über den Navigation Key zu navigieren, da wir nicht immer den aktuell angemeldeten User kennen.
@ObjectModel.semanticKey: [ 'NavigationKey' ]
define root view entity ZBS_R_SinEntityTP
as select from I_Language
left outer join zbs_tst_single as _Single on 0 = 0
composition of exact one to many ZBS_I_SinParticipant as _Participant
{
key 1 as NavigationKey,
key $session.user as UserID,
_Single.name as UserName,
_Single.random_number as RandomId,
_Single.local_created_by as LocalCreatedBy,
_Single.local_last_changed_by as LocalLastChangedBy,
_Single.local_last_changed as LocalLastChangedAt,
_Single.last_changed as LastChangedAt,
_Participant
}
where
I_Language.Language = $session.system_language
Die Child-Entität der Teilnehmer modellieren wir dann gegen die zweite Tabelle und definieren die "to-parent" Association, damit diese beiden miteinander verbunden sind. Den Schlüssel des Singletons definieren wir lokal, ebenso wie die User-ID. Diese kommen soweit nicht aus dem Datenmodell bzw. der Tabelle unterhalb.
define view entity ZBS_I_SinParticipant
as select from zbs_tst_sinpa
association to parent ZBS_R_SinEntityTP as _MainForm on $projection.NavigationKey = _MainForm.NavigationKey
and $projection.UserID = _MainForm.UserID
{
key uuid as ParticipantKey,
participant as Participant,
@Consumption.hidden: true
1 as NavigationKey,
@Consumption.hidden: true
$session.user as UserID,
_MainForm
}
Verhaltensdefinition
Die Verhaltensdefinition modellieren wir dann auf unserem Datenmodell. Dabei handelt es sich um eine Managed-Implementierung, die erst einmal so weit nicht vom Standard abweicht. Wir verwenden ebenfalls eine Draft Query, um den spezifischen Draft für den aktuell angemeldeten User zu erhalten. Dies ist wichtig, da wir nicht alle Datensätze zur ID (Singleton) zurückgeben wollen, sondern nur die Datensätze, die dem User gehören und die du auch in deinem Formular sehen sollst. Zusätzlich definieren wir den Unmanaged Save, da wir später beim Speichern der Daten die Informationen nicht in den Tabellen speichern wollen, sondern zum Beispiel für eine Weiterverarbeitung in einer zweiten Tabelle ablegen möchten.
define behavior for ZBS_R_SinEntityTP alias Singleton
draft table zbs_tst_singled query ZBS_I_SinSingletonDraft
lock master total etag LastChangedAt
authorization master ( instance )
with unmanaged save
Auf der Ebene der zweiten Entität definieren wir noch eine Determination, um die User-ID bei der Anlage zu befüllen. Grundsätzlich ist die Verbindung über den Core Data Service hergestellt, aber oft kommt das Feld im Draft ohne User-ID an. Deshalb müssen wir hier zusätzlich noch die Information befüllen, damit über die Draft-Query nur die passenden Datensätze zurückgegeben werden.
determination FillUserID on modify { create; }
Verhaltensimplementierung
In der Verhaltensimplementierung benötigen wir zwei Methoden, die wir implementieren müssen. Als Erstes definieren wir die Determination, die für uns den Usernamen per Draft aktualisiert. Hier reicht es, ein EML-Statement abzusetzen, um das Update der Daten anzustoßen. Wir übergeben die User-ID und setzen das Control-Flag, um den Datensatz nach Erstellung auf der Datenbank zu aktualisieren.
MODIFY ENTITIES OF ZBS_R_SinEntityTP IN LOCAL MODE
ENTITY Participant
UPDATE FROM VALUE #( FOR key IN keys
( %tky = key-%tky
UserID = sy-uname
%control-UserID = if_abap_behv=>mk-on ) ).
Als Zweites müssen wir die Methode SAVE_MODIFIED implementieren, um unsere eigentliche Speicherlogik zu definieren. Dabei legen wir die Daten in einer neuen Tabelle ab und simulieren damit eine Weiterverarbeitung der Daten. Zuerst summieren wir alle Teilnehmer, um die Anzahl der eingegebenen Teilnehmer zu erhalten, und aktualisieren zum Abschluss dann die Daten auf der Datenbank, um zu sehen, dass der Speichervorgang funktioniert hat.
LOOP AT update-singleton INTO DATA(new_dataset).
DATA(number_participants) = 0.
LOOP AT create-participant TRANSPORTING NO FIELDS WHERE NavigationKey = new_dataset-NavigationKey.
number_participants += 1.
ENDLOOP.
INSERT zbs_sin_result FROM @( VALUE #( uuid = xco_cp=>uuid( )->value
created_by = new_dataset-NavigationKey
created_at = utclong_current( )
user_name = new_dataset-UserName
random_id = new_dataset-RandomId
number_of_participants = number_participants ) ).
ENDLOOP.
Fiori App
Um ein gutes Formular zu erhalten, müssen wir die Fiori-Anwendung generieren und ins System deployen, um sie am Ende auch testen zu können.
Generator
Standardmäßig verwenden wir hier wieder den Generator „from Template“ und generieren uns eine Fiori-Elements-Anwendung. Dabei verwenden wir in diesem Fall das „Form Entry Object Page“-Element und nicht den List Report, wie wir es sonst bisher getan haben. Hintergrund dazu ist, dass es sich hierbei um eine Object Page handelt und wir direkt nach dem Einstieg in die App auf der Object Page landen und nicht in einer Liste. Daher hält das Template einige Spezialitäten bereit.
Als Service verwenden wir hier unseren Singleton-Service und definieren zusätzlich eine Deployment- und Fiori-Launchpad-Konfiguration, da wir später nach dem Deployment die Anwendung auch im Launchpad testen wollen.
Navigation
Nach dem Generieren der Anwendung gehen wir in die Page Map. Dort löschen wir die Object Page für die Teilnehmer, sollte diese vorhanden sein bzw. durch die Generierung erzeugt worden sein. Dies hat den Hintergrund, dass wir keine Navigation wollen, wenn wir auf den Eintrag klicken. Ebenfalls sollen neue Datensätze direkt in der Tabelle angelegt werden, ohne eine entsprechende Navigation auszulösen. Hier sollst du am Ende in einem Formular arbeiten.
Ebenfalls navigieren wir in die Object Page des Singletons und prüfen, ob es hier Warnungen gibt. In unserem Fall zum Beispiel sehen wir eine Warnmeldung, die sich bis runter auf die Teilnehmertabelle zieht. Hier kannst du die Elemente aufklappen, bis du ganz unten angekommen bist.
Markiere dann die Zeile mit der Tabelle und schaue in den Eigenschaften, wo die Warnmeldung herkommt. In unserer App wurde zum Beispiel der Typ der Tabelle nicht definiert. Das System weiß nicht, ob es eine Responsive oder eine Grid Table ist. Hier definieren wir den Standard für die Responsive Table. Wenn wir bestätigen, sollte die Warnmeldung verschwinden.
Anpassung
Nun führen wir die wichtigste Anpassung durch, damit unsere Anwendung sauber funktioniert und die Navigation am Anfang auch durchgeführt wird. Dazu navigieren wir in den "webapp"-Folder und suchen dort nach der Datei "Component.js". Diese wurde automatisch mit dem Projekt generiert und mit erstem Content befüllt. Hier solltest du eine Funktion für "getStartupParameters" finden, die bereits mit dem "preferredMode" CREATE vorbelegt wurde. Diese Eigenschaft bedeutet, dass beim Starten der App automatisch der Modus CREATE aktiviert und ein neuer Datensatz erzeugt wird. Da wir immer nur einen Datensatz haben, würde es zu einer Fehlermeldung kommen, da der Datensatz bereits existiert und ein Create nicht mehr möglich ist.
getStartupParameters: function() {
return Promise.resolve({
preferredMode: ["create"]
});
}
Dazu setzen wir den "preferredMode" auf EDIT. Das bedeutet, dass wir sofort im Edit Mode landen, wenn wir auf der Object Page angekommen sind. Zusätzlich müssen wir jetzt noch den eigentlichen Schlüssel mitgeben, für den wir in den Edit Mode gehen wollen. Hier kommt nun unser semantischer Schlüssel zum Einsatz, da wir die eigentliche User ID nicht kennen. Aber wir wissen, dass wir einen Datensatz erhalten, bei dem der NavigationKey auf 1 gesetzt wurde. Entsprechend setzen wir dieses Attribut für die Navigation.
getStartupParameters: function() {
return Promise.resolve({
preferredMode: ["edit"],
NavigationKey: "1"
});
}
Hinweis: Zusätzlich ist zu beachten, dass der NavigationKey auf dem semantischen Schlüssel basiert. Hier haben wir einiges ausprobiert und mit dem Debugger geschaut: Dieser muss immer einstellig sein, da es intern im JavaScript-Framework eine Prüfung gibt, die die Länge des angegebenen Schlüssels überprüft. Gibst du einen längeren Schlüssel an, weil deine Entität diesen hat, kann das Framework keine Zuordnung mehr durchführen und du landest auf einer leeren Seite.
Test
In unserem Test starten wir die Anwendung aus dem Launchpad heraus und landen direkt in der Eingabe, ohne eine weitere Taste drücken zu müssen. Wir geben die Informationen im oberen Bereich ein, wie den Organisator und eine zufällige Nummer, und können dann über den Create-Button über der Tabelle neue Einträge anlegen, die wir dann auch mit zwei Teilnehmern befüllen. Sind wir damit fertig, können wir über SAVE die Speicherlogik aufrufen und die Verarbeitung anstoßen.
Prüfen wir dann unsere Tabelle, die für die Dummy-Verarbeitung verantwortlich ist, finden wir dort den neuen Datensatz mit der Nummer, dem Namen und der Anzahl der Teilnehmer, die wir gespeichert haben. Damit hat der Prozess soweit funktioniert. Wie du bereits oben gesehen hast, ist das Formular nach dem Speichern erst einmal geschlossen und du kannst keine weitere Eingabe vornehmen. Hier reicht es, einmal Refresh zu drücken und die Anwendung neu zu laden, schon ist das Formular direkt wieder offen und kann für die nächste Eingabe genutzt werden.
Vollständiges Beispiel
Das vollständige Beispiel findest du bei uns im GitHub Repository und über den Commit findest du alle Änderungen. Innerhalb des Repository befindet sich das Beispiel im Paket ZBS_DEMO_RAP_PATTERN_SINGLETON. Die App wurde in das Fiori Paket deployt, um sie testen zu können.
Zusammenfassung
Das Singleton-Pattern hat die Eigenschaft, genau einen Datensatz zurückzugeben, um dir die Möglichkeit zu geben, an demselben Datensatz weiterzuarbeiten. Dies wird sehr oft für die Business Configuration verwendet, da hinter der Business Configuration auch ein Sperrobjekt steht und zum Beispiel auch ein Transportauftrag, der dann im Header gespeichert wird, während die Einträge gepflegt werden.
Eine zweite Möglichkeit, die wir in diesem Artikel gezeigt haben, ist eine Formulareingabe, die natürlich nicht nur für einen User funktionieren soll, sondern für unterschiedliche User. Dies ist möglich, indem wir noch ein User-Feld ergänzen und dann mit dem semantischen Schlüssel arbeiten. Zusätzlich ist unsere Anwendung auch noch draft-enabled. Das heißt: Wenn du die Daten eingibst, das Formular verlässt und später wieder aufrufst, sind bereits alle Daten vorbefüllt und du kannst genau dort weiterarbeiten, wo du aufgehört hast. Hier kannst du auch noch zusätzliche Aktionen implementieren, um das Formular per Klick auf wieder zu bereiningen in dem du die Felder im Draft löscht.
Fazit
Neben der Verwendung im Standard ist das Singleton Pattern vor allem für Formulare sehr gut verwendbar. Es bietet Vorteile wie eine zentrale Instanz zum Speichern, aber auch zum Sperren und zur Ablage von weiteren Informationen. Vor allem ist es aber ein Zwischenschritt, um auf die eigentliche Object Page für die Bearbeitung zu kommen.
Weitere Informationen:
SAP Help - Preferred Mode








