SQL och Node

Installera mysql paketet för node med npm.

Installera paketen mysql och dotenv för Node.

npm install mysql
  • mysql paketet innehĂ„ller drivrutiner för att anvĂ€nda mysql med node.

  • dotenv Ă€r ett paket för att skapa konfiguarationsfiler. Att spara konfigurationsdata separerat frĂ„n koden Ă€r god praxis.

En databas behöver anvÀndaruppgifter för att koppla upp sig till en databasserver. Denna data Àr kÀnslig och du behöver skydda den, dÀrför Àr det viktigt att inte ladda upp denna data pÄ GitHub. För att underlÀtta det anvÀnds paketet dotenv. Med dotenv kan vi skapa en konfigurationsfil och med hjÀlpa av .gitignore sÄ sÀkerstÀller vi att den inte laddas upp pÄ GitHub.

npm install mysql dotenv

Koppla upp

Detta exempel Àr plockat frÄn mysql-paketets hjÀlpsida, den Äterfinns Àven i Express-dokumentation. Ofta ser Node-hjÀlp ut pÄ detta sÀttet och det kan vara svÄrt att fÄ svar pÄ var detta ska skrivas(vi kommer till det). Du behöver inte koda detta, men om du gör det sÄ kan du lÀgga koden i en route.

const mysql = require('mysql');
const connection = mysql.createConnection({
  host     : 'example.org',
  user     : 'bob',
  password : 'secret'
});
 
connection.connect(function (err) {
  if (err) {
    console.error('error connecting: ' + err.stack);
    return;
  }
 
  console.log('connected as id ' + connection.threadId);
});

Först skapas en const för mysql-paketet, drivrutinen krÀvs. Efter det sÄ skapas en uppkoppling till mysql-servern utifrÄn den angivna konfigurationen. DÀrefter testats uppkopplingen med connect metoden och vid eventuellt fel sÄ loggas det, annars sÄ loggas uppkopplingen. Koden kan testas i app.js eller i en route fil för express om sÄ önskas.

Node kan fÄ problem med auth till MySql 8. DÄ behöver du uppdatera din mysql-anvÀndare pÄ mysql servern. SQL frÄgan för detta finns hÀr nedanför.

ALTER USER 'username'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
FLUSH privileges;

Konfiguration

Det första som behöver ÄtgÀrdas Àr konfigurationsvÀrdena i exemplet och för det sÄ ska vi anvÀnda dotenv paketet. En dotenv fil Àr en konfigurationsfil(eller en fil med miljövariabler, environment). Filnamnet börjar med en punkt, vilket Àr en Linux standard för att visa att det rör sig om en dold fil(vilket ofta konfigurationsfiler Àr).

Att arbeta med konfigurationen för ett projekt i en separat fil Àr en god praxis som lÄter dig konfigurera upp ett projekt utan att Àndra i koden. Att separera konfigurationen skyddar kÀnslig konfigurationsdata(lösenord, anvÀndare api-nycklar) frÄn att laddas upp pÄ till exempel GitHub. dotenv filen skapas i projektets root.

  1. Skapa först en fil med namnet .env-example utan konfigurationsvÀrden.

  2. Kopiera sedan filen till en ny fil med namnet .env och fyll i konfigurationsvÀrdena.

För en databaskonfiguration behövs, host, username, password och databas.

.env-example
DB_HOST=
DB_USER=
DB_PASS=
DB_DATABASE=
.env
DB_HOST=127.0.0.1
DB_USER=username
DB_PASS=password
DB_DATABASE=databasename

Skapa sedan en .gitignore fil i projektets root för att kontrollera sÄ att dotenv filen inte laddas upp pÄ GitHub.

.gitignore
# dotenv environment variables file
.env

Utan .gitignore kvittar det att du skapar .env filerna.

För att komma Ät vÀrdena frÄn dotenv filen sÄ behöver paketet laddas in sÄ tidigt som möjligt i applikationen. VÀrdena Äterfinns sedan i objektet process.env.

