Sérialisation des objets utilisateur

De RAD Studio
Aller à : navigation, rechercher

Remonter à Développement d'applications DataSnap


Il est maintenant possible de passer des objets définis par l'utilisateur entre les méthodes client et serveur en utilisant des objets JSON.

Le paradigme est basé sur le marshaling des objets utilisateur en objets JSON, puis de nouveau en objets utilisateur sur le côté opposé. DataSnap fournit une suite de sérialisations génériques dans l'unité DBXJSONReflect par le biais de la classe TTypeMarshaller. Les objets utilisateur peuvent être transformés en une représentation équivalente, puis reconvertis en instances utilisateur selon une suite de classes converter et reverter.

TJSONMarshal et TJSONUnMarshal sont deux implémentations de la sérialisation basée sur des objets JSON : les objets utilisateur sont transformés en objets JSON équivalents, qui sont reconvertis en objets utilisateur.

Les objets définis par l'utilisateur ne peuvent pas tous être sérialisés uniquement selon les classes de marshaling. Ils peuvent être étendus avec des convertisseurs personnalisés pour les champs utilisateur qui ne peuvent pas être correctement sérialisés par le biais de RTTI. Une conversion peut être associée à un type ou un champ de la classe utilisateur. Les quatre types de convertisseurs qui existent pour chaque catégorie sont listés dans le tableau suivant :

Description Champ Type
Conversion en une chaîne TStringConverter TTypeStringConverter
Conversion en un objet TObjectConverter TTypeObjectConverter
Conversion en un tableau de chaînes TStringsConverter TTypeStringsConverter
Conversion en un tableau d'objets TObjectsConverter TTypeObjectsConverter

Le principe convertisseur / restaurateur est basé sur la transformation des données en une représentation à partir de laquelle l'instance peut être restaurée. Il est possible de choisir la conversion d'une structure de données complexe en une chaîne simple qui peut être analysée pour être restaurée, ou une méthode plus sophistiquée mais plus efficace peut être choisie selon le cas. Supposons qu'une collection d'objets doit être sérialisée. Une approche peut consister à transformer chaque élément en une chaîne et à concaténer toutes les chaînes avec un séparateur unique. Une méthode plus efficace consiste à convertir la collection en un tableau d'objets. Le restaurateur reçoit ce tableau en entrée et peut reconstituer la collection complexe.

L'exemple de code listé dans cette page est fractionné en blocs pour une meilleure compréhension. L'exemple fait partie d'un projet de bibliothèques RAD Studio que vous devez créer afin de l'exécuter et de voir les résultats. Le code de cette page doit être contenu dans le même code source (fichier .pas) que la fiche principale du projet.

 {$R *.res}
 
 uses
   SysUtils, Classes, JSON, DBXJSON, DBXJSONReflect;

Nous allons commenter un exemple de code qui sérialise les types définis par l'utilisateur ci-dessous.

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure MainProc;
	
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TAddress = record
    FStreet: String;
    FCity: String;
    FCode: String;
    FCountry: String;
    FDescription: TStringList;
  end;

  TPerson = class
  private
    FName: string;
    FHeight: integer;
    FAddress: TAddress;
    FSex: char;
    FRetired: boolean;
    FChildren: array of TPerson;
    FNumbers: set of 1..10;
  public
    constructor Create;
    destructor Destroy; override;

    procedure AddChild(kid: TPerson);
  end;

L’exemple utilise les variables suivantes :

 var
   m: TJSONMarshal;
   unm: TJSONUnMarshal;

Les déclarations des membres de la classe TPerson :

constructor TPerson.Create;
begin
  FAddress.FDescription := TStringList.Create;
end;

destructor TPerson.Destroy;
begin
  FAddress.FDescription.Free;
  inherited;
end;

procedure TPerson.AddChild(kid: TPerson);
begin
  SetLength(FChildren, Length(FChildren) + 1);
  FChildren[Length(FChildren) - 1] := kid;
end;

L'exemple inclut des types de collection complexes (TStringList), des ensembles, des tableaux et des enregistrements. Nous avons choisi de considérer FNumbers comme un champ transitoire (par défaut pour un ensemble).

La procédure principale du programme :


procedure TForm1.MainProc;
var
  person, newperson: TPerson;
  kid: TPerson;
  JSONString: String;

