Come creare una Web API con Delphi, DMVCFramework e PostgreSQL - Parte 1 [ITALIANO]

👉 This article is available in english too.

👉 Questo articolo è disponibile anche in inglese.

Introduzione

DMVCFramework è il framework REST Delphi più popolare su Github. Viene utilizzato per creare soluzioni Web basate sullo stile RESTful o sul protocollo JSON-RPC (o qualsiasi tipo di “stile” che preferisci). A differenza di molti altri compatitor (sia open source che prodotti commerciali) DMVCFramework raggiunge il Richardson Maturity Model di livello 3 - a.k.a. “Glory of REST” - come lo chiama M. Fowler nel suo famoso articolo sugli approcci REST. Uno dei principali punti di forza di DMVCFramework è la capacità di creare applicazioni autonome senza dipendere da altro. Puoi semplicemente creare la tua API e distribuirla senza alcun framework, runtime, ecc.. Un’altra strategia di distribuzione consiste nel distribuire le API DMVCFramework come servizio Windows, come modulo Apache (per Windows e Linux), come demone Linux o come ISAPI IIS ( per Windows). Tutti questi tipi di deploy permettono di rilasciare una soluzione pulita, veloce e performante con una configurazione minima.

Chi frequenta i miei corsi lo sa, credo fortemente che imparare da semplici esempi pratici sia la cosa migliore per apprendere e costruire cose più complesse; pertanto, questo articolo ti guiderà attraverso la creazione di un’API RESTful CRUD (crea-leggi-aggiorna-elimina) semplice ma completa utilizzando DMVCFramework. In questo esempio utilizzerò PostgreSQL come RDBMS. Sì, mi piace davvero tanto PostgreSQL, ma DMVCFramework può essere utilizzato ovviamente con qualsiasi tipo di motore di database o anche con nessuno. Sebbene DMVCFramework possa fornire API RESTful e API JSON-RPC, in questo articolo l’enfasi maggiore sarà posta sull’API RESTful e sulla connettività del database.

🔔 Tieni presente che il codice in questo articolo funziona con dmvcframework-3.2.3-radium e versioni più recenti. Se per qualche motivo sei costretto ad utilizzare una versione precedente potrebbero essere necessarie alcune piccole modifiche. Nel caso fosse così, scrivimelo nei commenti e provvederò a inserire le note di compatibilità.

dmvcframework-3.4.0-neon introduce molte nuove funzionalità incluse le tanto attese azioni funzionali (functional actions). In una versione futura di questo articolo mostrerò come implementare la stessa API con azioni funzionali. Se sei interessato a dmvcframework-3.4.0-neon, puoi consultare questo articolo.

Progettazione delle API Web

Come è prassi consolidata, iniziaremo con la progettazione delle API; non pensare al database, al tipo di distribuzione o ad altri dettagli - inizia semplicemente con la parte del sistema fornita ai tuoi clienti: le API.

In questo caso l’API è abbastanza semplice. Abbiamo bisogno di un semplice CRUD su una singola entità. Alla fine l’API subirà alcune modifiche e non vogliamo far smettere di funzionare gli eventuali client, quindi la nostra API sarà versionata. Ecco l’API che implementeremo.

GET /api/v1/customers (recupera una lista di clienti)
GET /api/v1/customers/$id (recupera il cliente con id = $id)
POST /api/v1/customers (crea un nuovo cliente)
PUT /api/v1/customers/$id (aggiorna il cliente con id = $id)
DELETE /api/v1/customers/$id (elimina il cliente con id = $id)

Come puoi vedere, questa API è davvero semplice ma dobbiamo pur iniziare da qualche parte! La miglioreremo nelle parti successive dell’articolo. Come abbiamo detto DMVCFramework è conforme a RMM3 (noto anche come supporto HATEOAS), quindi l’endpoint “elenco clienti” fornirà il collegamento anche alle altre entità.

Creazione della struttura del database

In questo esempio il nostro database è molto semplice: solo una tabella customers. Puoi utilizzare qualsiasi database tu voglia, ma in questo caso utilizzeremo PostgreSQL. Lo script per creare la tabella customers è il seguente.

DDL per la creazione della tabella Customers

CREATE TABLE customers (
 id int8 generated always as identity NOT NULL,
 code varchar(20) NULL,
 description varchar(200) NULL,
 city varchar(200) NULL,
 note text NULL,
 rating int2 NOT NULL DEFAULT 0,
 CONSTRAINT customers_pk PRIMARY KEY (id)
);

Abbastanza semplice, ma efficace per i nostri scopi. Per fare qualche test, ho caricato nella mia tabella un po’ di record.

