WiRL - Accesso al database

Luca Minuti - Mar 12 - - Dev Community

Nello scorso articolo abbiamo visto come costruire una classe con WiRL che permetta di esporre una risorsa ReST con i metodi necessari per le classiche operazioni CRUD. L'esempio era basato su un'ipotetica classe TUser che veniva serializzata e deserializzata in JSON.

Molto spesso i dati a cui fanno riferimento le risorse ReST sono prelevate da un database. Nell'articolo citato non si faceva riferimento alla provenienza del dato ma le istanze di TUser potevano essere, per esempio, caricate da un database tramite un ORM o manualmente. In alcuni casi però, può essere più conveniente avere un accesso più diretto al database, in particolare usando la classe TDataSet.

La definizione della risorsa

Dal punto di vista del client la risorsa e i suoi metodi di accesso saranno esattamente identici all'esempio visto in precedenza:

# Legge i dati dell'utente tramite id
GET /api/user/{id}

# Restituisce i dati di una serie di utenti filtrati con diversi parametri
GET /api/user?name={name}&email={email}

# Modifica un utente (nel corpo del messaggio saranno presenti i dati dell'utente, per esempio in JSON)
PUT /api/user/{id}

# Aggiunge un utente
POST /api/user

# Elimina un utente tramite id
DELETE /api/user/{id}
Enter fullscreen mode Exit fullscreen mode

Lettura dei dati

Cominciamo a vedere come fare a leggere i dati. Nello schema di sopra abbiamo due punti di ingresso per restituire al client gli utenti:

  • in base all'id (restituirà un solo utente)
  • o filtrati tramite diversi parametri (restituirà una lista di utenti).
  [Path('user')]
  TUserResource = class(TDataModule)
  private
    ...
  public
    [GET]
    [Path('{id}')]
    [SingleRecord]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUserById([PathParam('id')] AId: Integer): TDataSet;

    [GET]
    [Produces(TMediaType.APPLICATION_JSON)]
    function GetUser(
        [QueryParam('name')] const AName: string;
        [QueryParam('email')] const AEmail: string
    ): TDataSet;

  ...
Enter fullscreen mode Exit fullscreen mode

Com'è possibile intuire facilmente l'implementazione dei due metodi è molto semplice: basterà filtrare opportunamente un dataset e restituirlo. In questo esempio la risorsa è un DataModule. In questo modo i componenti necessari (connessione, dataset, transazione, ecc.) possono essere "appoggiati" direttamente sulla risorsa. Ovviamente è anche possibile (e sicuramente più flessibile) usare un DataModule definito esternamente.

Gli attributi usati sono gli stessi dell'articolo precedete tranne SingleRecord. Questo attributo fa in modo che il JSON ricavato dal DataSet non contenga una lista di oggetti ma un unico oggetto.

Attenzione: perché l'attributo funzioni correttamente è necessario che il progetto faccia riferimento alla unit WiRL.MessageBody.SingleRecord.

Inserimento, modifica e cancellazione

Nel caso di inserimento e modifica di un dato il client ci invierà un JSON che dovrà essere usato come modello per l'operazione richiesta. Per quanto riguarda l'eliminazione invece sarà sufficiente l'id del record da eliminare. I metodi possono quindi essere dichiarati nel seguente modo:

  [Path('user')]
  TUserResource = class(TDataModule)
  public
    ...
    [POST]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function AppendUser([BodyParam] AUser: TJSONObject): TJSONObject;

    [PUT]
    [Path('{id}')]
    [Consumes(TMediaType.APPLICATION_JSON)]
    [Produces(TMediaType.APPLICATION_JSON)]
    function UpdateUser(
        [PathParam('id')] AId: Integer; 
        [BodyParam] AUser: TJSONObject
    ): TJSONObject;

    [DELETE]
    [Path('{Id}')]
    function DeleteUser([PathParam] Id: Integer): TJSONObject;

  end;

Enter fullscreen mode Exit fullscreen mode

L'implementazione dei metodi è piuttosto semplice ma spesso abbastanza lunga, si tratta di passare tutti i parametri ad un dataset o costruirsi direttamente le stringhe SQL per INSERT e UPDATE. Se i nodi del JSON e i campi del database hanno lo stesso nome è possibile usare la classe TWiRLResolver che rende un po' più rapida la scrittura del codice.


function TUserResource.AppendUser(AUser: TJSONObject): TJSONObject;
begin
  // Aggiorna dei campi lato server 
  TJSONUtils.SetJSONValue('CREATED_BY', 'LMINUTI', AUser);
  // è anche possibile eseguire dei controlli di integrità e sollevare
  // un'eccezione che verrà trasformata in un errore HTTP

  // Esegue l'aggiornamento usando TWiRLResolver 
  TWiRLResolver.InsertDataSet(qryUser, AUser);

  Result := TJSONObject.Create(TJSONPair.Create('success', TJSONTrue.Create));
end;

function TUserResource.UpdateUser(AId: Integer; AUser: TJSONObject): TJSONObject;
begin
  TWiRLResolver.UpdateDataSet(qryUser, AUser);
  Result := TJSONObject.Create(TJSONPair.Create('success', TJSONTrue.Create));
end;

function TUserResource.DeleteUser(Id: Integer): TJSONObject;
begin
  TWiRLResolver.DeleteDataSet(qryUser, Id);
  Result := TJSONObject.Create(TJSONPair.Create('success', TJSONTrue.Create));
end;

Enter fullscreen mode Exit fullscreen mode

Registrazione

Per quanto riguarda la registrazione della risorsa normalmente WiRL permette di registrare solo classi con un costruttore senza parametri. In questo caso, visto che abbiamo usato un datamodule come classe base, abbiamo un costruttore con un parametro. Quindi dobbiamo passare a WiRL un factory method che sia in grado di creare la risorsa.

initialization
  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>(
    function: TObject
    begin
      Result := TUserResource.Create(nil);
    end
  );
Enter fullscreen mode Exit fullscreen mode

Conclusioni

In questo articolo abbiamo visto come creare una classe che implementi le principali operazioni di lettura e scrittura su una risorsa ReST. Non abbiamo usato nessuno strato intermedio (con i pregi e difetti che comporta questa scelta) ma siano andati ad usare direttamente la classe TDataSet.

. . . . . . . . . . . .