SAP HANA – Erfahrungen mit der In-Memory-Datenbank, Teil 4: Eine HANA-Anwendung

In den ersten beiden Beiträgen zu dieser Blogreihe haben wir uns in erster Linie mit den Möglichkeiten von SAP HANA zur Performancesteigerung auf Datenbank-Ebene auseinandergesetzt. Der dritte Teil beinhaltete grundsätzliche Überlegungen und (subjektive) Einschätzungen zu den Einsatzmöglichkeiten von SAP HANA.

In diesem vierten Beitrag wollen wir eine einfache Anwendung für SAP HANA bauen und hierbei insbesondere auf die HANA-spezifischen Anteile eingehen.

Rahmenbedingungen

Als Grundlage für unsere Beispielanwendung soll unser Datenmodell aus dem ersten Teil der Blogreihe verwendet werden. Die Beispielanwendung wird als native SAP HANA-Anwendung erstellt.

Native SAP HANA-Anwendungen laufen in der HANA XS-Engine. In unserem Beispiel verwenden wir die folgenden Technologien:

  • Zwei HANA-Datenbank-Tabellen (als Erweiterung zu den Tabellen aus dem ersten Teil der Blogreihe)
  • Eine HANA-SQL-View zur Vereinfachung des Datenzugriffs
  • OData–Service zur Bereitstellung der einzelnen Datensätze
  • JSON-Service auf der Basis von server-side JavaScript zur Bereitstellung von Zählwerten
  • Ein einfaches SAP UI5-Frontend zum Aufruf der Services und zur Darstellung der Ergebnisse.

In diesem Beitrag wollen wir die Erzeugung und das Zusammenspiel der HANA-spezifischen Anteile der Anwendung schwerpunktmäßig betrachten.

Eine hervorragende generelle Einführung in die Erzeugung von HANA XS-Applikationen findet sich in „8 Easy Steps to Develop an XS application on the SAP HANA Cloud Platform“. Auf diese Einführungsschritte werden wir in diesem Beitrag nicht mehr explizit eingehen.

Die Datenbasis für unser Beispiel bilden die folgenden Tabellen:

Tabelle sxxxtrial.pkennzeichen.KfzDemo.data::kennzeichen

table.schemaName = "NEO_...";
table.tableType = COLUMNSTORE;
table.description = "Kfz-Kennzeichen";
table.columns = [
 {name = "lkr"; sqlType = NVARCHAR; nullable = false; length=3;},
 {name = "buchstaben"; sqlType = NVARCHAR; nullable = false; 
          length=2;},
 {name = "ziffern"; sqlType = NVARCHAR; nullable = false; 
          length=4;},
 {name = "b1"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "b2"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "z1"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "z2"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "z3"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "z4"; sqlType = NVARCHAR; nullable = false; length=1;},
 {name = "status"; sqlType = NVARCHAR; nullable = false; 
          length=1;},
 {name = "name"; sqlType = NVARCHAR; nullable = true; length=50;},
 {name = "vorname"; sqlType = NVARCHAR; nullable = true; 
          length=50;},
 {name = "strasse"; sqlType = NVARCHAR; nullable = true; 
          length=50;},
 {name = "plz"; sqlType = NVARCHAR; nullable = true; length=5;},
 {name = "ort"; sqlType = NVARCHAR; nullable = true; length=50;}
];
table.primaryKey.pkcolumns = ["lkr", "buchstaben", "ziffern"];

und Tabelle sxxxtrial.pkennzeichen.KfzDemo.data::status

table.schemaName = "NEO_...";
table.tableType = COLUMNSTORE;
table.description = "Status-Werte";
table.columns = [
   {name = "status"; sqlType = NVARCHAR; nullable = false; 
            length=1;},
   {name = "bezeichnung"; sqlType = NVARCHAR; nullable = false; 
            length=15;}
];
table.primaryKey.pkcolumns = ["status"];