Dati caricati manualmente nella tabella Customers

Creare l’application server

Installare e utilizzare DMVCFramework è abbastanza semplice, ma se hai bisogno di un tutorial dettagliato leggi il primo capitolo gratuito del libro DMVCFramework - the official guide (disponibile in inglese, spagnolo e portoghese). Il libro completo è disponibile sul sito web Leanpub.

Creiamo un nuovo progetto DMVCFramework utilizzando la procedura guidata fornita utilizzando le impostazioni mostrate di seguito.

Procedura guidata DMVCFramework

La procedura guidata genererà tutti i file necessari (dproj, dpr, pas e dfm) necessari al progetto. Salvare tutti i file utilizzando i seguenti nomi (questi nomi non sono obbligatori ma saranno utilizzati per farvi facilmente riferimento):

Nomi dei file da usare quando si salva il progetto

Parte del progetto Nome suggerito
Project file CRUDAPIVersion1.dproj
WebModule WebModuleU.pas
Controller Controllers.Customers.pas

A questo punto dovresti essere in grado di eseguire il progetto utilizzando Project->Run o semplicemente premendo F9. Dopo il salvataggio del file, in alcune parti del codice alcune unit vengono ancora referenziate utilizzando i vecchi nomi, correggile utilizzando i nomi che hai scelto e ricompila il progetto.

Ora, avviando il progetto dovresti ottenere qualcosa di simile al seguente.

DMVCFramework CRUDAPIVersion1.exe is running

Bene, il servizio funziona; ora bisogna fargli fare qualcosa di interessante e magari pure utile 😏.

Dichiarazione dell’entità Customer

Sebbene utilizzare DataSet e SQL possa essere utile per piccoli progetti, in questo esempio useremo il microframework MVCActiveRecord incluso in DMVCFramework: è veloce, facile da usare e ricco di funzionalità utili. Aggiungiamo una nuova unit al progetto, salviamola come Entities.Customer.pas e scriviamoci dentro il seguente codice.

Entità TCustomer mappata sulla tabella customers

unit Entities.Customer;

interface

uses
  MVCFramework.Serializer.Commons,
  MVCFramework.ActiveRecord,
  MVCFramework.Nullables;

