Luca Minuti
Posted on March 12, 2024
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}
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;
...
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;
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;
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
);
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
.
Posted on March 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.