Siirry sisältöön
b Liitostaulut ja -kyselytc Migraatiot, monen suhde moneen -yhteydet

a

Relaatiotietokannan käyttö Sequelize-kirjastolla

Tässä osassa tutustutaan relaatiotietokantoja käyttäviin Node-sovelluksiin. Osassa rakennetaan osista 3-5 tutulle muistiinpanosovellukselle relaatiotietokantaa käyttävä Node-backend. Osan suorittaminen edellyttää kohtuullista relaatiotietokantojen ja SQL:n osaamista. Eräs tapa hankkia riittävä osaaminen on kurssi Tietokantojen perusteet.

Osassa on 24 tehtävää, ja suoritusmerkintä edellyttää kaikkien tekemistä. Toisin kuin osat 0-7, tämä tehtävä palautetaan palautussovelluksessa omaan kurssi-instanssiinsa.

Dokumenttitietokantojen edut ja haitat

Olemme käyttäneet kaikissa kurssin aiemmissa osissa MongoDB-tietokantaa. Mongo on tyypiltään dokumenttitietokanta ja eräs sen ominaisimmista piirteistä on skeemattomuus, eli tietokanta ei ole kuin hyvin rajallisesti tietoinen siitä, minkälaista dataa sen kokoelmiin on talletettu. Tietokannan skeema on olemassa ainoastaan ohjelmakoodissa, joka tulkitsee datan tietyllä tavalla, esim. tunnistaen että jotkut kentät ovat viittauksia toisen kokoelman objekteihin.

Osien 3 ja 4 esimerkkisovelluksessa tietokantaan on talletettu muistiinpanoja ja käyttäjiä.

Muistiinpanoja tallettava kokoelma notes näyttää seuraavanlaiselta

[
  {
    "_id": "600c0e410d10256466898a6c",
    "content": "HTML is easy",
    "date": 2021-01-23T11:53:37.292+00:00,
    "important": false,
    "user": "600c0e410d10256466883a6a",
    "__v": 0
  },
  {
    "_id": "600c0edde86c7264ace9bb78",
    "content": "CSS is hard",
    "date": 2021-01-23T11:56:13.912+00:00,
    "important": true,
    "user": "600c0e410d10256466883a6a",
    "__v": 0
  },
]

Käyttäjät tallettava kokoelma users seuraavalta:

[
  {
    "_id": "600c0e410d10256466883a6a",
    "username": "mluukkai",
    "name": "Matti Luukkainen",
    "passwordHash" : "$2b$10$Df1yYJRiQuu3Sr4tUrk.SerVz1JKtBHlBOARfY0PBn/Uo7qr8Ocou",
    "__v": 9,
    notes: [
      "600c0edde86c7264ace9bb78",
      "600c0e410d10256466898a6c"
    ]
  },
]

MongoDB tuntee kyllä talletettujen olioiden kenttien tyypit, mutta sillä ei ole mitään tietoa siitä, minkä kokoelman olioihin käyttäjiin liittyvät muistiinpanojen id:t viittaavat. MongoDB ei myöskään välitä siitä, mitä kenttiä kokoelmiin talletettavilla olioilla on. MongoDB jättääkin täysin ohjelmoijan vastuulle sen, että tietokantaan talletetaan oikeanlaista tietoa.

Skeemattomuudesta on sekä etua että haittaa. Eräänä etuna on skeemattomuuden tuoma joustavuus: koska skeemaa ei tarvitse tietokantatasolla määritellä, voi sovelluskehitys olla tietyissä tapauksissa nopeampaa, ja helpompaa, skeeman määrittelyssä ja sen muutoksissa on joka tapauksessa nähtävä pieni määrä vaivaa. Skeemattomuuden ongelmat liittyvät virhealttiuteen: kaikki jää ohjelmoijan vastuulle. Tietokannalla ei ole mitään mahdollisuuksia tarkistaa onko siihen talletettu data eheää, eli onko kaikilla pakollisilla kentillä arvot, viittaavatko viitetyyppiset kentät olemassa oleviin ja ylipäätään oikean tyyppisiin olioihin jne.