type
  [MVCNameCase(ncLowerCase)]
  [MVCTable('customers')]
  TCustomer = class(TMVCActiveRecord)
  private
    [MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
    fID: NullableInt64;
    [MVCTableField('code')]
    fCode: NullableString;
    [MVCTableField('description')]
    fCompanyName: NullableString;
    [MVCTableField('city')]
    fCity: string;
    [MVCTableField('rating')]
    fRating: NullableInt32;
    [MVCTableField('note')]
    fNote: string;
  public
    property ID: NullableInt64 read fID write fID;
    property Code: NullableString read fCode write fCode;
    property CompanyName: NullableString read fCompanyName write fCompanyName;
    property City: string read fCity write fCity;
    property Rating: NullableInt32 read fRating write fRating;
    property Note: string read fNote write fNote;
  end;

implementation

end.

🔔 In questa versione delle API non è presente alcuna logica di business, ma la aggiungeremo nelle prossime versioni.

TMVCActiveRecord è molto potente ed è in grado di gestire strutture di dati complesse (ereditarietà, relazioni, aggregati e altro). Per avere un’idea di cosa può fare TMVCActiveRecord puoi leggere questo articolo.

Questa classe TCustomer mappa i campi interessanti della tabella CUSTOMERS nel nostro database. Gli attributi utilizzati nella dichiarazione consentono di descrivere completamente l’entità in termini di funzionalità, tipi di dati e nullabilità mentre non c’è alcuna informazione sulla lunghezza della stringa. L’unità MVCFramework.Nullables.pas utilizzata nella sezione interfaccia consente di utilizzare i tipi nullable introdotti in DMVCFramework per rappresentare correttamente i possibili valori, o l’assenza di essi, nei campi della tabella sottostante.

Dichiarare il Controller e le sue Action

I controller sono la parte più visibile di un’app DMVCFramework. Qualsiasi app ha almeno un controller. I metodi pubblici del controller sono chiamati Action e rappresentano i metodi che vengono effettivamente eseguiti dopo che il router ha analizzato l’url e tutti gli header.

Apri il file Controllers.Customers.pas e scrivi il seguente codice.

Il controller Customers - qui è dove implementiamo l’endpoint customers

unit Controllers.Customers;

interface

uses
  MVCFramework,
  MVCFramework.ActiveRecord,
  MVCFramework.Commons,
  Entities.Customer;

const
  BASE_API_V1 = '/api/v1';

type

  [MVCPath(BASE_API_V1 + '/customers')]
  TCustomersController = class(TMVCController)
  public
    [MVCPath]
    [MVCHTTPMethods([httpGET])]
    procedure GetCustomers(const [MVCFromQueryString('rql','')] RQL: String);

    [MVCPath('/($ID)')]
    [MVCHTTPMethods([httpGET])]
    procedure GetCustomerByID(const ID: Integer);

    [MVCPath('/($ID)')]
    [MVCHTTPMethods([httpPUT])]
    procedure UpdateCustomerByID(const ID: Integer);

    [MVCPath('/($ID)')]
    [MVCHTTPMethods([httpDELETE])]
    procedure DeleteCustomer(const ID: Integer);

    [MVCPath]
    [MVCHTTPMethods([httpPOST])]
    procedure CreateCustomers(const [MVCFromBody] Customer: TCustomer);
  end;

implementation

uses
  System.SysUtils,
  FireDAC.Comp.Client,
  FireDAC.Stan.Param,
  MVCFramework.Logger,
  MVCFramework.Serializer.Commons,
  JsonDataObjects;

{ TCustomersController }

procedure TCustomersController.CreateCustomers(const Customer: TCustomer);
begin
  Customer.Insert;
  Render201Created(BASE_API_V1 + '/customers/' + Customer.ID.Value.ToString);
end;

procedure TCustomersController.DeleteCustomer(const ID: Integer);
begin
  var lCustomer := TMVCActiveRecord.GetByPK<TCustomer>(ID);
  try
    lCustomer.Delete;
    Render204NoContent();
  finally
    lCustomer.Free;
  end;
end;

procedure TCustomersController.GetCustomerByID(const ID: Integer);
begin
  Render(ObjectDict().Add('data', TMVCActiveRecord.GetByPK<TCustomer>(ID)));
end;

procedure TCustomersController.GetCustomers(const RQL: String);
begin
  Render(ObjectDict().Add('data', TMVCActiveRecord.SelectRQL<TCustomer>(RQL, 100),
    procedure (const Customer: TObject; const Links: IMVCLinks)
    begin
      Links
        .AddRefLink
        .Add(HATEOAS.HREF, BASE_API_V1 + '/customers/' + TCustomer(Customer).ID.Value.ToString)
        .Add(HATEOAS.REL, 'self')
        .Add(HATEOAS._TYPE, 'application/json');
    end));
end;

procedure TCustomersController.UpdateCustomerByID(const ID: Integer);
begin
  var lCustomer := TMVCActiveRecord.GetByPK<TCustomer>(ID);
  try
    Context.Request.BodyFor<TCustomer>(lCustomer);
    lCustomer.Update;
    Render204NoContent(BASE_API_V1 + '/customers/' + lCustomer.ID.Value.ToString);
  finally
    lCustomer.Free;
  end;
end;

end.

Ora dobbiamo connetterci al database. Anche se questo può essere fatto in vari modi, qui ti suggerisco di seguire questo approccio, se fattibile per te.

  • Eseguila build del progetto e identifica dove si trova il file CRUDAPIVersion1.exe (probabilmente in Win32/Debug o Win64/Debug se non hai modificato il percorso di output predefinito e la configurazione di compilazione)

  • Allo stesso livello dell’eseguibile, crea un nuovo file denominato FDConnectionDefs.ini e scrivi il seguente testo riempiendo i dati tra < e > con i valori corretti.

[FDConnectionDefs.ini]
Encoding=UTF8

[crudapiversion1]
Database=<dbname>
User_Name=<dbusername>
Password=<dbpassword>
Server=<serverip>
Port=5432
DriverID=PG

Questo file è un file di configurazione standard di FireDAC ed è un modo pratico per creare un file di configurazione di database esterno perché viene caricato automaticamente da FireDAC senza alcuna codifica manuale. In alcuni ambienti non è possibile utilizzare questo file di testo non crittografato per archiviare password e altre informazioni relative al database (per motivi di sicurezza), ma può essere utile in molti scenari lato server.

Ora abbiamo creato tutti i pezzi necessari, ma ancora non “parlano” tra loro. Apri il WebModule e modifica il codice in modo che assomigli al seguente.

Modifiche da eseguire al WebModule per registrare controller e middleware

Fatto! Eseguiamo il progetto e utilizziamo la nostra API.

Testare la nostra Web API

Per utilizzare e testare l’API CRUD utilizzerò il client InsomniaREST disponibile su (https://insomnia.rest)[https://insomnia.rest]. Se non conosci Insomnia, ci sono (https://www.google.com/search?q=insomnia+rest+tutorial)[molti tutorial] utili per iniziare, puoi dargli dai un’occhiata. Se sei abituato a Postman o Embarcadero REST Debugger, va bene lo stesso.

Cominciamo recuperando un semplice elenco di clienti.

GET http://localhost:8080/api/v1/customers

{
  "data": [
    {
      "id": 4500,
      "code": "00008",
      "companyname": "Burger Inc.",
      "city": "Melbourne",
      "rating": 3,
      "note": "GAS",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4500",
          "rel": "self"
        }
      ]
    },
    {
      "id": 4501,
      "code": "00086",
      "companyname": "Motors Corp.",
      "city": "New York",
      "rating": 0,
      "note": "House",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4501",
          "rel": "self"
        }
      ]
    },
    {
      "id": 4503,
      "code": "00033",
      "companyname": "Motors Srl",
      "city": "New York",
      "rating": 2,
      "note": "Burger",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4503",
          "rel": "self"
        }
      ]
    },
    {
      "id": 4504,
      "code": "00043",
      "companyname": "Burger Inc.",
      "city": "London",
      "rating": 2,
      "note": "Motors",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4504",
          "rel": "self"
        }
      ]
    }
  ]
}

