Sommario
< Home
Stampa

Esempio di progetto NodeJs con database

In questa lezione andiamo a vedere come realizzare la parte server di una applicazione con database. Come tecnologia di database useremo Mysql.

Descrizione del progetto (analisi funzionale)

Come esercizio ipotizziamo che il comando dei vigili urbani di una città decida di sviluppare una applicazione web (Intranet) per gestire l’inserimento e la visualizzazione degli incidenti in città. Per la parte di frontend è prevista una pagina con la mappa (e la lista) degli incidenti, con un marcatore sulla mappa di ogni incidente. La tabella permette di filtrare per indirizzo/targa. E’ possibile tramite pulsante e relativa modale inserire un nuovo incidente.

Gli incidenti sono memorizzati in database. Ogni incidente ha le seguenti informazioni:

  • indirizzo (compreso di civico)
  • targhe coinvolte
  • data
  • ora
  • numero feriti
  • numero morti

Architettura dell’informazione

L’architettura dell’informazione prevede quindi che il dato gestito sia una lista di incidenti (con gli attributi sopra descritti), che sia previsto un flusso di informazione che centralizza la memorizzazione dei dati su un server unico accessibile da molteplici clients. Ogni client visualizza i dati sotto forma di mappa e lista, e permette di inserire un nuovo incidente. Per gestire il progetto si decide di usare questa architettura modulare.

I flussi previsti sono i seguenti:

  • ad ogni avvio dell’applicazione client, questa carica la lista incidenti. L’azione è iniziata dal controller, che a sua volta attiva il middleware client, che esegue la richiesta HTTP al middleware server, che a sua volta esegue l’analoga funzione su database module lato server che esegue la query; nel flusso di ritorno vengono formattati i dati in formato JSON che viene restituito dal web service nella risposta HTTP. Infine il controller esegue la render (trasformazione in html) del risultato tramite mappa o lista.
  • L’inserimento è una operazione eseguita dall’utente, che attiva il controller, che a sua volta invia il nuovo incidente al middleware client, viene generata la richiesta HTTP ricevuta dal middleware server. Questo invia l’oggetto ricevuto al layer di database, che esegue la query di creazione del nuovo oggetto.

Database

Abbiamo individuato le quattro operazioni fondamentali su una entità di un database (CRUD, CREATE READ UPDATE DELETE) che consentono di manipolare la singola entità. In questo esercizio ci sono tuttavia due entità. La prima rappresenta il singolo incidente coi suoi attributi, Tutavia le targhe coinvolte sono una entità separata che necessita di una tabella apposita. Lo schema logico risultante è il seguente:

Un po’ di SQL

Prima di scrivere il codice su NodeJS ripassiamo le query principali. Partiamo dalla creazione della tabella accident

CREATE TABLE IF NOT EXISTS accident (
         id INT PRIMARY KEY AUTO_INCREMENT,
         address varchar(255) NOT NULL,
         date DATE NOT NULL,
         time TIME NOT NULL,
         injured int NOT NULL,
         dead int NOT NULL) 

Come si vede si usano le colonne di tipo DATE e TIME per memorizzare data ed ora. Questo è comodo perché ci permette di usare gli input html date e time senza fare complicate trasformazioni di tipo.

Vediamo ora la sua tabella dipendente, ovvero plates

CREATE TABLE IF NOT EXISTS plates (
            id INT PRIMARY KEY AUTO_INCREMENT,
            plate VARCHAR(20) NOT NULL,
            idAccident INT NOT NULL,
            FOREIGN KEY (idAccident) REFERENCES accident(id) ON DELETE CASCADE) 

In questo caso definiamo la chiave idAccident e il relativo vincolo di chiave esterna. Aggiungiamo anche la regola ON DELETE CASCADE che elimina automaticamente le righe se viene eliminato l’incidente collegato.

Vediamo adesso l’inserimento:

INSERT INTO accident(address, date, time, injured, dead)
         VALUES (...)