Kom ihÄg att starta om din server efter du Àndrat i dotenv filen, sÄ att Àndringarna laddas.

app.js
require('dotenv').config();

Med dotenv paketet laddat kan det ovanstÄende exemplet Àndras till.

const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_DATABASE
});
 
connection.connect(function (err) {
  if (err) {
    console.error('error connecting: ' + err.stack);
    return;
  }
 
  console.log('connected as id ' + connection.threadId);
});

Databasmodell

För att anvÀnda mysql kommer vi att skapa en ÄteranvÀndbar modell. Denna modell kommer Àven att anvÀnda en pool med uppkopplingar. Modellen kommer att exportera en skapad pool för anvÀndning.

Skapa en ny mapp models i projektets root och i models en fil med namn, db.js.

models/db.js
const mysql = require('mysql');

const pool = mysql.createPool({
  connectionLimit: 10,
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_DATABASE
});

module.exports = pool;

Skapa sedan en ny route fil för att testa modellen, routes/dbtest.js.

routes/dbtest.js
const express = require('express');
const router = express.Router();
const pool = require('../models/db');

pool.getConnection((error, connection) => {
  if (error) {
    throw error;
  }

  res.send('connected as id ' + connection.threadId);

  connection.release();
});

module.exports = router;

App.js behöver uppdateras för att ladda routen, gör följande Àndring dÀr de andra routerna laddas och anvÀnds.

app.js
...
const dbtestRouter = require('./routes/dbtest');
...
app.use('/dbtest', dbtestRouter);

Databasmodellen sparas i variabeln pool för anvÀndning. Sedan anvÀnds metoden .getConnection() för att hÀmta en uppkoppling frÄn poolen. Sidan visar sedan uppkopplingens id innan uppkopplingen slÀpps, release. Testa vad som sker utan connection.release().

DatabasfrÄga

För att testa att kommunicera med en databas behöver du starta en mysql-server och importera databasen frÄn föregÄende kapitels exempel.

Databasdump finns hÀr. AnvÀnd wget för att hÀmta den.

wget https://raw.githubusercontent.com/jensnti/Webbserverprogrammering/master/exempel/meeps.sql

Du kan ocksÄ anvÀnda Tableplus för att importera databaser.

NÀr du laddat ned filen sÄ förbered för att importera den med att skapa en databas(om det behövs).

sudo service mysql restart
mysql -u USERNAME -p DATABASE < FILENAME

Ändra sedan test-routen till att innehĂ„lla en faktisk SQL frĂ„ga. Routen kommer nu att svara pĂ„ /test och svara med resultatet av databasfrĂ„gan i JSON. Du kan Ă€ven logga(console.log eller console.table) resulatet frĂ„n databasfrĂ„gan för att se hur objektet ser ut(det skrivs dĂ„ i terminalen dĂ€r du startat node).

LÀr dig anvÀnda console funktionera för att felsöka din kod. Det finns ett antal olika console funktioner du kan anvÀnda.

routes/dbtest.js
router.get('/', function (req, res, next) {
  const sql = 'SELECT * FROM meeps';

  pool.query(sql, function (err, result, fields) {
    if (err) throw err;
    console.table(result);
    res.json({
      status: 200,
      result
    });
  });
});

Databasresultat till en view

I det hÀr steget ska vi skapa en view som kan visa resultatet av en SQL frÄga. Detta för att sammankoppla alla delarna. För att göra detta sÄ behöver routen Àndras. Test-routens respons ska anvÀnda render metoden med en test-view och resultatets data.

routes/dbtest.js
router.get('/', function (req, res, next) {
  const sql = 'SELECT * FROM meeps';
  
  pool.query(sql, function (err, result, fields) {
    if (err) throw err;
    
    res.render('test', { result: result });
  });
});

Skapa sedan en view som heter test. I en första test skriver vi ut vÀrdena frÄn mysql resultatet med interpolation i Pug. Det Àr dÄ viktigt att se till att vÀrden som skrivs ut escapas, detta för att skadlig kod eventuellt kan sparas i en databas och sedan reproduceras för en anvÀndare pÄ webbplatsen.

