Distributed Applications
You can build distributed applications using Aurelius. When mapping classes, you can specify any class ancestor, and you can define which fields and properties will be mapped or not. This gives you flexibility to use almost any framework for building distributed applications - even if that framework requires that the classes need to have specific behavior (like inheriting from a specific base class, for example).
Still, Aurelius provides several mechanisms and classes that make building distributed applications even easier. The following topics describe features for building distributed applications using Aurelius.
JSON - JavaScript Object Notation
When building distributed applications, you need to transfer your objects between peers. Usually to transfer objects you need to convert them (marshal) to a format that you can send through your communication channel. Currently one of the most popular formats for that is the JSON format. It's simple, text representation, that can easily be parsed, lightweight, and portable. You can build your server using Aurelius, retrieve your objects from database, convert them to JSON, send the objects through any communication channel to client, and from the client, you can convert the JSON back to an Aurelius object. Since it's a portable format, your client doesn't even need to be a Delphi application using Aurelius - you can use a JavaScript client, for example, that fully supports the JSON format, or any other language.
To converting Aurelius objects to JSON you can use one of the available JSON serializers:
Serializer := TDataSnapJsonSerializer.Create;
try
JsonValue := Serializer.ToJson(Customer);
finally
Serializer.Free;
end;
To convert a JSON notation back to an Aurelius object, you can use one of the available JSON deserializers:
Deserializer := TDataSnapJsonDeserializer.Create;
try
Customer := Deserializer.FromJson<TCustomer>(JsonValue);
finally
Deserializer.Free;
end;
The following topics describes in more details how to better use the JSON with Aurelius.
Available Serializers
Aurelius uses an open architecture in JSON support that allows you to use any framework for parsing and generating the JSON representation. This makes it easy to use your preferred framework for building distributed applications and use legacy code. For example, if you are using DataSnap, Aurelius provides the DataSnap serializer that converts the object to a TJsonValue object which holds the JSON representation structure. You can use the TJsonValue directly in a DataSnap server to send JSON to the client. Other frameworks use different objects for JSON representation (or simply string format) so you can use any you want.
The following table lists the currently available JSON serializer/deserializer classes in Aurelius, what framework they use, and what is the base type that is uses for JSON representation:
Framework | Serializer class | Deserializer class | JSON Class | Declared in unit | Vendor Site |
---|---|---|---|---|---|
DataSnap | TDataSnapJsonSerializer | TDataSnapJsonDeserializer | TJsonValue | Aurelius.Json.DataSnap | Delphi Native |
SuperObject | TSuperObjectJsonSerializer | TSuperObjectJsonDeserializer | ISuperObject | Aurelius.Json.SuperObject | http://code.google.com/p/superobject/ |
All serializers have a ToJson method that receives an object and returns the type specified by the JSON Class in the table above.
All deserializers have a generic FromJson method that receives the type specified by JSON class in the table above and returns the type specified in the generic parameter.
Both serializer and deserializer need a reference to a TMappingExplorer object to work with. You can pass the object in the Create constructor when creating a serializer/deserializer, or you can use the method with no parameter to use the default mapping setup.
The following code snippets illustrate different ways of using the serializers.
Serializing/Deserializing an Aurelius object using DataSnap JSON classes and default mapping setup:
uses
{...}, Aurelius.Json.DataSnap;
var
Serializer: TDataSnapJsonSerializer;
Deserializer: TDataSnapJsonDeserializer;
Customer: TCustomer;
AnotherCustomer: TCustomer;
JsonValue: TJsonValue;
begin
{...}
Serializer := TDataSnapJsonSerializer.Create;
Deserializer := TDataSnapJsonDeserializer.Create;
try
JsonValue := Serializer.ToJson(Customer);
AnotherCustomer := Deserializer.FromJson<TCustomer>(JsonValue);
finally
Serializer.Free;
Deserializer.Free;
end;
{...}
end;
Serializing/Deserializing an Aurelius object using SuperObject and custom mapping setup:
uses
{...}, Aurelius.Json.SuperObject;
var
Serializer: TSuperObjectJsonSerializer;
Deserializer: TSuperObjectJsonDeserializer;
Customer: TCustomer;
AnotherCustomer: TCustomer;
SObj: ISuperObject;
CustomMappingExplorer: TMappingExplorer;
begin
{...}
Serializer := TSuperObjectJsonSerializer.Create(CustomMappingExplorer);
Deserializer := TSuperObjectJsonDeserializer.Create(CustomMappingExplorer);
try
SObj := Serializer.ToJson(Customer);
AnotherCustomer := Deserializer.FromJson<TCustomer>(SObj);
finally
Serializer.Free;
Deserializer.Free;
end;
{...}
end;
Serialization behavior
Aurelius maps each relevant field/attribute to the JSON representation, so that the JSON holds all (and only) relevant information to represent an object state. So for example, a class mapped like this:
[Entity]
[Table('ARTISTS')]
[Id('FId', TIdGenerator.IdentityOrSequence)]
TArtist = class
private
[Column('ID', [TColumnProp.Unique, TColumnProp.Required, TColumnProp.NoUpdate])]
FId: Integer;
FArtistName: string;
FGenre: Nullable<string>;
function GetArtistName: string;
procedure SetArtistName(const Value: string);
public
property Id: integer read FId;
[Column('ARTIST_NAME', [TColumnProp.Required], 100)]
property ArtistName: string read GetArtistName write SetArtistName;
[Column('GENRE', [], 100)]
property Genre: Nullable<string> read FGenre write FGenre;
end;
will generate the following JSON representation:
{
"$type": "Artist.TArtist",
"$id": 1,
"FId": 2,
"ArtistName": "Smashing Pumpkins",
"Genre": "Alternative"
}
Note that fields FId and properties ArtistName and Genre are mapped, and so are the ones that appear in the JSON format. Aurelius includes extra meta fields (starting with $) for its internal use that will make it easy to later deserialize the object. Nullable types and dynamic properties are automatically handled by the serializer/deserializer.
Blob fields
Content of blobs are converted into a base64 string so it can be properly deserialized back to a binary format (Data field is truncated in example below):
{
"$type": "Images.TImage",
"$id": 1,
"FId": 5,
"ImageName": "Landscape",
"Data": "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz..."
}
If blobs are set to be lazy and they are not loaded, then they will not be fully sent in JSON representation, but only a meta information that will allow you to load it later. See more at Lazy-Loading with JSON.
Associations
If the object being serialized has associations and/or many-valued associations, those objects are also serialized in the JSON. The following example shows a serialization of a class TSong which has properties Album, Artist and SongFormat that points to other objects:
{
"$type": "Song.TSong",
"$id": 1,
"FAlbum": {
"$proxy": "single",
"key": 2,
"class": "TMediaFile",
"member": "FAlbum"
},
"MediaName": "Taxman2",
"Duration": 230,
"FId": 1,
"FArtist": {
"$proxy": "single",
"key": 1,
"class": "TMediaFile",
"member": "FArtist"
},
"FileLocation": "",
"SongFormat": {
"$type": "SongFormat.TSongFormat",
"$id": 2,
"FId": 1,
"FormatName": "MP3"
}
}
If the association is marked as lazy-loading and is not load yet, then they will not be included in JSON representation, but instead a meta information will be included for later loading the value. In the example above, FAlbum and FArtist were defined as proxies and were not loaded, so the object they hold is a proxy meta information. On the other hand, SongFormat property is loaded and the whole TSongFormat object is serialized in it. For more information on lazy-loading, see Lazy-Loading with JSON.
Lazy-Loading with JSON
An object being serialized might have associations and many-valued associations defined to be lazy-loaded. When that is the case and the proxies are not loaded yet, the associated objects are not serialized, but instead, an object with metadata for that proxy is serialized instead. Take a look at the following example (irrelevant parts of the real JSON notation were removed):
{
"$type": "Song.TSong",
"$id": 1,
"FId": 1,
"FAlbum": {
"$proxy": "single",
"key": 2,
"class": "TMediaFile",
"member": "FAlbum"
},
"FileLocation": ""
}
In that example, TSong has a FAlbum field of type Proxy<TAlbum>. The song being serialized doesn't have the FAlbum field loaded, so instead of the actual TAlbum object to be serialized, a proxy object is serialized instead. The proxy object is indicated by the presence of the meta property "$proxy", which indicates if it's a proxy for a single object or a list.
How does the deserializer handle this? All JSON deserializers have a property ProxyLoader which points to an interface of type IJsonProxyLoader declared like this:
IJsonProxyLoader = interface
function LoadProxyValue(ProxyInfo: IProxyInfo): TObject;
end;
While the IProxyInfo object is declared like this (in unit
Aurelius.Types.Proxy
):
IProxyInfo = interface
function ProxyType: TProxyType;
function ClassName: string;
function MemberName: string;
function Key: Variant;
end;
When the TSong object in the previous example is deserialized, an internal proxy is set automatically in the FAlbum field. When the Album property of Song object is read, the proxy calls the method LoadProxyValue of the IJsonProxyLoader interface. So for the object to be loaded by the proxy, you must provide a valid IJsonProxyLoader interface in the deserializer so that you can load the proxy and pass it back to the engine. The easiest way to create an IJsonProxyLoader interface is using the TJsonProxyLoader interface object provided by Aurelius.
The following code illustrates how to do it:
Deserializer := TDataSnapJsonDeserializer.Create;
try
Deserializer.ProxyLoader := TJsonProxyLoader.Create(
function(ProxyInfo: IProxyInfo): TObject
var
Serializer: TDataSnapJsonSerializer;
Deserializer: TDataSnapJsonDeserializer;
JsonObject: TJsonValue;
begin
Serializer:= TDataSnapJsonSerializer.Create;
Deserializer := TDataSnapJsonDeserializer.Create;
try
JsonObject := DatasnapClient.RemoteProxyLoad(Serializer.ToJson(ProxyInfo));
Result := Deserializer.FromJson(JsonObject, TObject);
finally
Deserializer.Free;
Serializer.Free;
end;
end
);
Song := Deserializer.FromJson<TSong>(JsonValueWithSong);
finally
Deserializer.Free;
end;
// At this point, Song.Album is not loaded yet.
// When the following line of code is executed (Album property is read)
// then the method specified in the ProxyLoader will be executed and
// Album will be loaded.
Album := Song.Album;
AlbumName := Album.Name;
You can safely destroy the deserializer after the object is loaded, since the reference to the proxy loader will be in the object itself. It's up to you how to implement the ProxyLoader. In the example above, we are assuming we have a client object with a RemoteProxyLoad method that calls a server method passing the ProxyInfo data as JSON format. In the server, you can easily implement such method just by receiving the proxy info format, converting it back to IProxyInfo interface and then calling TObjectManager.ProxyLoad method:
// This method assumes that Serializer, Deserializer and ObjectManager
// objects are already created by the server
function TMyServerMethods.RemoteProxyLoad(JsonProxyInfo: TJsonValue): TJsonValue;
var
ProxyInfo: IProxyInfo;
begin
ProxyInfo := Deserializer.ProxyInfoFromJson<IProxyInfo>(JsonProxyInfo);
Result := Serializer.ToJson(ObjectManager.ProxyLoad(ProxyInfo));
end;
Lazy-Loading Blobs
In an analog way, you can lazy-load blobs with JSON. It works exactly the same as loading associations. The deserializer has a property named BlobLoader which points to an IJsonBlobLoader interface:
IJsonBlobLoader = interface
function ReadBlob(BlobInfo: IBlobInfo): TArray<byte>;
end;
And the IBlobInfo object is declared like this
(in unit Aurelius.Types.Blob
):
IBlobInfo = interface
function ClassName: string;
function MemberName: string;
function Key: Variant;
end;
And you can use TObjectManager.BlobLoad method at server side.
Memory Management with JSON
When deserializing a JSON value, objects are created by the deserializer. You must be aware that not only the main object is created, but also the associated objects, if it has associations. For example, if you deserialize an object of class TSong, which has a property TSong.Album, the object TAlbum will be also deserialized. Since you are not using an object manager that manages memory for you, in theory you would have to destroy those objects:
Song := Deserializer.FromJson<TSong>(JsonValue);
{ do something with Song, then destroy it - including associations }
Song.Album.Free;
Song.Free;
You might imagine that if your JSON has a complex object tree, you will
end up having to destroy several objects (what about
Song.Album.AlbumType.Free
, for example). To minimize this problem,
deserializers have a property OwnsEntities that when enabled, destroys
every object created by it (except lists). So your code can be built
this way:
Deserializer := TDataSnapJsonDeserializer.Create;
Deserializer.OwnsEntities := true;
Song := Deserializer.FromJson<TSong>(JsonValue);
{ do something with Song, then destroy it - including associations }
Deserializer.Free;
// After the above line, Song and any other associated object
// created by the deserializer are destroyed
Alternatively, if you still want to manage objects by yourself, but want to know which objects were created by the deserializer, you can use OnEntityCreated event:
Deserializer.OnEntityCreated := EntityCreated;
procedure TMyClass.EntityCreated(Sender: TObject; AObject: TObject);
begin
// Add created object to a list for later destruction
FMyObjects.Add(AObject);
end;
In addition to OnEntityCreated event, the deserializer also provides Entities property which contains all objects created by it:
property Entities: TEnumerable<TObject>;
Note about JSON classes created by serializer
You must also be careful when converting objects to JSON. It's up to you to destroy the class created by the serializer, if needed. For example:
var
JsonValue: TJsonValue;
begin
JsonValue := DataSnapDeserializer.ToJson(Customer);
// JsonValue must be destroyed later
In the previous example, JsonValue is a TJsonValue object and it must be destroyed. Usually you will use DataSnap deserializer in a DataSnap application and in most cases where you use TJsonValue objects in DataSnap, the framework will destroy the object automatically. Nevertheless you must pay attention to situations where you need to destroy it.