begin
  m := TJSONMarshal.Create(TJSONConverter.Create);
  unm := TJSONUnMarshal.Create;

  { For each complex field type, we will define a converter/reverter pair. We will individually deal
    with the "FChildren" array, the "TStringList" type, and the "FAddress" record. We will transform
    the array type into an actual array of "TPerson", as illustrated below. }

  m.RegisterConverter(TPerson, 'FChildren', function(Data: TObject; Field: String): TListOfObjects
  var
    obj: TPerson;
    I: Integer;

  begin
    SetLength(Result, Length(TPerson(Data).FChildren));
    I := Low(Result);
    for obj in TPerson(Data).FChildren do
    begin
      Result[I] := obj;
      Inc(I);
    end;
  end);

  { The implementation is quite straightforward: each child "TPerson" is appended to a predefined
    type instance "TListOfObjects". Later on, each of these objects will be serialized by the same
    marshaller and added to a "TJSONArray" instance. The reverter will receive as argument a
    "TListOfObjects" being oblivious of the "TJSONArray" used for that. }

  { For "TStringList", we will have a generic converter that can be reused for other marshal instances.
    The converter simply returns the array of strings of the list. }

  { Note that this converter is not really needed here because the TStringList converter is already
    implemented in the Marshaller. }

  m.RegisterConverter(TStringList, function(Data: TObject): TListOfStrings
  var
    i, count: integer;

  begin
    count := TStringList(Data).Count;
    SetLength(Result, count);
    for I := 0 to count - 1 do
      Result[i] := TStringList(Data)[i];
  end);

  { Finally, the address record will be transformed into an array of strings, one for each record
    field with the description content at the end of it. }

  m.RegisterConverter(TPerson, 'FAddress', function(Data: TObject; Field: String): TListOfStrings
  var
    Person: TPerson;
    I: Integer;
    Count: Integer;

  begin
    Person := TPerson(Data);
    if Person.FAddress.FDescription <> nil then
      Count := Person.FAddress.FDescription.Count
    else
      Count := 0;
    SetLength(Result, Count + 4);
    Result[0] := Person.FAddress.FStreet;
    Result[1] := Person.FAddress.FCity;
    Result[2] := Person.FAddress.FCode;
    Result[3] := Person.FAddress.FCountry;
    for I := 0 to Count - 1 do
      Result[4+I] := Person.FAddress.FDescription[I];
  end);

  { It is easy to imagine the reverter's implementation, present below in bulk. }

  unm.RegisterReverter(TPerson, 'FChildren', procedure(Data: TObject; Field: String; Args: TListOfObjects)
  var
    obj: TObject;
    I: Integer;

  begin
    SetLength(TPerson(Data).FChildren, Length(Args));
    I := Low(TPerson(Data).FChildren);
    for obj in Args do
    begin
      TPerson(Data).FChildren[I] := TPerson(obj);
      Inc(I);
    end
  end);

  { Note that this reverter is not really needed here because the TStringList reverter is already
    implemented in the Unmarshaller. }

  unm.RegisterReverter(TStringList, function(Data: TListOfStrings): TObject
  var
    StrList: TStringList;
    Str: string;

  begin
    StrList := TStringList.Create;
    for Str in Data do
      StrList.Add(Str);
    Result := StrList;
  end);

  unm.RegisterReverter(TPerson, 'FAddress', procedure(Data: TObject; Field: String; Args: TListOfStrings)
  var
    Person: TPerson;
    I: Integer;

  begin
    Person := TPerson(Data);
    if Person.FAddress.FDescription <> nil then
      Person.FAddress.FDescription.Clear
    else if Length(Args) > 4 then
      Person.FAddress.FDescription := TStringList.Create;

    Person.FAddress.FStreet := Args[0];
    Person.FAddress.FCity := Args[1];
    Person.FAddress.FCode := Args[2];
    Person.FAddress.FCountry := args[3];
    for I := 4 to Length(Args) - 1 do
      Person.FAddress.FDescription.Add(Args[I]);
  end);

  { The test code is as follows. }

  person := TPerson.Create;
  person.FName := 'John Doe';
  person.FHeight := 167;
  person.FSex := 'M';
  person.FRetired := false;
  person.FAddress.FStreet := '62 Peter St';
  person.FAddress.FCity := 'TO';
  person.FAddress.FCode := '1334566';
  person.FAddress.FDescription.Add('Driving directions: exit 84 on highway 66');
  person.FAddress.FDescription.Add('Entry code: 31415');

  kid := TPerson.Create;
  kid.FName := 'Jane Doe';
  person.AddChild(kid);

  { Marshal the "person" as a JSONValue and display its contents. }

  JSONString := m.Marshal(person).ToString;
  Memo1.Lines.Clear;
  Memo1.Lines.Add(JSONString);
  Memo1.Lines.Add('-----------------------');

  { Unmarshal the JSONString to a TPerson class }

  newperson := unm.Unmarshal(TJSONObject.ParseJSONValue(JSONString)) as TPerson;
  Memo1.Lines.Add(newperson.FName);
  Memo1.Lines.Add(IntToStr(newperson.FHeight));

  { and so on for the other fields }
end;

Et le bouton qui appelle MainProc :

procedure TForm1.Button1Click(Sender: TObject);
begin
  MainProc;
end;

La représentation JSON intermédiaire pour le code de test de l'exemple de code ci-dessus est la suivante :

 {"type":"Converter.TPerson",
  "id":1,
  "fields":{"FName":"John Doe",
            "FHeight":167,
            "FAddress":["62 Peter St","TO","1334566","","Driving directions: exit 84 on highway 66","Entry code: 31415"],
            "FSex":"M",
            "FRetired":false,
            "FChildren":[{"type":"Converter.TPerson",
                          "id":2,
                          "fields":{"FName":"Jane Doe",
                                    "FHeight":0,
                                    "FAddress":["","","",""],
                                    "FSex":"",
                                    "FRetired":false,
                                    "FChildren":[]
                                   }
                         }
                        ]
           }
 }

La réussite de la sérialisation peut être vérifiée en s'assurant que tous les champs sont sérialisés, avec la méthode HasWarnings du marshal.

Remarque : Le processus de sérialisation résout les références circulaires.

Voir aussi