Tämän osan fokuksessa olevat relaatiotietokannat taas nojaavat vahvasti skeeman olemassaoloon, ja skeemallisten tietokantojen edut ja haitat ovat lähes päinvastaiset skeemattomiin verrattuna.

Syy sille miksi kurssin aiemmat osat käyttivät MongoDB:tä liittyvät juuri sen skeemattomuuteen, jonka ansiosta tietokannan käyttö on ollut relaatiotietokantoja heikosti tuntevalle jossain määrin helpompaa. Useimpiin tämänkin kurssin käyttötapauksiin olisin itse valinnut relaatiotietokannan.

Sovelluksen tietokanta

Tarvitsemme sovellustamme varten relaatiotietokannan. Vaihtoehtoja on monia, käytämme kurssilla tämän hetken suosituinta Open Source ‑ratkaisua PostgreSQL:ää. Voit halutessasi asentaa Postgresin (kuten tietokantaa usein kutsutaan) koneellesi.

Käytämme nyt hyväksemme sitä, että osista 3 ja 4 tuttuille pilvipalvelualustoille Fly.io ja Heroku on mahdollista luoda sovellukselle Postgres-tietokanta.

Tämän osan teoriamateriaalissa rakennetaan osissa 3 ja 4 rakennetun muistiinpanoja tallettavan sovelluksen backendendistä Postgresia käyttävä versio.

Koska emme tarvitse tässä osassa mihinkään pilvessä olevaa tietokantaa (käytämme sovellusta ainoastaan paikallisesti) on eräs mahdollisuus hyödyntää kurssin osan 12 oppeja ja käyttää Postgresia paikallisesti Dockerin avulla. Pilvipalveluiden Postgresohjeiden jälkeen annamme myös lyhyen ohjeen miten Postgresin saa helposti pystyn Dockerin avulla.

Fly.io

Luodaan nyt sopivan hakemiston sisällä Fly.io-sovellus komennolla fly launch ja luodaan sovellukselle Postgres-tietokanta:

fullstack content

Luomisen yhteydessä Fly.io kertoo tietokannan salasanan, joka tarvitaan jotta sovellus saa yhteyden tietokantaan. Tämä on ainoa kerta kun salasana on mahdollista nähdä tekstimuodossa, joten se on syytä ottaa talteen!

Huomaa, että jos et aio laittaa sovellusta ollenkaan Fly.io:hon, on mahdollista luoda palveluun myös pelkkä tietokanta. Ohjeet siihen täällä.

Tietokantaan saadaan psql-konsoliyhteys komennolla

flyctl postgres connect -a <sovelluksen_nimi-db>

omassa tapauksessani sovelluksen nimi on fs-psql-lecture joten komento on seuraava:

flyctl postgres connect -a fs-psql-lecture-db

Heroku

Käytettäessä Herokua luodaan sopivan hakemiston sisällä Heroku-sovellus, lisätään sille tietokanta ja katsotaan komennolla heroku config mikä on tietokantayhteyden muodostamiseen tarvittava connect string:

heroku create
heroku addons:create heroku-postgresql:hobby-dev
heroku config
=== cryptic-everglades-76708 Config Vars
DATABASE_URL: postgres://<username>:thepasswordishere@ec2-44-199-83-229.compute-1.amazonaws.com:5432/<db-name>

Tietokantaan saadaan psql-konsoliyhteys suorittamalla psql Herokun palvelimella seuraavasti (huomaa, että komennon parametrit riippuvat Heroku-sovelluksen connect urlista):

heroku run psql -h ec2-44-199-83-229.compute-1.amazonaws.com -p 5432 -U <username> <dbname>

Komento kysyy salasanaa ja avaa psql-konsolin:

Password for user <username>:
psql (13.4 (Ubuntu 13.4-1.pgdg20.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=# 

Docker

Tämä ohje olettaa, että hallitset Dockerin peruskäytön esim. osan 12 opettamassa laajuudessa.

Käynnistä Postgresin Docker image komennolla

docker run -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres

Tietokantaan saadaan psql-konsoliyhteys komennon docker exec avulla. Ensin tulee selvittää kontainerin id:

$ docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED          STATUS          PORTS                    NAMES
ff3f49eadf27   postgres   "docker-entrypoint.s…"   31 minutes ago   Up 31 minutes   0.0.0.0:5432->5432/tcp   great_raman
docker exec -it ff3f49eadf27 psql -U postgres postgres
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.

postgres=#

Näin määriteltynä tietokantaan talletettu data säilyy ainoastaan niin kauan kontti on olemassa. Data saadaan säilymään määrittelemällä datan talletukseen volume, katso lisää täältä.

psql-konsolin käyttöä

Erityisesti relaatiotietokantaa käytettäessä on oleellista päästä tietokantaan käsiksi myös suoraan. Tapoja tähän on monia, on olemassa mm. useita erilaisia graafisia käyttöliittymiä, kuten pgAdmin. Käytetään nyt kuitenkin Postgresin psql-komentorivityökalua.

Kun konsoli on avattu, kokeillan psql:n tärkeintä komentoa \d, joka kertoo tietokannan sisällön:

postgres=# \d
Did not find any relations.

Kuten arvata saattaa, tietokannassa ei ole mitään.

Luodaan taulu muistiinpanoja varten:

CREATE TABLE notes (
    id SERIAL PRIMARY KEY,
    content text NOT NULL,
    important boolean,
    date time
);

Muutama huomio: sarake id on määritelty pääavaimeksi (engl. primary key), eli sarakkeen arvon tulee olla jokaisella taulun rivillä uniikki ja arvo ei saa olla tyhjä. Tyypiksi sarakkeelle on määritelty SERIAL, joka ei ole todellinen tyyppi vaan lyhennysmerkintä sille, että kyseessä on kokonaislukuarvoinen sarake, jolle Postgres antaa automaattisesti uniikin, kasvavan arvon rivejä luotaessa. Tekstiarvoinen sarake content on määritelty siten, että sille on pakko antaa arvo.

Katsotaan tilannetta konsolista käsin. Ensin komento \d, joka kertoo mitä tauluja kannassa on:

postgres=# \d
                 List of relations
 Schema |     Name     |   Type   |     Owner
--------+--------------+----------+----------------
 public | notes        | table    | postgres
 public | notes_id_seq | sequence | postgres
(2 rows)

Taulun notes lisäksi Postgres loi aputaulun notes_id_seq, joka pitää kirjaa siitä, mikä arvo sarakkeelle id annetaan seuraavaa muistiinpanoa luotaessa.

Komennolla \d notes näemme miten taulu notes on määritelty:

postgres=# \d notes;
                                     Table "public.notes"
  Column   |          Type          | Collation | Nullable |              Default
-----------+------------------------+-----------+----------+-----------------------------------
 id        | integer                |           | not null | nextval('notes_id_seq'::regclass)
 content   | text                   |           | not null |
 important | boolean                |           |          |
 date      | time without time zone |           |          |
Indexes:
    "notes_pkey" PRIMARY KEY, btree (id)

Sarakkeella id on siis oletusarvo (default), joka saadaan kutsumalla Postgresin sisäistä funktiota nextval.

Lisätään tauluun hieman sisältöä:

insert into notes (content, important) values ('Relational databases rule the world', true);
insert into notes (content, important) values ('MongoDB is webscale', false);

Ja katsotaan miltä luotu sisältö näyttää:

postgres=# select * from notes;
 id |               content               | important | date
----+-------------------------------------+-----------+------
  1 | relational databases rule the world | t         |
  2 | MongoDB is webscale                 | f         |
(2 rows)

Jos yritämme tallentaa tietokantaan dataa, joka ei ole skeeman mukaista, se ei onnistu. Pakollisen sarakkeen arvo ei voi puuttua:

postgres=# insert into notes (important) values (true);
ERROR:  null value in column "content" of relation "notes" violates not-null constraint
DETAIL:  Failing row contains (9, null, t, null).

Sarakkeen arvo ei voi olla väärää tyyppiä:

postgres=# insert into notes (content, important) values ('only valid data can be saved', 1);
ERROR:  column "important" is of type boolean but expression is of type integer
LINE 1: ...tent, important) values ('only valid data can be saved', 1);                                                                 ^

Skeemassa olemattomia sarakkeita ei hyväksytä:

postgres=# insert into notes (content, important, value) values ('only valid data can be saved', true, 10);
ERROR:  column "value" of relation "notes" does not exist
LINE 1: insert into notes (content, important, value) values ('only ...

Seuraavaksi on aika siirtyä käyttämään tietokantaa sovelluksesta käsin.

Relaatiotietokantaa käyttävä Node-sovellus

Alustetaan sovellus tavalliseen tapaan komennolla npm init ja asennetaan sille kehitysaikaiseksi riippuvuudeksi nodemon sekä seuraavat suoritusaikaiset riippuvuudet:

npm install express dotenv pg sequelize

Näistä jälkimmäinen Sequelize on kirjasto, jonka kautta käytämme Postgresia. Sequelize on niin sanottu Object relational mapping (ORM) ‑kirjasto, joka mahdollistaa JavaScript-olioiden tallentamisen relaatiotietokantaan ilman SQL-kielen käyttöä, samaan tapaan kuin MongoDB:n yhteydessä käyttämämme Mongoose.

Testataan, että yhteyden muodostaminen onnistuu. Luodaan tiedosto index.js ja sille seuraava sisältö:

require('dotenv').config()
const { Sequelize } = require('sequelize')

const sequelize = new Sequelize(process.env.DATABASE_URL)

const main = async () => {
  try {
    await sequelize.authenticate()
    console.log('Connection has been established successfully.')
    sequelize.close()
  } catch (error) {
    console.error('Unable to connect to the database:', error)
  }
}

main()

Huom: Herokua käyttäessä yhteyden muodostuksen saattaa joutua konfiguroimaan seuraavasti:

const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialectOptions: {    ssl: {      require: true,      rejectUnauthorized: false    }  },})

