Variant object documents
With _Obj()
, an object variant
instance
will be initialized with data supplied two by two, as Name,Value
pairs, e.g.
var V1,V2: variant; // stored as any variant ... V1 := _Obj(['name','John','year',1972]); V2 := _Obj(['name','John','doc',_Obj(['one',1,'two',2.5])]); // with nested objects
Then you can convert those objects into JSON, by two means:
- Using the
VariantSaveJson()
function, which return directly one UTF-8 content; - Or by trans-typing the
variant
instance into a string (this will be slower, but is possible).
writeln(VariantSaveJson(V1)); // explicit conversion into RawUTF8 writeln(V1); // implicit conversion from variant into string // both commands will write '{"name":"john","year":1982}' writeln(VariantSaveJson(V2)); // explicit conversion into RawUTF8 writeln(V2); // implicit conversion from variant into string // both commands will write '{"name":"john","doc":{"one":1,"two":2.5}}'
As a consequence, the Delphi IDE debugger is able to display such variant
values as their JSON representation.
That is, V1
will be displayed as
'"name":"john","year":1982'
in the IDE debugger Watch
List window, or in the Evaluate/Modify (F7) expression
tool.
This is pretty convenient, and much more user friendly than any class-based
solution (which requires the installation of a specific design-time package in
the IDE).
You can access to the object properties via late-binding, with any depth of nesting objects, in your code:
writeln('name=',V1.name,' year=',V1.year); // will write 'name=John year=1972' writeln('name=',V2.name,' doc.one=',V2.doc.one,' doc.two=',doc.two); // will write 'name=John doc.one=1 doc.two=2.5 V1.name := 'Mark'; // overwrite a property value writeln(V1.name); // will write 'Mark' V1.age := 12; // add a property to the object writeln(V1.age); // will write '12'
Note that the property names will be evaluated at runtime only, not at
compile time.
For instance, if you write V1.nome
instead of
V1.name
, there will be no error at compilation, but an
EDocVariant
exception will be raised at execution (unless you set
the dvoReturnNullForUnknownProperty
option to
_Obj/_Arr/_Json/_JsonFmt
which will return a null
variant for such undefined properties).
In addition to the property names, some pseudo-methods are available for
such object variant
instances:
writeln(V1._Count); // will write 3 i.e. the number of name/value pairs in the object document writeln(V1._Kind); // will write 1 i.e. ord(sdkObject) for i := 0 to V2._Count-1 do writeln(V2.Name(i),'=',V2.Value(i)); // will write in the console: // name=John // doc={"one":1,"two":2.5} // age=12 if V1.Exists('year') then writeln(V1.year);
You may also trans-type your variant
instance into a
TDocVariantData record
, and access directly to its
internals.
For instance:
TDocVariantData(V1).AddValue('comment','Nice guy'); with TDocVariantData(V1) do // direct transtyping if Kind=sdkObject then // direct access to the TDocVariantDataKind field for i := 0 to Count-1 do // direct access to the Count: integer field writeln(Names[i],'=',Values[i]); // direct access to the internal storage arrays
By definition, trans-typing via a TDocVariantData record
is
slightly faster than using late-binding.
But you must ensure that the variant
instance is really a
TDocVariant
kind of data before transtyping e.g. by calling
DocVariantType.IsOfType(aVariant)
.
Variant array documents
With _Arr()
, an array variant
instance
will be initialized with data supplied as a list of Value1,Value2,...,
e.g.
var V1,V2: variant; // stored as any variant ... V1 := _Arr(['John','Mark','Luke']); V2 := _Obj(['name','John','array',_Arr(['one','two',2.5])]); // as nested array
Then you can convert those objects into JSON, by two means:
- Using the
VariantSaveJson()
function, which return directly one UTF-8 content; - Or by trans-typing the
variant
instance into a string (this will be slower, but is possible).
writeln(VariantSaveJson(V1)); writeln(V1); // implicit conversion from variant into string // both commands will write '["John","Mark","Luke"]' writeln(VariantSaveJson(V2)); writeln(V2); // implicit conversion from variant into string // both commands will write '{"name":"john","array":["one","two",2.5]}'
As a with any object document, the Delphi IDE debugger is able to
display such array variant
values as their JSON
representation.
Late-binding is also available, with a special set of pseudo-methods:
writeln(V1._Count); // will write 3 i.e. the number of items in the array document writeln(V1._Kind); // will write 2 i.e. ord(sdkArray) for i := 0 to V1._Count-1 do writeln(V1.Value(i),':',V2._(i)); // will write in the console: // John John // Mark Mark // Luke Luke if V1.Exists('John') then writeln('John found in array');
Of course, trans-typing into a TDocVariantData record
is
possible, and will be slightly faster than using late-binding.
Create variant object or array documents from JSON
With _Json()
or _JsonFmt()
, either a
document or array variant
instance will be
initialized with data supplied as JSON, e.g.
var V1,V2,V3,V4: variant; // stored as any variant ... V1 := _Json('{"name":"john","year":1982}'); // strict JSON syntax V2 := _Json('{name:"john",year:1982}'); // with MongoDB extended syntax for names V3 := _Json('{"name":?,"year":?}',[],['john',1982]); V4 := _JsonFmt('{%:?,%:?}',['name','year'],['john',1982]); writeln(VariantSaveJSON(V1)); writeln(VariantSaveJSON(V2)); writeln(VariantSaveJSON(V3)); // all commands will write '{"name":"john","year":1982}'
Of course, you can nest objects or arrays as parameters to the
_JsonFmt()
function.
The supplied JSON can be either in strict JSON syntax, or with the
MongoDB extended syntax, i.e. with unquoted property names.
It could be pretty convenient and also less error-prone when typing in the
Delphi code to forget about quotes around the property names of your JSON.
Note that TDocVariant implements an open interface for adding any
custom extensions to JSON: for instance, if the SynMongoDB.pas
unit is defined in your application, you will be able to create any MongoDB
specific types in your JSON, like ObjectID()
, new
Date()
or even /regex/option
.
As a with any object or array document, the Delphi IDE
debugger is able to display such variant
values as their JSON
representation.
Per-value or per-reference
By default, the variant
instance created by _Obj() _Arr()
_Json() _JsonFmt()
will use a copy-by-value pattern.
It means that when an instance is affected to another variable, a new
variant
document will be created, and all internal values will be
copied. Just like a record
type.
This will imply that if you modify any item of the copied variable, it won't change the original variable:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972]); V2 := V1; // create a new variant, and copy all values V2.name := 'James'; // modifies V2.name, but not V1.name writeln(V1.name,' and ',V2.name); // will write 'John and James'
As a result, your code will be perfectly safe to work with, since
V1
and V2
will be uncoupled.
But one drawback is that passing such a value may be pretty slow, for instance, when you nest objects:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972]); V2 := _Arr(['John','Mark','Luke']); V1.names := V2; // here the whole V2 array will be re-allocated into V1.names
Such a behavior could be pretty time and resource consuming, in case of a huge document.
All _Obj() _Arr() _Json() _JsonFmt()
functions have an optional
TDocVariantOptions
parameter, which allows to change the behavior
of the created TDocVariant
instance, especially setting
dvoValueCopiedByReference
.
This particular option will set the copy-by-reference pattern:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972],[dvoValueCopiedByReference]); V2 := V1; // creates a reference to the V1 instance V2.name := 'James'; // modifies V2.name, but also V1.name writeln(V1.name,' and ',V2.name); // will write 'James and James'
You may think this behavior is somewhat weird for a variant
type. But if you forget about per-value objects and consider those
TDocVariant
types as a Delphi class
instance (which
is a per-reference type), without the need of having a fixed schema
nor handling manually the memory, it will probably start to make sense.
Note that a set of global functions have been defined, which allows direct
creation of documents with per-reference instance lifetime, named
_ObjFast() _ArrFast() _JsonFast() _JsonFmtFast()
.
Those are just wrappers around the corresponding _Obj() _Arr() _Json()
_JsonFmt()
functions, with the following JSON_OPTIONS[true]
constant passed as options parameter:
const
/// some convenient TDocVariant options
// - JSON_OPTIONS[false] is _Json() and _JsonFmt() functions default
// - JSON_OPTIONS[true] are used by _JsonFast() and _JsonFastFmt() functions
JSON_OPTIONS: array[Boolean] of TDocVariantOptions = (
[dvoReturnNullForUnknownProperty],
[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]);
When working with complex documents, e.g. with BSON / MongoDB documents, almost all content will be created in "fast" per-reference mode.
Advanced TDocVariant process
Object or array document creation options
As stated above, a TDocVariantOptions
parameter enables to
define the behavior of a TDocVariant
custom type for a given
instance.
Please refer to the documentation of this set of options to find out the
available settings. Some are related to the memory model, other to
case-sensitivity of the property names, other to the behavior expected in case
of non-existing property, and so on...
Note that this setting is local to the given variant
instance.
In fact, TDocVariant
does not force you to stick to one memory
model nor a set of global options, but you can use the best pattern depending
on your exact process.
You can even mix the options - i.e. including some objects as
properties in an object created with other options - but in this case, the
initial options of the nested object will remain. So you should better use this
feature with caution.
You can use the _Unique()
global function to force a variant
instance to have an unique set of options, and all nested documents to become
by-value, or _UniqueFast()
for all nested documents to
become by-reference.
// assuming V1='{"name":"James","year":1972}' created by-reference _Unique(V1); // change options of V1 to be by-value V2 := V1; // creates a full copy of the V1 instance V2.name := 'John'; // modifies V2.name, but not V1.name writeln(V1.name); // write 'James' writeln(V2.name); // write 'John' V1 := _Arr(['root',V2]); // created as by-value by default, as V2 was writeln(V1._Count); // write 2 _UniqueFast(V1); // change options of V1 to be by-reference V2 := V1; V1._(1).name := 'Jim'; writeln(V1); writeln(V2); // both commands will write '["root",{"name":"Jim","year":1972}]'
The easiest is to stick to one set of options in your code, i.e.:
- Either using the
_*()
global functions if your business code does send someTDocVariant
instances to any other part of your logic, for further storage: in this case, the by-value pattern does make sense; - Or using the
_*Fast()
global functions if theTDocVariant
instances are local to a small part of your code, e.g. used as schema-less Data Transfer Objects (DTO).
In all cases, be aware that, like any class
type, the
const
, var
and out
specifiers of method
parameters does not behave to the TDocVariant
value, but to its
reference.
Integration with other mORMot units
In fact, whenever a schema-less storage structure is needed, you
may use a TDocVariant
instance instead of class
or
record
strong-typed types:
- Client-Server ORM will support
TDocVariant
in any of theTSQLRecord variant
published properties; - Interface-based services will support
TDocVariant
asvariant
parameters of any method, which make them as perfect DTO; - Since JSON support is implemented with any
TDocVariant
value from the ground up, it makes a perfect fit for working with AJAX clients, in a script-like approach; - If you use our
SynMongoDB.pas
unit to access a MongoDB server,TDocVariant
will be the native storage to create or access BSON arrays or objects documents; - Cross-cutting features (like logging or
record
/ dynamic array enhancements) will also benefit from thisTDocVariant
custom type.
We are pretty convinced that when you will start playing with
TDocVariant
, you won't be able to live without it any more.
It introduces the full power of late-binding and schema-less patterns to your
application, which can be pretty useful for prototyping or in Agile
development.
You do not need to use scripting engines like Python or JavaScript to have this
feature, if you need it.
Feedback and comments are welcome in our forum, as usual!