NoSQL
Dieses Dokument beschreibt den Umgang mit NoSql Datenbanken.
Genutzte Datenbank: MongoDB
Genutzter Datenbanktyp: DocumentDB
Schreiben von Daten
Datenbanktypen des Typs DocumentDB speichern Json-Dokumente beliebiger Struktur. Deshalb ist es möglich jegliche Struktur der SPS in DocumentDBs abzubilden. Dieses Dokument kann mithilfe des FB_JsonDataType automatisch erzeugt oder über die String-Bausteine zusammengebaut werden. Achten Sie darauf, dass die Dokumentvariable groß genug gewählt wird. Falls Sie mehrere Dokumente gleichzeitig hineinschreiben wollen, können Sie diese auch in einem Json-Array übergeben.
Zur Vorbereitung werden zunächst die QueryOptions definiert. Dafür wird die betreffende Collection und der Abfragetyp angegeben. Für jeden Abfragetypen steht eine eigene Struktur zur Verfügung. Für das Schreiben von Dokumenten wird die Struktur T_QueryOptionDocumentDB_Insert genutzt.
VAR
fbNoSQLQueryBuilder_DocumentDB: FB_NoSQLQueryBuilder_DocumentDB;
InsertQueryOptions: T_QueryOptionDocumentDB_Insert;
sDocument : STRING(2000);
END_VAR
InsertQueryOptions.pDocuments:= ADR(sDocument);
InsertQueryOptions.cbDocuments:= SIZEOF(sDocument);
fbNoSQLQueryBuilder_DocumentDB.eQueryType := E_DocumentDbQueryType.InsertOne;
fbNoSQLQueryBuilder_DocumentDB.sCollectionName := 'myCollection';
fbNoSQLQueryBuilder_DocumentDB.pQueryOptions := ADR(InsertQueryOptions);
fbNoSQLQueryBuilder_DocumentDB.cbQueryOptions := SIZEOF(InsertQueryOptions);
Um das Dokument in die Datenbank zu schreiben wird der FB_NoSQLQueryEvt verwendet. Mit der Execute()-Methode werden die übergebenen Dokumente in die Datenbank geschrieben. Diese Ausführung verläuft asynchron zur SPS und kann mehrere Zyklen dauern. Der boolesche Rückgabewert signalisiert, wann der Baustein seinen Prozess abgeschlossen hat:
VAR
fbNoSQLQuery: FB_NoSQLQueryEvt(sNetID := '', tTimeout := TIME#15S0MS);
fbJsonDataType: FB_JsonReadWriteDatatype;
END_VAR
CASE eState OF
…
eMyDbState.Write:
// set the document yourself as json format (Example)
sDocument := '{"myBool" : true,
"Name" : "Some Name Value",
"Value": 2.3,
"Value2":3,
"Child":{"Name":"Single Child",
"Value":1,
"myBool":true,
"arr":[12.0,13.0,14.0,15.0],
"myBool2" : true},
"Children":[
{"Name":"Child1"
,"Value": 1,
"myBool" : true,
"arr":[12.1,13.1,14.1,15.1],
"myBool2" : true},
{"Name":"Child2",
"Value":2,
"myBool" : true,
"arr":[12.2,13.2,14.2,15.2],
"myBool2" : true},
{"Name":"Child3",
"Value":1,
"myBool" : true,
"arr":[12.3,13.3,14.3,15.3],
"myBool2" : true}]
}';
IF fbNoSQLQuery.Execute(1, myQueryBuilder) THEN
IF fbNoSQLQuery.bError THEN
InfoResult := fbNoSQLQuery.ipTcResult;
eState:= eMyDbState.Error;
ELSE
eState:= eMyDbState.Idle;
END_IF
END_IF
…
END_CASE
Die Datenbanken erkennen, mit welchem Datentyp die einzelnen Variablen gespeichert werden. Jedoch kann der Datentyp, wie bei MongoDB, explizit angegeben werden. Falls ein Timestamp explizit als Datentyp abspeichert werden soll, muss dieser im Json-Dokument definiert werden:
sDocument := '{…"myTimestamp": ISODate("2019-02-01T14:46:06.0000000"), …}';
Der String kann nicht nur über die String-Formatierungsbausteine der TwinCAT 3 Bibliotheken formatiert werden, sondern auch über Hilfsbausteine für Json-Dokumente, wie dem FB_JsonReadWriteDatatype aus der Tc3_JsonXml.
// set the document by JsonDataType
sTypeName := fbJsonDataType.GetDatatypeNameByAddress(SIZEOF(anyValue[1]), ADR(anyValue[1]));
sDocument := fbJsonDataType.GetJsonStringFromSymbol(sTypeName, SIZEOF(anyValue [1]), ADR(anyValue [1]));
Lesen von Daten
Das Datenschema in der dokumentbasierten Datenbank kann für jedes Dokument unterschiedlich sein. Im Gegensatz dazu folgt die SPS ohne Weiteres einem festen Prozessabbild. Es kann sein, dass die Daten dem Prozessabbild nicht entsprechen.
In der Datenbank stehen zwei verschiedene Arten zur Verfügung, um Daten auszulesen. Die Find-Abfrage und die Aggregation. Beide liefern Ergebnisse aus der Datenbank zurück, wobei die Aggregation erweiterte Möglichkeiten bietet, die Daten in eine entsprechende Form zu bringen oder Operationen auszuführen, um beispielswese Mittelwerte direkt zu errechnen.
Zur Vorbereitung werden zunächst die QueryOptions definiert. Dafür werden die betreffende Collection und der Abfragetyp angegeben. Für jeden Abfragetypen steht eine eigene Struktur zur Verfügung. Für das Aggregieren von Dokumenten wird die Struktur T_QueryOptionDocumentDB_Aggregation genutzt.
VAR
fbNoSQLQueryBuilder_DocumentDB: FB_NoSQLQueryBuilder_DocumentDB;
AggregationQueryOptions: T_QueryOptionDocumentDB_Aggregate;
sPipeStages: STRING(1000);
END_VAR
AggregationQueryOptions.pPipeStages := ADR(sPipeStages);
AggregationQueryOptions.cbPipeStages := SIZEOF(sPipeStages);
fbNoSQLQueryBuilder_DocumentDB.eQueryType := E_DocumentDbQueryType.Aggregation;
fbNoSQLQueryBuilder_DocumentDB.sCollectionName := 'myCollection';
fbNoSQLQueryBuilder_DocumentDB.pQueryOptions := ADR(AggregationQueryOptions);
fbNoSQLQueryBuilder_DocumentDB.cbQueryOptions := SIZEOF(AggregationQueryOptions);
Um die Aggregationsabfrage abzuschicken wird der FB_NoSQLQueryEvt verwendet. Mit der ExecuteDataReturn()-Methode werden die Parameter übermittelt und die zurückgelieferten Daten in die übergebene Speicherreferenz gelegt. Diese Ausführung verläuft asynchron zur SPS und dauert mehrere Zyklen. Der boolesche Rückgabewert signalisiert, wann der Baustein seinen Prozess abgeschlossen hat:
VAR
fbNoSQLQuery: FB_NoSQLQueryEvt(sNetID := '', tTimeout := TIME#15S0MS);
fbNoSQLResult: FB_NoSQLResultEvt(sNetID := '', tTimeout := TIME#15S0MS);
END_VAR
CASE eState OF
…
eMyDbState.Aggregation:
sPipeStages :='{$$match :{}}';
IF fbNoSQLQuery.ExecuteDataReturn(1, myQueryBuilder, pNoSqlResult:= ADR(fbNoSQLResult), nDocumentLength=> nDocumentLength)) THEN
IF fbNoSQLQuery.bError THEN
InfoResult := fbNoSQLQuery.ipTcResult;
eState:= eMyDbState.Error;
ELSE
eState:= eMyDbState.Idle;
END_IF
END_IF
…
END_CASE
Die Syntax von sPipeStages hängt vom Datenbanktypen ab. Dieser wird alle Datensätze zurückliefern. Weitere exemplarische Möglichkeiten sind (mit fiktiven Datensätzen):
Operator | Beschreibung |
---|---|
{$$match : {Place : “NorthEast”}} | Alle Datensätze, welche „NorthEast“ als Wert des Elements „Place” tragen. |
{$$project : { myValue : { $arrayElemAt : ["$WindPlantData.RotorSensor", 2]} } } | Liefert alle RotorSensor-Daten vom Arrayelementplatz 2 als „myValue“ zurück. |
{$$project : {RotorAvg : {$avg: "$WindPlantData.RotorSensor"} } } | Liefert den Durchschnittswert des Datenarrays “RotorSensor” als „RotorAvg“ zurück. |
Die vollständige Dokumentation der Operatoren finden Sie bei dem jeweiligen Datenbankhersteller.
Eine Referenz zu den zurückgelieferten Daten liegt nun im FB_NoSQLResultEvt–Funktionsbaustein. Diese können nun als Json Dokumente in einen String oder als Struktur ausgelesen werden. Hier werden die Daten nun direkt in ein Array einer passenden Struktur gelesen. Über den SQL Query Editors des Database Servers haben Sie die Möglichkeit direkt eine passende Struktur zu generieren, die zum Datensatz passt. Statt eines Arrays ist es auch möglich beim Abruf von nur einem Datensatz eine Adresse zu einer einzelnen Struktur zu hinterlegen.
VAR
fbNoSQLResult: FB_NoSQLResultEvt(sNetID := '', tTimeout := TIME#15S0MS);
aRdStruct : ARRAY [0..9] OF ST_MyCustomStruct;
fbNoSqlValidation : FB_NoSQLValidationEvt(sNetID := '', tTimeout := TIME#15S0MS);
END_VAR
CASE eState OF
…
eMyDbState.ReadStruct:
IF fbnoSQLDBResult.ReadAsStruct(0, 4, ADR(aRdStruct), SIZEOF(aRdStruct), bValidate := TRUE, ADR(fbNoSqlValidation), bDataRelease:= TRUE) THEN
IF fbnoSQLDBResult.bError THEN
InfoResult := fbnoSQLDBResult.ipTcResult;
eState:= eMyDbState.Error;
ELSE
eState:= eMyDbState.Idle;
END_IF
END_IF
…
END_CASE
Der TwinCAT Database Server achtet bei der Zuordnung zwischen Datensatz und Struktur auf die Namen der Elemente im Datensatz und die Namen der Variablen. Sollen diese sich unterscheiden, kann in der SPS das Attribut „ElementName“ genutzt werden:
TYPE ST_WindFarmData :
STRUCT
{attribute 'ElementName' := '_id'}
ID: T_ObjectId_MongoDB;
{attribute 'ElementName' := 'Timestamp'}
LastTime: DT;
{attribute 'ElementName' := 'WindPlantData'}
Data: ST_WindFarmData_WindPlantData;
END_STRUCT
END_TYPE
„ElementName“ gibt in diesem Beispiel an, wie die Daten im Dokument der Datenbank heißen. Mit Hilfe des Startindizes und Angabe der Datensatzanzahl kann außerdem bei diesem Aufruf bestimmt werden, welche Datensätze zurückgeliefert werden sollen. Um mögliche Doppelungen zu vermeiden wird darauf hingewiesen, dass diese Optionen bereits mit Operatoren in den „PipeplineStages“ durchgeführt werden können.
Validieren von Daten
Falls es beim FB_NoSqlResult Konflikte zwischen dem Datensatz und der Struktur in der SPS gab, können sie mit dem FB_NoSQLValidationEvt ausgelesen werden. Konflikte können beispielsweise fehlende oder übrig gebliebene Datensätze, aber auch Datentypprobleme sein. Mit der Methode GetIssues() können alle Konflikte als Array von Strings ausgelesen werden. Übrig gebliebene Daten, die nicht in der SPS-Struktur wiedergefunden wurden, können als Array von Strings im Json-Format über GetRemainingData() ausgelesen werden. Gegebenenfalls können diese dann noch einmal separat in die richtige Struktur ausgelesen werden oder aber über die TwinCAT Json Bibliothek interpretiert werden.
VAR
fbNoSqlValidation : FB_NoSQLValidationEvt(sNetID := '', tTimeout := TIME#15S0MS);
aIssues : ARRAY[0..99] OF STRING(512);
aRemaining : ARRAY [0..9] OF STRING(1000);
END_VAR
CASE eState OF
…
eMyDbState.ValidationIssues:
IF fbValidation.GetIssues(ADR(aIssues), SIZEOF(aIssues), FALSE) THEN
IF fbValidation.bError THEN
InfoResult := fbValidation.ipTcResult;
eState:= eMyDbState.Error;
ELSE
eState:= eMyDbState.Idle;
END_IF
eMyDbState.ValidationRemaining:
IF fbValidation.GetRemainingData(ADR(aRemaining), SIZEOF(aRemaining), SIZEOF(aRemaining[1]), bDataRelease:= FALSE)THEN
IF fbValidation.bError THEN
InfoResult := fbValidation.ipTcResult;
eState:= eMyDbState.Error;
ELSE
eState:= eMyDbState.Idle;
END_IF
…
END_CASE
Download: TF6420_BestPractise_NoSql.zip