Yhteydenmuodostamista varten tulee tiedostoon .env tallentaa connect string, jonka perusteella yhteyden muodostus tapahtuu.

Herokua käyttäessäsi saat connect stringin selville komennolla heroku config, ja tiedoston .env sisältö tulee olemaan seuraavan kaltainen

$ cat .env
DATABASE_URL=postgres://<username>:thepasswordishere@ec2-54-83-137-206.compute-1.amazonaws.com:5432/<databasename>

Fly.io:a käyttäessä paikallinen tietokantayhteys täytyy ensin tehdä mahdolliseksi tunneloimalla paikallisen koneen portti 5432 Fly.io:n tietokannan porttiin komennolla

flyctl proxy 5432 -a <app-name>-db

omassa tapauksessani komento on

flyctl proxy 5432 -a fs-psql-lecture-db

Komento tulee jättää päälle siksi aikaa kuin tietokantaa käytetään. Konsoli-ikkunaa siis ei saa sulkea.

Fly.io:n connect-string on seuraavaa muotoa:

$ cat .env
DATABASE_URL=postgres://postgres:thepasswordishere@localhost:5432/postgres

Salasana on se, joka on otettu talteen tietokantaa luodessa.

Dockeria käytettäessä connect string on:

DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5432/postgres

Connect stringin viimeinen osa postgres viittaa käytettävään tietokannan nimeen. Nyt se on valmiiksi luotava ja oletusarvoisesti käytössä oleva postgres-niminen tietokanta. Komennolla CREATE DATABASE on tarvittaessa mahdollista luoda muita tietokantoja Postgres-tietokantainstanssiin.

Kun connect string on määritety tiedostoon .env voidaan kokeilla muodostuuko yhteys:

$ node index.js
Executing (default): SELECT 1+1 AS result
Connection has been established successfully.

Jos ja kun yhteys toimii, voimme tehdä ensimmäisen kyselyn. Muutetaan ohjelmaa seuraavasti:

require('dotenv').config()
const { Sequelize, QueryTypes } = require('sequelize')
const sequelize = new Sequelize(process.env.DATABASE_URL)

const main = async () => {
  try {
    await sequelize.authenticate()
    const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT })    console.log(notes)    sequelize.close()  } catch (error) {
    console.error('Unable to connect to the database:', error)
  }
}