Per scrivere su plates occorre avere invece l’id dell’oggetto appena creato (lo vedremo in NodeJs) e la query sarà la seguente:

INSERT INTO plates(plate, idAccident) 
            VALUES ('...', XXX)

dove l’id è quello già in nostro possesso.

Veniamo alla Select.

SELECT id, address, date, time, injured, dead FROM accident 

E’ molto importante capire perché quando si fa una select è sempre necessario mettere tutti i campi, almeno in un software che esegue automaticamente la query, e non usare quindi “select *”. Le ragioni sono le seguenti:

  • l’enumerazione delle colonne richieste evita di richiedere colonne che non ci servono ma che sono presenti nel database (le prestazioni migliorano);
  • evitiamo di richiedere colonne che non esistono per un nostro errore di codice, o un errore nella query di creazione;
  • evitiamo di richiedere colonne che non esistono più perché il database è stato modificato nel frattempo

In sostanza dobbiamo ricordare sempre che una query è una richiesta ad un sistema software esterno su cui non abbiamo controllo mentre scriviamo il codice. Se usiamo select * decidiamo inconsapevolmente di accettare un risultato senza ulteriori controlli che potrebbe generare errori in altri punti del programma. Errori difficili da individuare e che richiedono tempo per essere risolti. Meglio avere un errore subito anziché subirne le conseguenze.

Infine abbiamo la delete

DELETE FROM accident
        WHERE id=XXX

E’ importante osservare che la delete di plates non è necessaria, il vincolo CASCADE visto sopra cancella automaticamente anche le corrispondenti righe nella tabella secondaria.

Modulo database

A questo punto possiamo scrivere il modulo database.js su Node:

const fs = require('fs');
const mysql = require('mysql2');
const conf = JSON.parse(fs.readFileSync('conf.json'));
conf.ssl = {
   ca: fs.readFileSync(__dirname + '/ca.pem')
}
const connection = mysql.createConnection(conf);

const executeQuery = (sql) => {
   return new Promise((resolve, reject) => {
      connection.query(sql, function (err, result) {
         if (err) {
            console.error(err);
            reject();
         }
         console.log('done');
         resolve(result);
      });
   })
}

const database = {
   createTable: async () => {
      await executeQuery(`
         CREATE TABLE IF NOT EXISTS accident (
         id INT PRIMARY KEY AUTO_INCREMENT,
         address varchar(255) NOT NULL,
         date DATE NOT NULL,
         time TIME NOT NULL,
         injured int NOT NULL,
         dead int NOT NULL)             
      `);
      return await executeQuery(`
         CREATE TABLE IF NOT EXISTS plates (
            id INT PRIMARY KEY AUTO_INCREMENT,
            plate VARCHAR(20) NOT NULL,
            idAccident INT NOT NULL,
            FOREIGN KEY (idAccident) REFERENCES accident(id) ON DELETE CASCADE)      
      `);
   },
   insert: async (accident) => {
      let sql = `
         INSERT INTO accident(address, date, time, injured, dead)
         VALUES (
            '${accident.address}', 
            '${accident.date}', 
            '${accident.time}', 
            ${accident.injured}, 
            ${accident.dead})
           `;
      const result = await executeQuery(sql);
      accident.plates.forEach(async (element) => {
         sql = `
            INSERT INTO plates(plate, idAccident) 
            VALUES (
               '${element}', 
               ${result.insertId})
         `;
         await executeQuery(sql);
      });
   },
   delete: (id) => {
      let sql = `
        DELETE FROM accident
        WHERE id=${id}
           `;
      return executeQuery(sql);
   },
   select: async () => {
      let sql = `
        SELECT id, address, date, time, injured, dead FROM accident 
           `;
      const result = await executeQuery(sql);
      await Promise.all(result.map(async (accident) => {
         sql = `
            SELECT plate FROM plates WHERE idAccident=${accident.id} 
           `;
         const list = await executeQuery(sql);
         accident.plates = list.map(p => p.plate);
      }));
      return result;
   },
   drop: async () => {
      let sql = `
            DROP TABLE IF EXISTS plates
           `;
      await executeQuery(sql);
      sql = `
            DROP TABLE IF EXISTS accident
           `;
      await executeQuery(sql);
   }
}