Il risultato viene automaticamente inserito in un oggetto json con un array di proprietà dei dati. Questo approccio rappresenta una procedura consigliata ed è piuttosto utile ed efficiente rispetto alla restituzione diretta dell’array JSON. Ogni oggetto cliente contiene una proprietà aggiuntiva “collegamenti” con informazioni relative all’HATEOAS (questo è tutto, collegamenti per scoprire altre risorse correlate).

DMVCFramework ha integrato il supporto RQL. La nostra piccola API supporta già tutti i tipi di filtri utilizzando RQL. Diciamo che abbiamo bisogno di tutti i clienti con ID ≥ 4503 e ID ≤ 4504 ordinati per rating decrescente e città crescente.

GET http://localhost:8080/api/v1/customers?rql=and(ge(id,4503),le(id,4504));sort(-rating,+city)

{
  "data": [
    {
      "id": 4503,
      "code": "00033",
      "companyname": "Motors Srl",
      "city": "New York",
      "rating": 4,
      "note": "Burger",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4503",
          "rel": "self"
        }
      ]
    },
    {
      "id": 4504,
      "code": "00043",
      "companyname": "Burger Inc.",
      "city": "London",
      "rating": 2,
      "note": "Motors",
      "links": [
        {
          "type": "application/json",
          "href": "/api/v1/customers/4504",
          "rel": "self"
        }
      ]
    }
  ]
}

Creiamo ora un nuovo cliente utilizzando la seguente richiesta POST con il relativo body.

POST http://localhost:8080/api/v1/customers

Corpo della richiesta POST

L’API restituisce la risorsa appena creata nell’intestazione “location” come mostrato di seguito.

Risposta alla precedente richiesta POST

Utilizzando l’URI restituito e l’API PUT possiamo modificare il cliente appena creato.

Corpo della richiesta PUT

Il comportamento predefinito di DMVCFramework consente di eseguire aggiornamenti parziali. Gli aggiornamenti parziali per impostazione predefinita funzionano utilizzando i seguenti criteri:

  • Se il corpo della richiesta non contiene l’attributo, lascia il valore invariato sull’oggetto serializzato;
  • Se il corpo della richiesta contiene l’attributo, aggiorna l’attributo mappato sull’oggetto con il nuovo valore;
  • Se la richiesta del corpo contiene l’attributo con valore null, imposta l’attributo come null nell’attributo mappato dell’oggetto;

Quindi, in breve, se l’attributo non è contenuto nel body della richiesta tale attributo rimane invariato nell’oggetto deserializzato.

Ecco l’esempio di una richiesta PUT utilizzata per eseguire aggiornamenti parziali.

Conclusioni

In questo articolo abbiamo visto come DMVCFramework renda davvero semplice la creazione di API potenti e RESTful. Permette di produrre automaticamente strutture JSON a partire da oggetti Plain Delphi. Il supporto RQL integrato consente di implementare facilmente il filtraggio, l’ordinamento e il limite delle risorse. Se ti è piaciuto questo articolo e la produttività di DMVCFramework, non puoi perderti la sezione “DMVCFramework - la guida ufficiale”. Sfrutta la potenza di REST e JSON-RPC utilizzando il framework più popolare per Delphi.

Come proseguire?

Nel prossimo articolo di questa serie, disponibile solo per utenti patreon, inizieremo a migliorare l’API con logica di business, Swagger/OpenAPI, HATEOAS, JSON Web Token e risorse nidificate.

Stay Tuned!

Comments

comments powered by Disqus