main()

Sovelluksen suorituksen pitäisi tulostaa seuraavasti:

Executing (default): SELECT * FROM notes
[
  {
    id: 1,
    content: 'Relational databases rule the world',
    important: true,
    date: null
  },
  {
    id: 2,
    content: 'MongoDB is webscale',
    important: false,
    date: null
  }
]

Vaikka Sequelize on ORM-kirjasto, jota käyttämällä SQL:ää ei juurikaan ole tarvetta itse kirjoittaa, käytimme nyt suoraan SQL:ää Sequelizen metodin query avulla.

Koska kaikki näyttää toimivan, muutetaan sovellus web-sovellukseksi.

require('dotenv').config()
const { Sequelize, QueryTypes } = require('sequelize')
const express = require('express')const app = express()
const sequelize = new Sequelize(process.env.DATABASE_URL)

app.get('/api/notes', async (req, res) => {  const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT })  res.json(notes)})const PORT = process.env.PORT || 3001app.listen(PORT, () => {  console.log(`Server running on port ${PORT}`)})

Sovellus näyttää toimivan. Siirrytään kuitenkin nyt käyttämään Sequelizeä SQL:n sijaan siten kuin sitä on tarkoitus käyttää.

Model

Sequelizea käytettäessä, jokaista tietokannan taulua edustaa model, joka on käytännössä oma JavaScript-luokkansa. Määritellään nyt sovellukselle taulua notes vastaava model Note muuttamalla koodi seuraavaan muotoon:

require('dotenv').config()
const { Sequelize, Model, DataTypes } = require('sequelize')const express = require('express')
const app = express()

const sequelize = new Sequelize(process.env.DATABASE_URL)

class Note extends Model {}
Note.init({  id: {    type: DataTypes.INTEGER,    primaryKey: true,    autoIncrement: true  },  content: {    type: DataTypes.TEXT,    allowNull: false  },  important: {    type: DataTypes.BOOLEAN  },  date: {    type: DataTypes.DATE  }}, {  sequelize,  underscored: true,  timestamps: false,  modelName: 'note'})
app.get('/api/notes', async (req, res) => {
  const notes = await Note.findAll()  res.json(notes)
})

const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

Muutama kommentti koodista. Modelin Note määrittelyssä ei ole mitään kovin yllättävää, jokaiselle sarakkeelle on määritelty tyyppi, sekä tarvittaessa muut ominaisuudet, kuten se onko kyseessä taulun pääavain. Modelin määrittelyssä oleva toinen parametri sisältää sequelize-olion sekä muuta konfiguraatiotietoa. Määrittelimme, että taululla ei ole usein käytettyjä aikaleimasarakkeita (created_at ja updated_at).

Määrittelimme myös underscored: true, joka tarkoittaa sitä, että taulujen nimet johdetaan modelien nimistä monikkomuotoisina snake case ‑versiona. Käytännössä tämä tarkoittaa sitä, että jos modelin nimi on, kuten tapauksessamme, Note päätellään siitä, että vastaavan taulun nimi on pienellä alkukirjaimella kirjoitettu nimen monikko eli notes. Jos taas modelin nimi olisi "kaksiosainen" esim. StudyGroup olisi taulun nimi study_groups. Sequelize mahdollistaa automaattisen taulujen nimien päättelyn sijaan myös eksplisiittisesti määriteltävät taulujen nimet.

Sama käytäntöä nimityksien osalta koskee myös sarakkeita. Jos olisimme määritelleet, että muistiinpanoon liittyy creationYear, eli tieto sen luomisvuodesta, määrittelisimme sen modeliin seuraavasti:

Note.init({
  // ...
  creationYear: {
    type: DataTypes.INTEGER,
  },
})

Vastaavan sarakkeen nimi tietokannassa olisi creation_year. Koodissa viittaus sarakkeeseen tapahtuu aina samassa muodossa mikä on modelissa, eli "camel case"-formaatissa.

Olemme myös määritelleet modelName: 'note', oletusarvoinen "modelin nimi" olisi isolla kirjoitettu Note. Haluamme kuitenkin pienen alkukirjaimen, se tekee muutaman asian jatkossa hieman mukavammaksi.