AnvÀnd console.log() i routen för att felsöka och undersöka data. Detta dumpar datan till den terminal dÀr du startat upp node.

Resultatet mÄste itereras, eftersom det Àr en array. För att göra det anvÀnds Pugs each funktion.

views/dbtest.pug
extends layout

block content
  each row in result
    p #{row.body}
    | #{row.updated_at}

För att fÄ ut den fullstÀndiga information, med författarens namn sÄ behöver SQL frÄgan anvÀnda en join pÄ users tabellen. Uppdatera koden som följer.

routes/dbtest.js
const sql = 'SELECT meeps.*, users.name FROM meeps JOIN users ON meeps.user_id = users.id';

NÀsta steg blir sedan att utveckla test-viewen. Detta kan med fördel göras som en mixin. Koden som följer skapar ett kort för en post.

views/dbtest.pug
  .card
    .card-body
      p.card-text= result.body
    .card-footer.d-flex
      p.small Posted by 
        a(href="#")= result.name
        |  on 
        time(date="#{updated}")= result.updated_at

Övning

  • Skapa en mixin som du kan kalla pĂ„ för varje rad i databasen, antingen skriver du kod för ett eget kort med css eller sĂ„ anvĂ€nder du ett Bootstrap kort.

Databas selektion

I SQL sÄ kan WHERE anvÀndas för att vÀlja rader utifrÄn ett logiskt uttryck. För att vÀlja en specifik meeps sÄ anvÀnds ID kolumnen. Det ser ut som följer med resultat.

SELECT * FROM meeps WHERE id = 2;
+----+-------------+--------------------------+---------------------+---------------------+---------+
| id | title       | body                     | created_at          | updated_at          | user_id |
+----+-------------+--------------------------+---------------------+---------------------+---------+
|  2 | Hello there | Lorem ipsum dolor sit... | 2020-09-25 13:04:34 | 2020-09-25 13:19:53 |       1 |
+----+-------------+--------------------------+---------------------+---------------------+---------+

Du kan sjÀlvklart vÀlja data utifrÄn andra fÀlt som user_id för att vÀlja alla poster frÄn en specifik anvÀndare. För wildcard sÄ anvÀnds %.

Övning

  • Prova att skriva en SQL frĂ„ga som lĂ„ter dig söka efter mailadresser i databasen. TĂ€nka att anvĂ€ndaren ska kunna söka efter ett förnamn och fĂ„ tillbaka matchande adresser. AnvĂ€nd SQL wildcards, %.

Selektion och node

För att kunna vÀlja rader frÄn databasen med specifika ID, sÄ mÄste servern kunna ta emot ID parametern. För att skicka en parameter behöver en route skapas eller Àndras för att tillÄta detta. DÄ anvÀnds :PARAMETERNAMN i routen.

router.get('/:id', function (req, res, next) {
  // console.log(req.params); hela objeketet
  console.log(req.params.id); // id parameterns vÀrde
});

Denna route pÄ / lÀser in en parameter med namnet :id. VÀrdet pÄ parametrarna Äterfinns i requestens(req) parameter-objekt. Du kommer Ät detta genom punkt-notation.

VÀrdet som skickats med en parameter kan sedan anvÀndas i SQL frÄgan, det görs genom en sÄ kallad prepared statment. Prepared statements(Àven parameterized query) anvÀnds av sÀkerhetsskÀl för att undvika SQL-injektioner.

routes/dbtest.js
router.get('/:id', function (req, res, next) {
  const sql = 'SELECT * FROM meeps WHERE id = ?';

  pool.query(sql, [req.params.id], function (err, result, fields) {
    if (err) throw err;
    res.json({
      status: 200,
      id: req.params.id,
      result: result
    });
  });
});