Für die Erstellung der Anwendung haben wir unser Datenmodell aus dem ersten Blog-Teil erweitert und die Tabellen mit entsprechenden Daten gefüllt. Die Tabelle kennzeichen enthält 7.020.000 Datensätze und die Tabelle status enthält 4 Datensätze.

Zur einfachen Darstellung der Datensätze in einer Tabelle wird die SQL-View vkennzeichen verwendet:

table.schemaName = "NEO_...";
query=
  "SELECT \"lkr\", \"buchstaben\", \"ziffern\", 
          \"bezeichnung\", \"kenn\".\"status\", 
          \"b1\", \"b2\", \"z1\", \"z2\", \"z3\", \"z4\"
   FROM \"NEO_.\".\"sx.pkennzeichen.KfzDemo.data::kennzeichen\" 
           as \"kenn\" 
   inner join  \"NEO_.\".\"sx.pkennzeichen.KfzDemo.data::status\" 
           as \"stat\"
   on \"stat\".\"status\" = \"kenn\".\"status\"";
depends_on_table=["sx.pkennzeichen.KfzDemo.data::kennzeichen",
                  "sx.pkennzeichen.KfzDemo.data::status"];

Darstellung der Oberfläche

HANA1-500px

 Die Oberfläche besteht aus folgenden Teilen:

  • Eingabemöglichkeit für ein Kfz-Kennzeichen-Muster mit „*“ als Platzhalter für beliebige Zeichen („Kfz-Kennzeichen“ links oben)
  • Numerische Darstellung der Anzahl von Kennzeichen zu diesem Muster, gruppiert nach dem Status der Kennzeichen („Werte“ links mittig)
  • Grafische Darstellung dieser Anzahlwerte darunter
  • Buttons, um den Status der Kfz-Kennzeichen gemäß Muster zu ändern („Aktionen“ mittig oben)
  • Einzel-Darstellung der Kfz-Kennzeichen gemäß Muster („ausgewählte Kennzeichen“ rechts).

Bei einer Änderung des Kfz-Kennzeichen-Musters werden die abhängigen Teile  („Werte“, Grafik und „ausgewählte Kennzeichen“) beim Verlassen des Feldes automatisch abgeändert.

Anmerkung: Die Oberfläche könnte sicher schöner und auch funktional anders aufgebaut sein. Das ist aber nicht der Fokus der Beispielanwendung.

Programmanteile Server

Auf Serverseite werden durch das Programm zwei Services zur Verfügung gestellt:

  • Ein JSON-Service zur Ermittlung der Anzahlwerte für das eingegebene Kfz-Kennzeichen-Muster. Dieser Service wird auch benutzt, um die Statuswerte zum Kennzeichen-Muster als Reaktion auf die Buttons zu ändern.
  • Ein OData-Service zur Bereitstellung der Einzel-Datensätze zum Kennzeichenmuster.

Die Verwendung zweier unterschiedlicher Technologien macht an dieser Stelle durchaus Sinn: Daten aus dem JSON-Service werden immer komplett an den Client übertragen und eignen sich deshalb nur für kleinere Datenmengen. Daten aus dem OData-Service werden bei Bedarf nachgelesen, so dass hier auch große Datenmengen sinnvoll zwischen Server und Client ausgetauscht werden können.

Die Bereitstellung des OData-Service auf der Serverseite erfolgt völlig unspektakulär über die Datei kennzeichen.xsodata:

service  {
  "NEO_..."."sxxxtrial.pkennzeichen.KfzDemo.data::vkennzeichen" 
            as "VKennzeichen"
  keys ("lkr","buchstaben", "ziffern");
}

Hierdurch wird die oben definierte SQL-View vkennzeichen unter dem Namen „VKennzeichen“ als OData-Datenquelle bekanntgegeben. Die konkrete Anforderung der Daten von diesem Service erfolgt über Aufruf-Parameter, die sowohl die auszuwählenden Datenfelder als auch die anzuwendenden Filter angeben. Diese Steuerung werden wir bei den JavaScript-Anteilen des Frontends besprechen.