Tietokantaoperaatio on helppo tehdä modelien tarjoaman kyselyrajapinnan avulla, metodi findAll toimii juuri kuten sen nimen perusteella olettaa toimivan:

app.get('/api/notes', async (req, res) => {
  const notes = await Note.findAll()  res.json(notes)
})

Konsoli kertoo että metodikutsu Note.findAll() aiheuttaa seuraavan kyselyn:

Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note";

Toteutetaan seuraavaksi endpoint uusien muistiinpanojen luomiseen:

app.use(express.json())

// ...

app.post('/api/notes', async (req, res) => {
  console.log(req.body)
  const note = await Note.create(req.body)
  res.json(note)
})

Uuden muistiinpanon luominen siis tapahtuu kutsumalla modelin Note metodia create ja antamalla sille parametriksi sarakkeiden arvot määrittelevän olion.

Metodin create sijaan tietokantaan tallentaminen olisi mahdollista tehdä käyttäen ensin metodia build luomaan halutusta datasta Model-olio, ja kutsumalla sille metodia save:

const note = Note.build(req.body)
await note.save()

Metodin build kutsuminen ei tallenna vielä olioata tietokantaan, joten olio on vielä mahdollista muokata ennen varsinaista talletustapahtumaa:

const note = Note.build(req.body)
note.important = trueawait note.save()

Esimerkkikoodin käyttötapaukseen metodi create sopii paremmin, joten pidättäydytään siinä.

Jos luotava olio ei ole validi, on seurauksena virheilmoitus. Esim. yritettäessä luoda muistiinpanoa ilman sisältöä operaatio epäonnistuu, ja konsoli paljastaa syyn olevan SequelizeValidationError: notNull Violation Note.content cannot be null

(node:39109) UnhandledPromiseRejectionWarning: SequelizeValidationError: notNull Violation: Note.content cannot be null
    at InstanceValidator._validate (/Users/mluukkai/opetus/fs-psql/node_modules/sequelize/lib/instance-validator.js:78:13)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

Lisätään uuden muistiinpanon lisäämisen yhteyteen vielä yksinkertainen virheenkäsittely:

app.post('/api/notes', async (req, res) => {
  try {
    const note = await Note.create(req.body)
    return res.json(note)
  } catch(error) {
    return res.status(400).json({ error })
  }
})

Tietokantataulujen automaattinen luominen

Sovelluksessamme on nyt yksi ikävä puoli, se olettaa että täsmälleen oikean skeeman omaava tietokanta on olemassa, eli että taulu notes on luotu sopivalla create table ‑komennolla.

Koska ohjelmakoodi säilytetään GitHubissa, olisi järkevää säilyttää myös tietokannan luovat komennot ohjelmakoodin yhteydessä, jotta tietokannan skeema on varmasti sama mitä ohjelmakoodi odottaa. Sequelize pystyy itse asiassa generoimaan skeeman automaattisesti modelien määritelmästä modelien metodin sync avulla.

Tuhotaan nyt tietokanta konsolista käsin antamalla psql-konsolissa seuraava komento:

drop table notes;

Komento \d paljastaa että taulu on hävinnyt tietokannasta:

postgres=# \d
Did not find any relations.

Sovellus ei enää toimi.

Lisätään sovellukseen seuraava komento heti modelin Note määrittelyn jälkeen:

Note.sync()

Kun sovellus käynnistyy, tulostuu konsoliin seuraava:

Executing (default): CREATE TABLE IF NOT EXISTS "notes" ("id"  SERIAL , "content" TEXT NOT NULL, "important" BOOLEAN, "date" TIMESTAMP WITH TIME ZONE, PRIMARY KEY ("id"));

Eli sovelluksen käynnistyessä suoritetaan komento CREATE TABLE IF NOT EXISTS "notes"..., joka luo taulun notes, jos se ei ole jo olemassa.

Muut operaatiot

Täydennetään sovellusta vielä muutamalla operaatiolla.

Yksittäisen muistiinpanon etsiminen onnistuu metodilla findByPk, koska se haetaan pääavaimena toimivan id:n perusteella:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    res.json(note)
  } else {
    res.status(404).end()
  }
})

Yksittäisen muistiinpanon hakeminen aiheuttaa seuraavanlaisen SQL-komennon:

Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note" WHERE "note"."id" = '1';

Jos muistiinpanoa ei löydy, palauttaa operaatio null, ja tässä tapauksessa annetaan asiaan kuuluva statuskoodi.

Muistiinpanon muuttaminen tapahtuu seuraavasti. Tuetaan ainoastaan kentän important muutosta, sillä sovelluksen frontend ei muuta tarvitse:

app.put('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    note.important = req.body.important
    await note.save()
    res.json(note)
  } else {
    res.status(404).end()
  }
})

Tietokantariviä vastaava olio haetaan kannasta findByPk-metodilla, olioon tehdään muutos ja lopputulos tallennetaan kutsumalla tietokantariviä vastaavan olion metodia save.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan GitHubissa, branchissa part13-1.

Sequelizen palauttamien olioiden tulostaminen konsoliin

JavaScript-ohjelmoijan tärkein apuväline on console.log, jonka aggressiivinen käyttö saa pahimmatkin bugit kuriin. Lisätään yksittäisen muistiinpanon reittiin konsolitulostus:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    console.log(note)    res.json(note)
  } else {
    res.status(404).end()
  }
})

Huomaamme, että lopputulos ei ole ihan se mitä odotimme:

note {
  dataValues: {
    id: 1,
    content: 'MongoDB is webscale',
    important: false,
    date: 2021-10-03T15:00:24.582Z,
  },
  _previousDataValues: {
    id: 1,
    content: 'MongoDB is webscale',
    important: false,
    date: 2021-10-03T15:00:24.582Z,
  },
  _changed: Set(0) {},
  _options: {
    isNewRecord: false,
    _schema: null,
    _schemaDelimiter: '',
    raw: true,
    attributes: [ 'id', 'content', 'important', 'date' ]
  },
  isNewRecord: false
}

Muistiinpanon tietojen lisäksi konsoliin tulostuu kaikenlaista muutakin. Pääsemme toivottuun lopputulokseen kutsumalla model-olion metodia toJSON:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    console.log(note.toJSON())    res.json(note)
  } else {
    res.status(404).end()
  }
})

Nyt lopputulos on juuri se mitä haluamme.

{ id: 1,
  content: 'MongoDB is webscale',
  important: false,
  date: 2021-10-09T13:52:58.693Z }

Jos kyse on kokoelmallisesta oliosta, ei metodi toJSON toimi suoraan, metodia on kutsuttava erikseen jokaiselle kokoelman oliolle:

router.get('/', async (req, res) => {
  const notes = await Note.findAll()

  console.log(notes.map(n=>n.toJSON()))
  res.json(notes)
})

Tulostus näyttää seuraavalta:

[ { id: 1,
    content: 'MongoDB is webscale',
    important: false,
    date: 2021-10-09T13:52:58.693Z },
  { id: 2,
    content: 'Relational databases rule the world',
    important: true,
    date: 2021-10-09T13:53:10.710Z } ]

Ehkä parempi ratkaisu on kuitenkin muuttaa kokoelma JSON:iksi tulostamista varten metodilla JSON.stringify:

router.get('/', async (req, res) => {
  const notes = await Note.findAll()

  console.log(JSON.stringify(notes))
  res.json(notes)
})

Tämä tapa on parempi erityisesti, jos kokoelman oliot sisältävät muita olioita. Usein on myös hyödyllistä muotoilla oliot ruudulle sisennetysti lukijaystävällisempään muotoon. Tämä onnistuu komennolla:

console.log(JSON.stringify(notes, null, 2))

Tulostus seuraavassa:

[
  {
    "id": 1,
    "content": "MongoDB is webscale",
    "important": false,
    "date": "2021-10-09T13:52:58.693Z"
  },
  {
    "id": 2,
    "content": "Relational databases rule the world",
    "important": true,
    "date": "2021-10-09T13:53:10.710Z"
  }
]