Ändringen i test-routen skapar en route för get som tillĂ„ter en id parameter. Rad 2 skapar SQL-frĂ„gan dĂ€r frĂ„getecknet Ă€r en parameter. FrĂ„gan körs sedan pĂ„ rad 4, dĂ„ ersĂ€tts ? med [req.params.id]. Det gĂ„r utmĂ€rkt att anvĂ€nda flera parameterar(tĂ€nk post med ett formulĂ€r). Med flera parametrar Ă€r det viktigt att de Ă€r satta i rĂ€tt ordning.

Om frÄgan exekveras korrekt sÄ returneras svaret som json till webblÀsaren.

Övning

Skapa lÀnkar till enskilda meeps.

  • test.pug, ge varje meep en lĂ€nk till /test/:id

  • skapa /test/:id routen

  • skapa SQL som hĂ€mtar vald meep utifrĂ„n :id (inklusive join)

  • skapa en meep.pug views som visar vald meep(ersĂ€tt json i exemplet ovan)

Databasmodell, asynkrona frÄgor

Ibland sÄ uppstÄr problem med databasuppkopplingen eller sÄ behövs det flera uppkopplingar. Vid dessa tillfÀllen sÄ kan du behöva större kontroll över vad som sker och hur data levereras till klienten. En lösning pÄ detta Àr att göra asynkrona anrop, dÄ kan du styra över att din kod ska invÀnta svar frÄn databasen.

För att möjliggöra asynkrona anrop Àndras databasmodellen till följande.

models/db.js
const mysql = require('mysql');

const pool = mysql.createPool({
  connectionLimit: 10,
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_DATABASE
});

function query(sql, params) {
  return new Promise((resolve, reject) => {
    pool.query(sql, ...params, function (err, result, fields) {
      if (err) reject(err);
      resolve(result);
    });
  });
}

module.exports = { pool, query };

Query funktionen accepterar en SQL frÄga samt tillhörande parametrar.

Flera frÄgor

Nu kan query funktionen anvÀndas i en route genom att requira den. Med query funktionen kan routen göras asynkron med nyckelordet async.

Async tillÄter anvÀndningen av await vilket tillÄter koden att invÀnta svar. Det görs genom nÄgot som kallas för promises. En promise resulterar i att den antingen resolves eller rejects, körs eller misslyckas. MDN async function.

I koden nedan sÄ Àndras users routen för att tillÄta anvÀndaren att köra den med en parameter :id, anvÀndarens id. För att illustrera dubbla SQL-frÄgor sÄ hÀmtas dÄ Àven den anvÀndarens alla tillhörande meeps. Utan att anvÀnda async och await sÄ vÀntar aldrig scriptet pÄ att dessa frÄgor ska köras, utan returnerar ingen data. JÀmför koden hÀr nedan och prova bÄda versionerna.

routes/users.js
var express = require('express');
var router = express.Router();
const { query } = require('../models/db');

router.get('/', function (req, res, next) {
  res.render('users', { title: 'Userpage', users: ['Hans', 'Moa', 'Bengt', 'Frans', 'Lisa'] });
});

router.get('/:id', function (req, res, next) {
  try {
    const user = query(
      'SELECT * FROM users WHERE id = ?',
      req.params.id
    );

    const meeps = query(
      'SELECT * FROM meeps WHERE user_id = ?',
      req.params.id
    );

    res.render('users', {
      id: req.params.id,
      user: user,
      meeps: meeps
    });
  } catch (e) {
    console.error(e);
    next(e);
  }
});

module.exports = router;

Koden vÀntar pÄ att promises för bÄda SQL-frÄgorna ska bli "klar". NÀr den Àr klar sÄ renderas users-view med databas-frÄgornas data. FrÄgorna körs i ett try block för att fÄnga eventuella fel med catch. Om ett fel uppstÄr sÄ fÄngas det upp och vi anvÀnder Express inbyggda felhanterare, next(error).

I anvÀndar-vyn sÄ anvÀnds selektion i Pug-koden för att det inte ska visas fel nÀr data saknas(testa att köra utan if-satserna).

Last updated