Der JSON-Service wird über server-side JavaScript bereitgestellt. Durch die Verwendung von JavaScript sind wir hier deutlich flexibler, allerdings ist auch der Programmieraufwand deutlich höher als beim OData-Service. Es sei aber nochmals daran erinnert, dass JSON nur für kleinere Datenmengen sinnvoll ist, weil immer die gesamte Ergebnismenge an den Client übermittelt wird.

Intern verwendet dieser Service ein SQL-Select-Statement, um die gewünschten Daten zu besorgen. Für die Filterung der Datenmenge müssen die Aufrufparameter des Services korrekt ausgewertet werden. Diese Aufrufparameter werden in die WHERE-Bedingung des Select-Statements überführt.

//
// Parameter auswerten
//
var lkr = $.request.parameters.get("lkr");
if (lkr === undefined)  { lkr = "AAA"; }
lkr = lkr.toUpperCase();
var sWhere = "\"lkr\" = '" + lkr + "' ";
var i;
for (i = 0; i < $.request.parameters.length; ++i) {
  var name = $.request.parameters[i].name.toUpperCase();
  var value = $.request.parameters[i].value.toUpperCase();
  if (value !== "*" & value !== undefined) {
    if (name === "B1") {sWhere += "and \"b1\"='" + value + "' ";}
    if (name === "B2") {sWhere += "and \"b2\"='" + value + "' ";}
    if (name === "Z1") {sWhere += "and \"z1\"='" + value + "' ";}
    if (name === "Z2") {sWhere += "and \"z2\"='" + value + "' ";}
    if (name === "Z3") {sWhere += "and \"z3\"='" + value + "' ";}
    if (name === "Z4") {sWhere += "and \"z4\"='" + value + "' ";}
  }
}

Anschließend wird das Statement ausgeführt und das Result-Set in internen Variablen ausgewertet.

//
//Statistische Werte bestimmen
//
var connection = $.db.getConnection();
var statement = null;
var resultSet = null;

var frei = 0;
var reserviert = 0;
var belegt = 0;
var gesperrt = 0;

var select_kennzeichen = 
 "SELECT \"status\", count(*) " +
 " FROM \"NEO_...\".\"sx.pkennzeichen.KfzDemo.data::kennzeichen\" " +
 " WHERE " + sWhere +
 " group by \"status\"";

try {
   statement = connection.prepareStatement(select_kennzeichen);
   resultSet = statement.executeQuery();
   while (resultSet.next()) {
      var status = resultSet.getString(1);
      if (status === "F") { frei       = resultSet.getInteger(2);}
      if (status === "R") { reserviert = resultSet.getInteger(2);}
      if (status === "B") { belegt     = resultSet.getInteger(2);}
      if (status === "S") { gesperrt   = resultSet.getInteger(2);}
   }
}
catch (e) { frei_result.error = e.toString(); }

Anschließend werden die Ergebnisse als JSON-Liste aufbereitet. Die Struktur der Ergebnisse ist so aufgebaut, dass die unterschiedlichen Statuswerte immer in derselben Zeile der Ergebnisliste erscheinen – unabhängig davon, ob überhaupt alle Statuswerte im konkreten Kennzeichen-Muster enthalten sind. Dadurch wird eine direkte Datenbindung innerhalb von SAP UI5 ermöglicht.

Die hier verwendete Organisation des Ergebnisses zum JSON-Service kann auch direkt von den vorgefertigten grafischen SAP UI5-Elementen verarbeitet werden. Sonst wäre durchaus auch eine andere Struktur für die Ergebnisübermittlung denkbar.

var statsList = [];
var frei_result = {};
var reserviert_result = {};
var belegt_result = {};
var gesperrt_result = {};

frei_result.status       = "frei";
frei_result.value        = frei;
reserviert_result.status = "reserviert";
reserviert_result.value  = reserviert;
belegt_result.status     = "belegt";
belegt_result.value      = belegt;
gesperrt_result.status   = "gesperrt";
gesperrt_result.value    = gesperrt;