module.exports = database;

Notare che si è aggiunta anche la funzione DROP, a solo scopo di debugging.

Per quanto riguarda la select va osservato bene questo codice:

select: async () => {
      let sql = `
        SELECT id, address, date, time, injured, dead FROM accident 
           `;
      const result = await executeQuery(sql);
      await Promise.all(result.map(async (accident) => {
         sql = `
            SELECT plate FROM plates WHERE idAccident=${accident.id} 
           `;
         const list = await executeQuery(sql);
         accident.plates = list.map(p => p.plate);
      }));
      return result;

Per ogni riga scaricata viene eseguita una seconda query che legge tutte le targhe associate e costruisce un array coi nomi. In pratica vogliamo costruire un oggetto con questa struttura di esempio:

{
        address: "via Torino 22",
        date: "2025-02-15,
        time: "13:20:51,
        injured: 2,
        dead: 1,
        plates: ["CA231BC", "GT334PH"]
}

Questa struttura è molto comoda come formato dati di scambio con il client.

Modulo server.js (Middleware server)

const express = require("express");
const http = require('http');
const path = require('path');
const app = express();
const database = require("./database");
database.createTable();

app.use("/", express.static(path.join(__dirname, "public")));
app.post("/insert", async (req, res) => {
  const accident = req.body.accident;
  try {
    await database.insert(accident);
    res.json({result: "ok"});
  } catch (e) {
    res.status(500).json({result: "ko"});
  }
})
app.get('/accidents', async (req, res) => {
    const list = await database.select();
    res.json(list);
});
app.delete('/delete/:id', async (req, res) => {
  await database.delete(req.params.id);
  res.json({result: "ok"});
})
const server = http.createServer(app);
const port = 5600;
server.listen(port, () => {
  console.log("- server running on port: " + port);
});

I web services che andiamo a creare sono i seguenti:

URL pathmetododescrizione
/getmappa la cartella public per l’html/JS lato client
/accidentsgetesegue una select e restituisce tutti gli incidenti. Il formato dell’array di JSON è quello indicato sopra.
/delete/:iddeleteelimina un incidente
/insertpostinserisce un nuovo incidente. Il formato del JSON è quello indicato sopra.

Modulo middleware client

La parte client è lasciata per esercizio (riusare le specifiche del progetto Vigili Urbani). Si riporta qui solo il middleware lato client.

const createMiddleware = () => {
    return {
      load: async () => {
        const response = await fetch("/accidents");
        const json = await response.json();
        return json;
      },
      delete: async (id) => {
        const response = await fetch("/delete/" + id, {
          method: 'DELETE',
        });
        const json = await response.json();
        return json;
      },
      add: async (accident) => {
        const response = await fetch("/insert", {
            method: 'POST',
            headers: {
                "content-type": "application/json"
            },
            body: JSON.stringify({
                accident: accident
            })
        });
        const json = await response.json();
        return json;        
      }
    }
  }

  export default createMiddleware;

Alcune note finali:

  • nella creazione della pagina web, ricordarsi di gestire nelle tabelle il pulsante di cancellazione
  • nella creazione della form usare una struttura come la seguente:
<form id="accidentForm">
        <label for="address">Indirizzo:</label>
        <input type="text" id="address" required>

        <label for="date">Data:</label>
        <input type="date" id="date" required>

        <label for="time">Ora:</label>
        <input type="time" id="time" required>

        <label for="injured">Feriti:</label>
        <input type="number" id="injured" min="0" required>

        <label for="dead">Morti:</label>
        <input type="number" id="dead" min="0" required>

        <button id="submi" type="button">Registra</button>
    </form>

Da qui la conversione nel formato data e ora corretto è molto semplice.