Inspiriert durch diese
Frage im Forum, wird im siebten Artikel der mORMot Vorstellungsreihe gezeigt, wie mit Hilfe von
DocVariant und
Virtual TreeView ein Viewer zur Anzeige von Daten im JSON-Format erstellt wird. Im Zusammenspiel zeigen die beiden Komponenten ihre Stärken. Als Datenpool die eierlegende Wollmilchsau DocVariant, hier in Form von
TDocVariantData und der
TVirtualStringTree als ultraschnelle Anzeige für hierarchische Datenstrukturen. Schnell heißt hier, die Anzeige steht für eine 25MB große JSON-Datei in weniger als 100 Millisekunden. Ich hoffe, die Lust auf Weiterlesen ist geweckt.
Die enthaltenen Verweise zur Dokumentation verlinken zur aktuell verfügbaren
mORMot1 Hilfe. Für das Beispiel wird
mORMot2 verwendet. Die Namen für Klassen und Funktionen können sich leicht unterscheiden. Im Anhang befindet sich der Sourcecode und das ausführbare Programm.
Disclaimer: Der Sourcecode ist weder getestet noch optimiert. Er sollte mit Delphi ab Version 10.3 funktionieren. Die Benutzung der zur Verfügung gestellten Materialien erfolgt auf eigene Gefahr.
Die Beispiel-Anwendung ist ein einfacher JSON-Viewer. Das Laden eines Dokuments mit einer Größe von mehreren 100MBs ist möglich. Begrenzt wird es vom vorhandenen Arbeitsspeicher oder der maximalen Länge eines Strings. Mit dem Beispiel Quelltext für das Programm bekommt man:
- Ein Dokument kann aus einer Datei geladen oder über das Clipboard importiert werden. Es lässt sich wieder im gepackten Format als Datei speichern.
- Einzelne Zweige eines Dokuments können als Datei gespeichert oder als Objekt mit Erweiterung Stammknoten als Attribut ins Clipboard exportiert werden.
- Jeder Zweig kann in einem eigenen Ansichtsfenster ausgelagert dargestellt und mit Doppelklick auf den Reiter geschlossen werden. Bei multipler Ansicht ist die Löschfunktion gesperrt.
- Einen einfachen Editor, um Werte von Attributen zu bearbeiten oder Einträge respektive ganze Zweige zu löschen.
Grundsätzliches Vorwissen
Um DocVariant zu verstehen, muss man sich mit dem Typ Variant auskennen. Zur Umsetzung werden keine obskuren Hacks, sondern geschickt die vorhandenen Möglichkeiten genutzt. Wem die Klasse
System.Variants.TInvokeableVariantType etwas sagt, kann den folgenden Absatz überlesen.
Wichtig: Die Ausführungen gelten für
32-Bit und ab Delphi XE7. Für ältere Versionen ist
TVariantManager das Stichwort. Die Erklärungen sind nur rudimentär, deshalb begleitend die beiden Einstiegslinks in die Delphi Hilfe zum weiteren Nachlesen:
System.Variant,
System.Variants.TInvokeableVariantType.
1) Am Anfang der System.Variant
Ein Variant wird als 16-Byte großes Record gespeichert. Die ersten beiden Bytes sind für das Typ-Feld (VType) einer Variante reserviert. Für die System-Typen sind Konstanten definiert, z.B. ist es für Boolean die Konstante varBoolean mit dem Wert 11. Der Wertebereich darf die ersten 12 Bits umfassen. Es können eigene Typen registriert werden. Für diese soll der Typ-Wert ab 272 beginnen. Es gibt Funktionen zum Abfragen des Types:
Delphi-Quellcode:
var
v: Variant;
begin
v := 'Test';
if VarIsType(v, varUString) then
ShowMessage(Format('%d - %s', [VarType(v), VarTypeAsText(VarType(v))])); // Result: 258 - UnicodeString
Ein Variant lässt sich nach
TVarData hart casten. Das ist möglich, weil gilt:
SizeOf(Variant) = SizeOf(TVarData)
und die interne Anordnung nachgebildet wird.
Delphi-Quellcode:
var
v: Variant;
begin
v := True;
if TVarData(v).VType = varBoolean then
begin
var b: Boolean := TVarData(v).VBoolean;
ShowMessage(BoolToStr(b, True)); // Result: True
2) DocVariant, ein Variant mit Struktur
Das Geniale an einem DocVariant ist, dass es eine beliebig komplexe Datenstruktur aus Objekt(en) und/oder Arrays, oder aus Kombinationen von beiden sein kann. Nur der zur Verfügung stehende Arbeitsspeicher ist die Grenze. In der mORMot Hilfe steht: "
A custom variant type used to store any JSON/BSON document-based content - i.e. name/value pairs for objects, or an array of values (including nested documents), stored in a TDocVariantData memory structure". Die DocVariant Syntax sieht für Pascal Entwickler etwas gewöhnungsbedürftig aus, weil es eher an eine Scriptsprache erinnert. Mehr dazu in der
Hilfe nachlesen. Ein DocVariant lässt sich auf mehrere Arten erstellen. Ein Beispiel zum Kennenlernen:
Delphi-Quellcode:
var
v: Variant;
begin
TDocVariant.NewFast(v);
v.a := 10;
v.b := 3.3;
v.c := True;
// DocVariant: Count=3, Value(0)=10, Value('b')=3,30, NameIndex('b')=2
ShowMessage(Format('DocVariant: Count=%d, Value(0)=%d, Value(''b'')=%f, NameIndex(''b'')=%d',
[Integer(v._Count), Integer(v.Value(0)), Double(v.Value('b')), Integer(v.NameIndex('b'))]));
Es wird ein DocVariant vom Typ Objekt mit den Variant-Optionen
[dvoReturnNullForUnknownProperty, dvoValueCopiedByReference] erzeugt. Beim Erstellen kann der Typ und/oder die Variant-Option(en) angeben, oder eine der neun
New... Initialisierungsfunktionen mit sinnvoller Vorauswahl verwendet werden. Was man auch im Beispiel sieht, ist die Möglichkeit Pseudo-Eigenschaften oder -Funktionen aufzurufen. Von diesen gibt es eine stattliche Anzahl:
_Count,
_Kind,
_JSON,
_(idx),
Value(idx),
Value(Name),
Name(idx),
Add(Item),
Add(Name, Value),
Exists(Name),
Delete(idx),
Delete(Name) und
NameIndex(Name).
3) Ein Variant und seine Pseudo-Eigenschaften und -Funktionen
Wie oben beschrieben, besitzt jeder Variant einen Typ-Wert. Dieser Wert ist gleichzeitig
Selektor, der bestimmt, welche Bearbeitungsklasse herangezogen wird. Für selbst definierte Variant-Typen, zum einfachen und schnellen Zugriff auf alle Erweiterungen, verwendet mORMot die eigene Basisklasse
TSynInvokeableVariantType, abgeleitet von
TInvokeableVariantType. Diese Klasse ist Vorfahre der Klassen:
TDocVariant,
TBsonVariant,
TSqlDBRowVariantType und
TOrmTableRowVariant. Mit der Funktion
SynRegisterCustomVariantType werden diese Klassen im System registriert. Eine Objektinstanz der Klasse
TDocVariant wird nach der Registrierung in der Variable
DocVariantType gespeichert. Wichtiger in der Praxis ist die Variable
DocVariantVType. Sie enthält den VarType Wert, mit dem ein DocVariant im System registriert ist. Immer wenn jetzt ein Variant mit dem Typ-Wert
DocVariantVType unterwegs ist, steht im Hintergrund die Klasse
TDocVariant zur Übernahme der eigentlichen Arbeit parat. Am einfachsten lässt es sich nachvollziehen, wenn in der
Unit mormot.core.variants Breakpoints an den Anfang der Funktionen
TSynInvokeableVariantType.DispInvoke,
TDocVariant.DoFunction und
TDocVariant.DoProcedure gesetzt werden. Schritt für Schritt lässt sich dann nachverfolgt, wie die Abläufe im Hintergrund vonstattengehen.
4) TDocVariantData, das Geheimnis hinter DocVariant
Kein Geheimnis, aber eine clevere Idee. Die Struktur von
TDocVariantData ist wie folgt definiert:
Delphi-Quellcode:
TDocVariantData = record
private
// note: this structure uses all TVarData available space: no filler needed!
VType: TVarType; // 16-bit
VOptions: TDocVariantOptions; // 16-bit
VName: TRawUtf8DynArray; // pointer
VValue: TVariantDynArray; // pointer
VCount: integer; // 32-bit
Wer richtig zählt, kommt auf 16-Bytes. Damit gilt:
SizeOf(Variant) = SizeOf(TVarData) = SizeOf(TDocVariantData)
. Wer schreibt
TDocVariant.New(v);
ruft folgende Funktion auf:
Delphi-Quellcode:
procedure TDocVariantData.Init(aOptions: TDocVariantOptions);
begin
VType := DocVariantVType;
aOptions := aOptions - [dvoIsArray, dvoIsObject];
VOptions := aOptions;
pointer(VName) := nil; // to avoid GPF when mapped within a TVarData/variant
pointer(VValue) := nil;
VCount := 0;
end;
Es initialisiert sich eine Struktur zur Aufnahme von Name-Value Pairs. Es stehen über 150 Eigenschaften und Funktionen, davon mehr als 30 unterschiedliche Init-Varianten, zur Verfügung.
5) Immer auf der sicheren Seite
Man kann einen Variant nach TDocVariantData hart casten. Besser ist es, dafür die Funktion
_Safe zu verwenden. Diese Funktion sorgt dafür, dass
immer ein
DocVariant zurückgeliefert wird. Wenn kein realer vorhanden ist, dann ein Fake-Variant. Dieser ist in der Konstante
DocVariantDataFake definiert. Damit lässt es sich schön spuken. Jeder Strich ist wichtig:
Delphi-Quellcode:
var
v: Variant;
begin
v := _JSON('{"First": "Tom", "Last": "Selleck", "Profession": "Actor"}');
// First: Tom, Last: Selleck, Profession: Actor
ShowMessage(Format('First: %s, Last: %s, Profession: %s', [v.First, v.Last, v.Profession]));
// What happens if you write the following?
// Example 1:
var isPeaPuree: Boolean := TDocVariantData(v).O['One'].O['Two'].O['Three'].B['IsPeaPuree'];
ShowMessage(Format('Does he like pea puree? %s', [BoolToStr(isPeaPuree, True)]));
// Result: "Does he like pea puree? False"
// Example 2:
_Safe(v).O_['One'].O_['Two'].O_['Three'].B['IsPeaPuree'] := True;
ShowMessage(v._JSON);
// Result: "{"First":"Tom","Last":"Selleck","Profession":"Actor","One":{"Two":{"Three":{"IsPeaPuree":true}}}}"
Wie schnell einzelne Bibliotheken beim Einlesen einer JSON-Datei mit 1MB Größe sind, hierzu ein paar Benchmark-Werte:
Plattform | Bibliothek | Durchsatz MB/s |
FPC/Delphi | mORMot TOrmTableJson | 507 |
FPC/Delphi | mORMot TDocVariantData | 169 |
FPC/Delphi | mORMot DynArrayLoadJson | 332 |
FPC | fpjson | 24 |
FPC | jsontools | 38 |
FPC | lgenerics | 48 |
FPC | SuperObject | 10,5 |
Delphi | Delphi's system.json | 5,8 |
Delphi | JsonDataObjects | 103 |
Delphi | SuperObject | 35 |
Delphi | X-SuperObject | 1,5 |
Delphi | Grijjy | 54 |
Delphi | dwsJSON | 97 |
Delphi | WinSoft JSON | 27 |
Die Tabelle zeigt, wie herausragend schnell die mORMot Klassen sind. Sie belegen mit deutlichem Vorsprung die ersten drei Plätze. Auch diese Zahlen geben Sicherheit.
JSON-Viewer
Eines vorweg: Das Design des Viewers ist hübsch hässlich. Da noch einige nette Funktionen auf meiner Festplatte liegen, wird das Styling auf Version 2, wahrscheinlicher in die Ewigkeit verschoben.
Mit dem erworbenen Vorwissen ausgestattet, jetzt zur konkreten Implementierung in der Anwendung. Der Viewer ist ausgelagert und von einem Panel abgeleitet. Damit ist die Anzeige von der Logik getrennt und lässt sich wie eine Komponente handhaben. Sie besteht nur aus wenigen Funktionen. Für den Durchgriff auf die Daten sind Methodenreferenzen definiert. Ihr Aufbau ist wie folgt:
Delphi-Quellcode:
TJsonTreePresenter = class(TCustomPanel)
strict private
FTree: TVirtualStringTree;
FDocData: PDocVariantData;
FDocDataName: String;
private
...
procedure DoTreeInitNode(pmSender: TBaseVirtualTree; pmParentNode, pmNode: PVirtualNode; var pmvInitialStates: TVirtualNodeInitStates);
procedure DoTreeInitChildren(pmSender: TBaseVirtualTree; pmNode: PVirtualNode; var pmvChildCount: Cardinal);
Zur Abbildung des Dokuments sind nur wenige Zeilen Quelltext notwendig:
Delphi-Quellcode:
procedure TJsonTreePresenter.DoTreeInitNode(pmSender: TBaseVirtualTree; pmParentNode, pmNode: PVirtualNode; var pmvInitialStates: TVirtualNodeInitStates);
var
title: String;
docVD: PDocVariantData;
begin
if pmParentNode = Nil then
begin
title := FDocDataName;
docVD := FDocData;
end
else
begin
docVD := PJsonDocNode(pmParentNode.GetData).docVData;
if docVD <> Nil then
begin
title := DocVariantPairCaption(docVD, pmNode.Index);
docVD := _Safe(docVD.Values[pmNode.Index]);
end;
end;
if (docVD <> Nil)
and (docVD.VarType = DocVariantVType) then
begin
var nodeData: PJsonDocNode := PJsonDocNode(pmNode.GetData);
nodeData.nodeName := title;
nodeData.docVData := docVD;
if docVD.Count > 0 then
Include(pmvInitialStates, ivsHasChildren);
end;
end;
procedure TJsonTreePresenter.DoTreeInitChildren(pmSender: TBaseVirtualTree; pmNode: PVirtualNode; var pmvChildCount: Cardinal);
var
docVD: PDocVariantData;
begin
docVD := PJsonDocNode(pmNode.GetData).docVData;
if docVD <> Nil then
pmvChildCount := docVD.Count;
end;
Für den Zugriff auf den Datenbaum sind die Funktionen
EditSelectedDocData,
DeleteSelectedDocData und
ProcessSelectedDocNode vorhanden. Ich hoffe, ich habe am Anfang nicht zu viel versprochen und das Weiterlesen hat sich gelohnt. Wer keine ausreichend große JSON-Datei zum Testen zur Verfügung hat, kann sich
Daten von GitHub laden.
Zusammenfassung
mORMot ist gut dokumentiert. Die Hilfe umfasst mehr als 2500 Seiten. Davon enthalten die ersten ca. 650 Seiten einen sehr lesenswerten allgemeinen Teil, der Rest ist
API Dokumentation. mORMot muss nicht in der
IDE installierten werden! Es reicht aus, die Bibliothek mit den Static-Dateien in ein Verzeichnis zu kopieren und die entsprechenden Bibliothekspfade anzulegen. In 3 Minuten ist es auf dem Rechner und bei Nichtgefallen in 3 Sekunden wieder spurlos entfernt. Es stehen viele Beispiele und ein freundliches
Forum zur Verfügung.
In der mORMot Reihe bisher veröffentlicht:
ORM und DocVariant kurz vorgestellt
ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
Einführung in methodenbasierte Services - Server und Client
Einführung in Interface-based Services - Server und Client
Mustache Editor mit integriertem HTTP-Server zum Anzeigen von HTML Seiten
Mediator zur Anbindung von FastReport mit Vorschau und/oder Designer
Bis bald...
Thomas