statsList.push(frei_result);
statsList.push(reserviert_result);
statsList.push(belegt_result);
statsList.push(gesperrt_result);

$.response.status = $.net.http.OK;
$.response.contentType = "application/json";
$.response.setBody(JSON.stringify(statsList));

Damit haben wir serverseitig die Voraussetzungen für die Anwendung geschaffen. Sowohl der OData- als auch der JSON-Service können direkt über die Eingabe der URL und die Angabe entsprechender Parameter im Web Browser getestet werden.

Aufbau Bildschirm und Data Binding

Bevor wir auf die Verwendung der Services eingehen, müssen wir noch den Aufbau der Bildschirmmaske und insbesondere die Verknüpfung der Oberflächenelemente mit den Datenwerten erklären.

Wir wollen uns an dieser Stelle wirklich auf die Datenverknüpfung beschränken. Auch diese ist Bestandteil des Sprachumfangs von SAP UI5 (bzw. deren Grundlage jQuery), erläutert aber das Zusammenspiel mit den Services der SAP HANA – Datenbank.

Die Verknüpfung der Oberflächenelemente mit den Daten wird bei der Definition der Oberflächenelemente angegeben. Zunächst wollen wir dies beispielhaft am Wertefeld für die „Gesperrte Kennzeichen“ zeigen:

var sperrFeld  = new sap.ui.commons.TextField({
    id: "sperr",                            // TextField mit ID
    editable: false,                        // read-only
    width: "80px",                          // Breite
    textAlign: sap.ui.core.TextAlign.Right, // rechtsbündige Zahl
    value:                                  // data binding
    { path: "/3/value",              // Datenfeld aus dem Service
      type: new sap.ui.model.type.Integer(  // Formatierung als 
        {groupingEnabled: true,             // Integer mit 
         groupingSeparator: "."})           // Tausenderpunkten
    }
});

Für die Anbindung an die Daten ist die Property value relevant. Die Angabe „/3/value“ besagt, dass der Wert aus dem dritten Datensatz des angebundenen Datenmodells und dort aus dem Datenfeld value entnommen werden soll.Die explizite Angabe eines Datensatzes ist in der Regel nicht notwendig (und in der Regel auch nicht sinnvoll), ist aber in unserer Konstellation der Tatsache geschuldet, dass unser Datenmodell auch direkt von der grafischen Darstellung der Werte verarbeitet werden soll.

Die Anbindung der Daten an die grafische Darstellung der Werte erfolgt ebenfalls bei der Definition der Grafik:

// create a Donut
var oChart = new sap.viz.ui5.Donut({
  width : "90%",                    // Breite
  height : "280px",                 // Höhe
                                    // Datenbindung
  dataset : new sap.viz.ui5.data.FlattenedDataset({
     dimensions : [
       { axis : 1, name : 'Status', value : "{status}" }               ],
     measures : [
       { name : 'Anzahl', value : '{value}' }
     ],
     data : { path : "/" }
  })
});

Die Datenbindung erfolgt über die Property dataset und dort wiederum über die Angabe der Wurzel der Daten („path“), die Dimension unterhalb der Wurzel ({status}) und die zugehörige Kennzahl ({value}). Diese IDs haben wir auch bei der Definition des JSON-Services und bei der Verknüpfung der Einzeldatenfelder (siehe sperrFeld) verwendet.

Hier haben wir also ein Beispiel, in dem EIN Datenmodell (und EIN Service) durch zwei Konsumenten auf der Client-Seite gleichzeitig verarbeitet wird.

Unser OData-Service wird mit einem Tabellenelement auf der Oberfläche verknüpft. Die Datenverknüpfung findet ebenfalls bei der Definition der Tabelle statt:

var oTable = new sap.ui.table.Table({
    id: "KfzTable",
    title: "ausgewählte Kennzeichen",
    visibleRowCount: 20,
    rowHeight: 20,
    columns : [  
      {label: "Landkreis", template: "lkr", sortProperty: "lkr"  },
      {label: "Buchstaben", template: "buchstaben", 
                            sortProperty: "buchstaben"  },
      {label: "Ziffern", template: "ziffern", 
                            sortProperty: "ziffern"  },
      {label: "Status", template: "bezeichnung", 
                            sortProperty: "bezeichnung", 
                            filterProperty: "bezeichnung" },    
    ]
});

Hier wird zur Verknüpfung das Property template der einzelnen Spalten der Tabelle verwendet, dem die Namen der entsprechenden Spalten unserer Basis –View zugewiesen werden.

Verwendung der Services

Die Oberflächenelemente sind durch diese Definitionen darauf vorbereitet, entsprechende Daten anzuzeigen. Jetzt müssen wir „nur“ noch die Daten (bzw. im Falle OData-Service eine Verknüpfung zu den Daten) zum Client bringen.

Die Verwendung der Services und der Abruf der Daten erfolgt auf der Client-Seite über entsprechende JavaScript-Programmierung:

// Daten initialisieren
var modelQuelle =       // relative Position der Service-Definition
    "services/statistik.xsjs";
var oModelDyn = new sap.ui.model.json.JSONModel();   // JSON-Modell
oModelDyn.loadData(modelQuelle, doParms());       // Daten besorgen
          // Modell als Standard an alle Oberflächenelemente hängen
sap.ui.getCore().setModel(oModelDyn);

Die Funktion doParms wertet das eingegebene Kfz-Kennzeichen-Muster aus und übersetzt es in Aufrufparameter der Methode loadData.

function getValueOf (id) {
   var feld = sap.ui.getCore().getElementById(id);
   feld.setValue(feld.getValue().toUpperCase());
   return feld.getValue();
};
function doParms (aktion) {
   var sParms = "lkr=" + getValueOf ("lkr");
   if (getValueOf("b1") != "*") 
      {sParms = sParms + "&b1=" + getValueOf("b1");};
   if (getValueOf("b2") != "*") 
      {sParms = sParms + "&b2=" + getValueOf("b2");};
   if (getValueOf("z1") != "*") 
      {sParms = sParms + "&z1=" + getValueOf("z1");};
   if (getValueOf("z2") != "*") 
      {sParms = sParms + "&z2=" + getValueOf("z2");};
   if (getValueOf("z3") != "*") 
      {sParms = sParms + "&z3=" + getValueOf("z3");};
   if (getValueOf("z4") != "*") 
      {sParms = sParms + "&z4=" + getValueOf("z4");};
   if (aktion != undefined) 
      {sParms = sParms + "&aktion=" + aktion; };
   return sParms;
};

Über den Aufruf der Methode loadData wird der zugehörige Service auf dem Server aktiviert, die Daten werden zum Client übertragen und alle Datenfelder, die an das Modell gebunden sind, werden automatisch mit Daten versorgt.

Für den OData-Service erfolgt die Anbindung analog:

//Create a model and bind the table rows to this model
var oTabModel = new
sap.ui.model.odata.ODataModel("services/kennzeichen.xsodata/", false);
oTable.setModel(oTabModel);
oTable.bindRows({path: "/VKennzeichen", 
  parameters: {select: 'lkr, buchstaben, ziffern, bezeichnung'}, 
  filters: doFilterList() 
});

mit der Ermittlung der Filterattribute:

function doFilterList () {
   var filterList = [];
   filterList.push( new  sap.ui.model.odata.Filter("lkr",
               [{operator: "EQ", value1: getValueOf ("lkr")}]) );
   if (getValueOf ("b1") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'b1', [{operator:"EQ", value1: getValueOf ("b1")}]));
   };
   if (getValueOf ("b2") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'b2', [{operator:"EQ", value1: getValueOf ("b2")}]));
   };
   if (getValueOf ("z1") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'z1', [{operator:"EQ", value1: getValueOf ("z1")}]));
   };
   if (getValueOf ("z2") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'z2', [{operator:"EQ", value1: getValueOf ("z2")}]));
   };
   if (getValueOf ("z3") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'z3', [{operator:"EQ", value1: getValueOf ("z3")}]));
   };
   if (getValueOf ("z4") != "*") {
      filterList.push (new sap.ui.model.odata.Filter(
          'z4', [{operator:"EQ", value1: getValueOf ("z4")}]));
   };
   return filterList;
};

Über den Aufruf der Methode bindRows erfolgt hier die Verknüpfung mit den selektierten Daten, die dann bei Bedarf an den Client übermittelt und in der Tabelle dargestellt werden.

Die Aktualisierung der Daten als Reaktion auf geänderte Eingaben im Kfz-Kennzeichen-Muster oder auf geänderte Statuswerte durch die Aktions-Buttons erfolgt über dieselben Methoden.  Durch den Aufruf der Services (und die oben beschriebene Definition der Oberflächenelemente) werden die Datenwerte am Bildschirm automatisch aktualisiert und auch Grafik und Tabelle werden automatisch neu aufgebaut.

Update-Vorgang als Reaktion auf die Aktions-Buttons

Wenn ein Aktions-Button getätigt wird, dann wird ebenfalls der JSON-Service statistik.xsjs aufgerufen, der dann einen zusätzlichen Parameter-Wert für aktion erhält.

Die Logik zum Ändern der Statuswerte für das angezeigte Kfz-Kennzeichen-Muster wird als Update-Statement in den Ablauf des Service integriert. Diese Logik beinhaltet auch die Realisierung der definierten Statusübergänge.

//
// Aktion bestimmen und UPDATE durchführen
//
var aktion = $.request.parameters.get("aktion");
if (aktion !== undefined) { aktion = aktion.toUpperCase(); }
var sNewStatus = "N";
var sOldStatus = "N";
// reservieren
if (aktion === "R") { sOldStatus = "F"; sNewStatus = "R"; }    
// belegen
if (aktion === "B") { sOldStatus = "R"; sNewStatus = "B"; }
// sperren    
if (aktion === "S") { sOldStatus = "F"; sNewStatus = "S"; }
// entsperren if (aktion === "E") { sOldStatus = "S"; sNewStatus = "F"; }
// rUecknehmen
if (aktion === "U") { sOldStatus = "R"; sNewStatus = "F"; } 
// freigeben 
if (aktion === "F") { sOldStatus = "B"; sNewStatus = "F"; }
if (sOldStatus !== "N") {    
   try {       
      var update_kennzeichen = 
     "UPDATE  "NEO_...\".
        \"sxtrial.pkennzeichen.KfzDemo.data::kennzeichen\" " +
     " set \"status\" = '" + sNewStatus + "'" +
     " WHERE " + sWhere +          " and \"status\" = '" + 
                  sOldStatus + "'";
     statement = connection.prepareStatement(update_kennzeichen);
     statement.executeUpdate();
     connection.commit();   
   }
   catch (e) { frei_result.error = e.toString(); } 
}

Damit wollen wir die Vorstellung unserer Beispielanwendung beenden und wünschen viel Spaß beim Selber-Ausprobieren.

 

Über den Autor

Bild: CONET, Rolf, Gadorosi
Leiter Business Unit | Beiträge

Rolf Gadorosi leitet bei der CONET Business Consultants GmbH die Business Unit SAP NetWeaver Development & Administration. In dieser Funktion fungiert er auch als organisatorischer und technischer Projektleiter und System Architect im Schwerpunkt mit Konzeption, Analyse, Design, Realisierung und Einführung von Individuallösungen und Erweiterungen auf Basis der SAP-Produktpalette.

1 Antwort

  1. Michelle sagt:

    Dass verschiedene Vorgänge jetzt automatisiert sind ist wirklich von großem Vorteil! Vielen Dank für das Tutorial!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert