WiRL - Accesso al database
Tags: delphi, rest, wirl
Published: 2024-03-12 17:26:43

Nello [scorso articolo](https://dev.to/lminuti/wirl-risorse-e-attributi-2c30) abbiamo visto come costruire una classe con [WiRL](https://github.com/delphi-blocks/WiRL) che permetta di esporre una risorsa ReST con i metodi necessari per le classiche operazioni [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). 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](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) o manualmente. In alcuni casi però, può essere più conveniente avere un accesso più diretto al database, in particolare usando la classe [TDataSet](https://docwiki.embarcadero.com/Libraries/Athens/en/Data.DB.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}
```

## 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).

```pascal
  [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;

  ...
```

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]((https://dev.to/lminuti/wirl-risorse-e-attributi-2c30)) 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:

```pascal
  [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;

```

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.

```pascal

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;

```

## 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.

```pascal
initialization
  TWiRLResourceRegistry.Instance.RegisterResource<TUserResource>(
    function: TObject
    begin
      Result := TUserResource.Create(nil);
    end
  );
```


## 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`.