Databeheer met Knex & Objection

Databeheer met Knex.js & Objection.js

INTRODUCTIE

Objection.js is an ORM (Object Relational Mapper) for Node.js, built on knex that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.

You get all the benefits of an SQL query builder but also a powerful set of tools for working with relations.

https://vincit.github.io/objection.js/

Installatie

Installeer knex

npm install knex

Installeer sqlite3

npm install sqlite3

Installeer objection

npm install objection

Installeer dotenv

npm install dotenv

Voorbereiding (1/5)

  • Maak een bestand knexfile.js aan in de root directory van je applicatie (zelfde niveau als .env, package.json, ...)
/*
  This file is used to configure the database connection for the Knex library.
  Depending on the environment, the configuration will be different.
  For now, we only have a development environment (also defined in .env file).
*/
const config = {};

export default config;
import dotenv from "dotenv";
dotenv.config();

const dbName = process.env.DATABASE_NAME || "database.sqlite3";

const config = {
  development: {
    client: "sqlite3",
    connection: {
      filename: `./${dbName}`, // the file that will store the database
    },
    useNullAsDefault: true,
    migrations: {
      tableName: "knex_migrations", // table that will store the migration history
      directory: "./src/migrations", // location of the migration files
      stub: "./migration.stub", // this is the file that will be copied when creating a new migration
    },
    seeds: {
      directory: "./src/seeds", // location of the seed files (seeds = initial data for the database)
      stub: "./seed.stub", // this is the file that will be copied when creating a new seed
    },
  },
};

export default config;

knexfile.js                                                (1/5)

Voorbereiding (2/5)

  • Maak manueel twee stub-files, in de root-directory:
    • migration.stub
    • seed.stub

Een stub-file bevat de basiscode dat gebruikt wordt als we later migrations & seeds creëeren

 

💡 We configureren die zelf, mits we met ES6 imports & exports werken (in tegenstelling tot de originele CommonJS notatie)

const tableName = "TABLENAME";

export function up(knex) {
    return knex.schema.createTable(tableName, function (table) {
        
    });
}

export function down(knex) {
    return knex.schema.dropTable(tableName);
}

migration.stub                                        (2/5)

const tableName = "TABLENAME";

const seed = async function (knex) {
  // Deletes ALL existing entries
  await knex(tableName).truncate();
  await knex(tableName).insert([
    { title: "Home", slug: "home" },
    { title: "Work", slug: "work" },
    { title: "School", slug: "school" },
  ]);
};

export { seed };

seed.stub                                                  (2/5)

  • Maak in de src/lib-folder een Knex.js bestand aan. Hier komt de configuratie voor de database connectie.
import knex from "knex";
import knexConfig from "../../knexfile.js";

// get the environment from the .env file
const environment = process.env.NODE_ENV || "development";

// get the configuration for the environment
const config = knexConfig[environment];

// create the connection
const Knex = knex(config);

// export the connection
export default Knex;

Voorbereiding (3/5)

Voorbereiding (4/5)

  •  Let op: we gebruiken in bovenstaande code variabelen uit een .env bestand 
PORT=3000
DATABASE_TYPE=sqlite
DATABASE_NAME=knex_demo.sqlite3
NODE_ENV=development

Voorbereiding (5/5)

  • Om data heen en weer, van en naar onze applicatie 
    • express.json() → Ontleedt JSON in req.body.
    • express.urlencoded({ extended: true })
      → Verwerkt HTML formulieren.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

Check...

  • Je bestandsstructuur zou er nu zo moeten uitzien

    (in het groen staan de nieuwe bestanden)

DBBrowser

DBBrowser

  • Met deze tool zal je het database-bestand kunnen openen en de data kunnen bekijken en wijzigen.

VSCode extension

  • Om tijdens de lessen webservices te consumeren, gebruiken we de vscode-extension Thunder Client.

Databeheer met Knex.js & Objection.js

Een variabele navigatie

An entity is an object that exists. It doesn't have to do anything; it just has to exist. In database administration, an entity can be a single thing, person, place, or object.

npx knex

  • Geef in je terminal npx knex in (en druk enter)
    • bekijk de beschikbare commando's

NPM ken je al...
The command npm is used to download JavaScript packages
from Node Package Manager.

NPX is...

npx is used to execute downloaded packages

> npx knex

Commands:
  init [options]                          Create a fresh knexfile.
  migrate:make [options] <name>           Create a named migration file.
  migrate:latest [options]                Run all migrations that have not yet been run.
  migrate:up [<name>]                     Run the next or the specified migration that has not yet been run.
  migrate:rollback [options]              Rollback the last batch of migrations performed.
  migrate:down [<name>]                   Undo the last or the specified migration that was already run.
  migrate:currentVersion                  View the current version for the migration.
  migrate:list|migrate:status             List all migrations files with status.
  migrate:unlock                          Forcibly unlocks the migrations lock table.
  seed:make [options] <name>              Create a named seed file.
  seed:run [options]                      Run seed files.
  help [command]                  display help for command

Migration

Creëer een nieuwe migration, via volgend commando in bash

Migrations are like version control for your database, allowing your team to modify and share the application's database schema. It's a set of instructions that define the changes you want to make to your database schema

npx knex migrate:make create_navigation_items_table

Er komt een nieuw bestand in de src/migrations-folder

Migration

const tableName = "navigation_items";

export function up(knex) {
  return knex.schema.createTable(tableName, function (table) {
    table.increments("id").primary();
    table.string("label").notNullable();
    table.string("target");
    table.string("url").notNullable();
  });
}

export function down(knex) {
  return knex.schema.dropTable(tableName);
}

Migration uitvoeren

Voer de migration uit, via volgend commando

npx knex migrate:latest

#--------------------------------
#Using environment: development
#Batch 1 run: 1 migrations

Jouw database is nu geïnitialiseerd, met een tabel navigation_items

(en 3 andere tabellen, die je voorlopig mag negeren)

Model

  • Maak een nieuwe entity aan in de models folder: NavigationItem.js
  • Je NavigationItem bevat een id, een url, een target en label
  • Objection zorgt voor een ORM query builder, gekoppeld aan
    • De tabel (navigation_items)
    • Eén of meerdere velden (url, target & label)
    • Eén uniek veld dat auto incrementeel is,
      de primary key (het veld id)
    • Al dan niet een tussentabel voor relations

NavigationItem

import knex from "../lib/Knex.js";
import { Model } from "objection";

// instantiate the model
Model.knex(knex);

// define the NavigationItem model
class NavigationItem extends Model {
  static get tableName() {
    return "navigation_items";
  }

  static get idColumn() {
    return "id";
  }

  static get jsonSchema() {
    return {
      type: "object",
      required: ["label", "url"],
      properties: {
        id: { type: "integer" },
        label: { type: "string", minLength: 1, maxLength: 255 },
        target: { type: "string", maxLength: 255 },
        url: { type: "string", minLength: 1, maxLength: 255 },
      },
    };
  }
}

export default NavigationItem;

NavigationItem

  • Dankzij een model kan je gebruik maken van een ORM query builder om eenvoudig queries te beschrijven.
  • Op een SQL database voer je queries uit om CRUD-acties uit te voeren.
    • Create, Read, Update & Delete

NavigationItem

Voorbeeld van een SQL-syntax:

SELECT navigation_items.id, navigation_items.url, navigation_items.text
FROM navigation_items
WHERE navigation_items.url="/about-us"

Wij zullen geen SQL schrijven tijdens de lessen,
achterliggend zal Knex jouw verzoeken omzetten naar SQL 
zodat de dev-complexiteit van onze applicatie aanzienlijk lager ligt.

NavigationItem

  • We voegen via de SQL Browser wat data toe:
    1. Open de database
    2. Rechterklik op navigation_items en blader door de gegevens
    3. Vul de velden url en label in
      • url: https://www.arteveldehs.be
      • text: Arteveldehogeschool
    4. Bewaar de wijzigingen

NavigationItem

  • We halen de wijzigingen op in de PagesController
    1. Importeer de NavigationItem entity
       
    2. Haal alle data op uit de database
      • Het resultaat is een Promise, dus je wacht tot de database is opgehaald (async - await)
import NavigationItem from "../models/NavigationItem.js";
const menuItems = await NavigationItem.query();

NavigationItem

  • Vervang de hardcoded menuItems met die uit de tabel.
  • Hieronder een codesnippet van de PagesController
import userData from "../data/user.js";
import NavigationItem from "../models/NavigationItem.js";

export const home = async (req, res) => {

  const menuItems = await NavigationItem.query();
  
  const pageData = {
    title: "Home",
    content: `
      <p>Welcome to our website. We are a small company that does great things!</p>
      <p>Feel free to browse our site and learn more about us.</p>
    `,
  };

  res.render("home", {
    ...pageData,
    userData,
    menuItems,
  });
};

Databeheer met Knex.js & Objection.js

OEFENINGEN (ophalen data)

👨🏾‍💻 Oefening 1 - User

  • Maak "Ada Lovelace" op de homepage dynamisch

    • Maak een users-tabel aan via een migration,

    •  

    • Beschrijf deze velden

      • id (auto increment), firstname, lastname, bio

    • Voeg drie users toe aan die tabel. Gebruik daarvoor  
      DB Browser for SQLite (vergeet niet om je wijzigingen te bewaren, anders blijft de database in "lock"-modus)

    • Maak een bijhorende model aan: "src/models/User.js"

  • Render de gegevens van user met id: 1, als je de home bezoekt

    • tip: via: .findById(1)

npx knex migrate:make create_users_table

👩🏼‍💻 Oefening 2 - Page

  • Maak de overige data van de pagina's dynamisch
    (home, about & contact)

    • Maak daarvoor een pages-tabel aan via een migration

    • Beschrijf daar deze velden

      • id, title, slug, content, is_homepage (boolean)

    • Voer de paginagegevens in de tabel in (met DB Browser)

    • Maak een bijhorend model aan: "src/models/Page.js"

  • Gebruik telkens de "slug" van de url, om het gepaste record uit de tabel te halen & te renderen.

    • tip, via:  .where('slug', '...')          zie: docs objection 

Dynamic routing 1/2

  • Door de kracht van een database met queries,
    kunnen we de code refactoren

  • Vereenvoudig de hardcoded page routes tot een dynamic route

// ---------------------- App routes ----------------------
// App routes for pages that will be rendered in the browser.
// These routes will return HTML data.

app.get("/", PageController.home);
// app.get("/about-us", PageController.about); // in commentaar
// app.get("/contact", PageController.contact); // in commentaar
app.get("/:slug", PageController.page);
  • De volgorde is belangrijk! Indien bovenstaande routes zouden gewisseld zijn van volgorde, zou je nooit de home kunnen bereiken

  • Maak nu een dynamische action in de PageController
    zie volgende slide ⬇️

Dynamic routing 2/2

  • We halen slug uit de request-parameters via req.params.slug

    • Indien er GEEN pagina-data is ( pageData == undefined ),
      retourneren we een 404

    • Indien er WEL pagina-data is, renderen we de pagina,
      via een default template

  • Voeg toe in PageController

export const page = async (req, res) => {
  const menuItems = await NavigationItem.query();
  const pageData = await Page.query().findOne({
    slug: req.params.slug,
  });

  // If the page does not exist, render a 404 page.
  if (!pageData) {
    res.status(404).send("Page not found.");
    return;
  }

  // Render the page with the necessary data.
  res.render("pages/default", {
    ...pageData,
    menuItems,
  });
};

Databeheer met Knex.js & Objection.js

HTTP Methods

The primary or most-commonly-used HTTP verbs (or methods, as they are properly called) are POST, GET, PUT, and DELETE. These correspond to create, read, update, and delete (or CRUD) operations, respectively.

HTTP Methods

  • Met HTTP methods sturen we een verzoek van client naar server
    • GET - Het opvragen van data
    • POST - Het sturen van data van client → server
    • PUT - Het volledig aanpassen/vervangen van data
    • PATCH - Het gedeeltelijk aanpassen van data
    • DELETE - Het verwijderen van data
  • 📌 Verschil tussen PUT en PATCH:

    • PUT overschrijft een hele resource (alle velden vereist).
    • PATCH wijzigt alleen specifieke velden zonder de rest te beïnvloeden.

Een overzicht van andere HTTP methods kan je hier vinden.

Databeheer met Knex.js & Objection.js

CRUD via REST API

checkpoint-api (a fresh start)

1. Repo (opnieuw) klonen en van branch wisselen

git clone https://github.com/pgmgent-pgm-3/knex-demo
cd knex-demo
git checkout checkpoint-api
# windows CMD
copy .env-defaults .env

# windows powershell
Copy-Item -Path .env-defaults -Destination .env 

# mac
cp .env-defaults .env 

2. .env maken (manueel, of via CLI)

branch

checkpoint-api (a fresh start)

npm install

3. Dependencies installeren

npm run db:up 
npm run db:seed

4. Migreren en seeden (m.b.v. extra                   in package.json)

branch

Verplaats de seeds naar de /seeds/disabled folder

checkpoint-api (a fresh start)

Nu de seeds uitgevoerd zijn, verplaatsen we ze, zodat ze niet opnieuw worden uitgevoerd bij een eventuele volgende seeding.

branch
npm run start:dev

Start the application

checkpoint-api (a fresh start)

branch

Een REST API (Representational State Transfer Application Program Interface) is een architectuurstijl waarmee software kan communiceren met andere software via een netwerk of op hetzelfde apparaat. Doorgaans gebruiken ontwikkelaars REST-API's om webservices te bouwen.

REST API

  • Wij maken een webservice en communiceren via
    HTTP-methods om CRUD-acties uit te voeren.
  • We kunnen CRUD-bewerkingen uitvoeren om
    • Interesses op te halen (GET)
    • Interesses toe te voegen (POST)
    • Interesses te verwijderen (DELETE)
    • Interesses aan te passen (PUT) 

Interest Entity (maak deze)

import knex from "../lib/Knex.js";
import { Model } from "objection";

// instantiate the model
Model.knex(knex);

class Interest extends Model {
  static get tableName() {
    return "interests";
  }

  static get idColumn() {
    return "id";
  }

  static get jsonSchema() {
    return {
      type: "object",
      required: ["name"],
      properties: {
        id: { type: "integer" },
        name: { type: "string", minLength: 1, maxLength: 255 },
      },
    };
  }
}

export default Interest;

API Controller

Method URI Action Route Name
GET /api/interests index interests.index
POST /api/interests store interests.store
GET /api/interests/{id} show interests.show
PUT /api/interests/{id} update interests.update
PATCH /api/interests/{id} update interests.update
DELETE /api/interests/{id} destroy interests.destroy
  • PUT vs PATCH:
    • PUT wordt gebruikt voor een volledige update (alle velden verplicht).
    • PATCH wordt gebruikt voor een gedeeltelijke update (enkele velden kunnen worden aangepast).
  • Consistente RESTful structuur, zoals gebruikt in moderne API's.

API Controller

  • Om de REST-api te kunnen aanspreken,
    maken we nieuwe controllers aan. 
  • Maak een folder /api/ in de controllers/ folder aan
  • Maak in de api folder een bestand InterestController.js
  • Exporteer 5 "lege" functies:
    1. index
    2. show
    3. update
    4. destroy
    5. store

API Controller

/**
 * Interest API Controller
 */

export const show = async (req, res, next) => {};
export const index = async (req, res, next) => {};
export const store = async (req, res, next) => {};
export const update = async (req, res, next) => {};
export const destroy = async (req, res, next) => {};

API Routes

  • Maak nieuwe routes in je app.js bestand aan. In een REST-api noemen we dit endpoints. Ofwel, de URL die we gebruiken om een HTTP-method uit te voeren.
app.get("/api/interest", API_InterestController.index);
app.get("/api/interest/:id", API_InterestController.show);
app.post("/api/interest", API_InterestController.store);
app.put("/api/interest/:id", API_InterestController.update);
app.delete("/api/interest/:id", API_InterestController.destroy);

API Routes

  • Vergeet niet om de functies die je aanspreekt te importeren uit de api controller in app.js
import * as API_InterestController from "./controllers/api/InterestController.js";

GET /api/interest   ➡️   index()

  • Om data op te halen gebruiken we
    • query() - om alle records op te vragen

       
  • Zoekcriteria geven we mee via de optionele .where() methode. 
  • 💡 je kan meerdere .where()'s combineren door te chainen

 

const interests = await Interest.query();
const interestsStartingWithLetterB = await Interest.query()
	.where('name', 'like', 'B%');
const interests = await Interest.query();
 meerdere records ophalen

GET /api/interest/:id   ➡️   show()

  • findById() - om één record op te vragen die overeenkomt met id

     
  • findOne() - om één record op te vragen die overeenkomt met zoekcriteria, dat we meegeven via een object

 

const interest = await Interest.query().findOne({ 
  name: req.params.name 
});
const interest = await Interest.query().findById(7);
 één record ophalen

POST  /api/interest   ➡️   store()

  • Om data toe te voegen aan de tabel,
    gebruiken we de functie insert()
// insert the interests
const insertedInterest = await Interest.query().insert(req.body);
  • Van deze functie krijgen we de nieuw ingevoegde
    data terug.
 één record bewaren
  • Om data aan te passen en te bewaren,
    gebruiken we de methode .patch().
// check if the name already exists
const interest = await Interest.query().findById(req.params.id).patch(req.body);

PUT  /api/interest/:id   ➡️   update()

await Interest.query().patchAndFetchById(req.params.id, req.body);
  • Of korter met .patchAndFetchById()
 één record wijzigen
  • Om data te verwijderen, gebruiken we de functie deleteById()
await Interest.query().deleteById(id);
  • 💡 Er is geen body beschikbaar bij een delete HTTP method,
    dus je stuurt ook hier de id mee via de parameters van je endpoint
const id = req.params.id;
// OF
const { id } = req.params; 
// zelfde maar via object destructuring

DELETE  /api/interest/:id   ➡️   destroy()

één record wissen

👩🏼‍💻 Oefening

  • We hebben reeds een User entity met
    • id: de primary key van een user (uniek en auto-increment)
    • firstname: de voornaam van een gebruiker 
    • lastname: de familienaam van een gebruiker
    • bio: de biografie
  • Je voorziet voor je API volgende endpoints:
    • GET      /api/user        -   index()
    • GET      /api/user/:id    -   show()
    • POST     /api/user        -   store()
    • PUT      /api/user/:id    -   update()
      
    • DELETE   /api/user/:id    -   destroy()

Databeheer met Knex.js & Objection.js

RELATIONS - one-to-one

In relational databases, a relationship exists between two tables when one of them has a foreign key that references the primary key of the other table. This single fact allows relational databases to split and store data in different tables, yet still link the disparate data items together.

One-To-One

  • In situaties waar er veel extra (meta) informatie is van bepaalde records, kan het interessant zijn om data af te zonderen in een aparte tabel.
  • Met een one-to-one relatie kan je ze met elkaar verbinden.

It’s a relationship where a record in one entity (table) is associated with exactly one record in another entity (table).

One-To-One

  • Voorbeelden:

    • countries - capitals
      (elk land heeft slechts en max 1 hoofdstad)

    • people - fingerprints
      (een fingerprint is uniek per persoon)
    • users - user_preferences
      (persoonlijke voorkeuren zijn gebonden aan één user)
    • users - user_meta
      (metadata over gebruiker zoals adres, gender, motto, ....)

One-To-One

One-To-One

  • Start vanaf branch checkpoint-relations
git remote add upstream https://github.com/pgmgent-pgm-3/knex-demo.git
git fetch upstream
git checkout checkpoint-relations
npm install
  • Maak een nieuwe file .env in de root directory
  • Kopieer de inhoud van .env-defaults naar .env
  • Voer de migraties uit + seed de database
npm run db:up
npm run db:seed

One-To-One

  • Maak een nieuwe migratie voor de user_meta tabel met npx knex migrate:make en beschrijf de migratie 
const tableName = "user_meta";

export function up(knex) {
  return knex.schema.createTable(tableName, function (table) {
    table.increments("id").primary();
    table.integer("user_id").notNullable();
    table.string("quote", 255);
    table.string("location", 255);
    // declare foreign key
    table.foreign("user_id").references("users.id");
  });
}

export function down(knex) {
  return knex.schema.dropTable(tableName);
}

One-To-One

One-To-One

Entity: UserMeta

import knex from "../lib/Knex.js";
import { Model } from "objection";

// instantiate the model
Model.knex(knex);

// related models
import User from "./User.js";

// define the UserMeta model
class UserMeta extends Model {
  // ... (tableName, idColumn, jsonScheme)

  static get relationMappings() {
    return {
      user: {
        relation: Model.BelongsToOneRelation,
        modelClass: User,
        join: {
          from: "user_meta.user_id",
          to: "users.id",
        },
      },
    };
  }
}

export default UserMeta;

One-To-One

  • Met relationMapping geven we aan hoe de relatie zit tussen de tabellen.
    • relation: soort relatie (Model.BelongsToOneRelation)
    • modelClass: gelinkt Model (User)
    • join: de velden die de relatie beschrijven via from & to

Entity: UserMeta

One-To-One

  • De relatie beschrijven we ook in de user-entity
static get relationMappings() {
  return {
    meta: {
      relation: Model.HasOneRelation,
      modelClass: UserMeta,
      join: {
        from: "users.id",
        to: "user_meta.user_id",
      },
    }
  };
}

Entity: User

One-To-One

  • We kunnen dankzij Objection gerelateerde data mee ophalen met withGraphFetched()
const userData = await User
	.query()
	.findById(1)
	.withGraphFetched("meta");

One-To-One

  • Voeg data toe aan de user_meta tabel
  • Toon in jouw applicatie de metadata op de homepage voor de user met id 1:
    • Favoriete quote
    • Woonplaats

Oefening

One-To-One

  • Gerelateerde data bewaren kan dankzij Objection ook eenvoudig met insertGraph()
const insertedUser = await User.query().insertGraph({
  firstname: "John",
  lastname: "Doe",
  meta: {
    quote: "One day, I'm gonna make the onions cry",
    location: "Oljst.",
  },
});	

Databeheer met Knex.js & Objection.js

RELATIONS - one-to-many / many-to-one

One-To-Many / Many-To-One

  • Voorbeelden
    • Een automerk kan meerdere modellen hebben, maar een model kan slechts aan één merk toebehoren
    • Een bol.com klant kan meerdere bestellingen doen, maar een bestelling kan slechts aan één klant toegewezen zijn
    • Een persoon kan meerdere huisdieren hebben, maar een huisdier kan slechts één baasje hebben

One record in a table can be associated with one or more records in another table.

One-To-Many / Many-To-One

One-To-Many/Many-To-One

  • In deze relatie zal entity A meerdere instanties bevatten van entity B.
  • Een gebruiker (User) kan meerdere huisdieren (Pet) bevatten.
  • Deze relatie moet ook aan beide kanten kenbaar worden gemaakt.

 One-To-May 

const tableName = "pets";

export function up(knex) {
  return knex.schema.createTable(tableName, function (table) {
    table.increments("id").primary();
    table.string("name").notNullable();
    table.string("species").notNullable();
    table.string("age").notNullable();
    table.integer("owner_id").notNullable();

    // Foreign Key
    table.foreign("owner_id").references("users.id");
  });
}

export function down(knex) {
  return knex.schema.dropTable(tableName);
}

One-To-Many/Many-To-One

  • Maak een nieuwe migratie voor de pets tabel met
    npx knex migrate:make en beschrijf de migratie 

One-To-Many/Many-To-One

  • Maak een Pet entity
// imports & config
class Pet extends Model {
  static get tableName() {
    return "pets";
  }

  static get idColumn() {
    return "id";
  }

  static get jsonSchema() {
    return {
      type: "object",
      required: ["name", "species", "age"],
      properties: {
        id: { type: "integer" },
        name: { type: "string", minLength: 1, maxLength: 255 },
        species: { type: "string", minLength: 1, maxLength: 255 },
        age: { type: "integer" },
        owner_id: { type: "integer" },
      },
    };
  }

  static get relationMappings() {
    return {},
  }
}

export default Pet;

One-To-Many/Many-To-One

  • Voeg aan de User verwijzing naar Pet toe.
    Er is één gebruiker en meerdere huisdieren
    • one-to-many
[...]
static get relationMappings() {
    return {
      meta: {
        relation: Model.HasOneRelation,
        modelClass: UserMeta,
        join: {
          from: "users.id",
          to: "user_meta.user_id",
        },
      },
      pets: {
        relation: Model.HasManyRelation,
        modelClass: Pet,
        join: {
          from: "users.id",
          to: "pets.owner_id",
        },
      },
    };
}
[...]

One-To-Many/Many-To-One

  • Voeg aan Pet een verwijzing naar User toe. Er zijn meerdere huisdieren met één gebruiker (eigenaar)  
    • many-to-one
[...]
static get relationMappings() {
    return {
      owner: {
        relation: Model.BelongsToOneRelation,
        modelClass: User,
        join: {
          from: "pets.owner_id",
          to: "users.id",
        },
      },
    };
}
[...]

One-To-Many/Many-To-One

  • Nu kunnen we een zoekopdracht uitvoeren van beide kanten.
const users = await User.query().withGraphFetched("[meta, pets]");
// &
const pets = await Pet.query().withGraphFetched("owner");

One-To-Many/Many-To-One

  • Ook bewaren is nu kinderspel
 const insertedUser = await User.query().insertGraph({
   firstname: "Jane",
   lastname: "Doe",
   pets: [
     { name: "zorro", species: "cat", age: 3}, 
     { name: "kung fu", species: "panda", age: 10 }, 
   ],
   meta: {
     quote: "According to my cat, everything belongs to the cat",
     location: "Kittycat boat.",
   },
 });

Databeheer met Knex.js & Objection.js

RELATIONS - many-to-many

Many-To-Many

  • Voorbeelden
    • Gebruikers kunnen meerdere interesses hebben en interesses kunnen toebehoren aan meerdere gebruikers.
    • Canvas-cursussen kunnen meerdere studenten bevatten en studenten kunnen toegewezen zijn aan meerdere Canvas-cursussen
    • Blog posts kunnen meerdere tags hebben en tags kunnen van toepassing zijn op meerdere blog posts

A many-to-many relationship occurs when multiple records in a table are associated with multiple records in another table. 

Many-To-Many

  • Meerdere referenties kunnen meerdere keren voorkomen... OFWEL meerdere primary keys verwijzen naar meerdere referenties in een andere tabel.
  • Meerdere users kunnen meerdere interesses hebben
    en...
    een bepaalde interesse kan voor meerdere users gelden

Many-To-Many

  • Stel we hebben 3 users:
    • Martha, houdt van
      • Pilates
      • Boksen
      • Snoeiharde Techno
    • Chantal
      • Snoeiharde Techno
      • Judo
    •  
    • Philippe
      • Judo
      • Programmeren
      • Snoeiharde Techno

Many-To-Many

  • Dus... 3 users:
    • Martha
    • Chantal
    • Philippe
  • En... 5 interesses
    • Pilates
    • Boksen
    • Judo
    • Snoeiharde Techno
    • Programmeren

8 streepjes --> relaties

Many-To-Many

  • Dus... 3 users:
    1. Martha
    2. Chantal
    3. Philippe
  • En... 5 interesses
    1. Pilates
    2. Boksen
    3. Judo
    4. Snoeiharde Techno
    5. Programmeren

user_interest

1-1

1-2

1-4

2-3

2-4

3-3

3-4

3-5

... met een tussentabel

Many-To-Many

  • Een tussentabel wordt ook wel pivot table genoemd.
  • Vaak is de naamgeving enkelvoudig en alfabetisch (good practice)
    (letter i komt in alfabet voor letter u), dus de naam is interest_user

Many-To-Many

  • Beschrijf met Knex.js de migration voor de pivot tabel
const tableName = "interest_user";

export function up(knex) {
  return knex.schema.createTable(tableName, function (table) {
    table.increments("id").primary();
    table.integer("user_id").notNullable();
    table.integer("interest_id").notNullable();
    // declare foreign key
    table.foreign("user_id").references("users.id");
    table.foreign("interest_id").references("interests.id");
  });
}

export function down(knex) {
  return knex.schema.dropTable(tableName);
}

Many-To-Many

  • We passen de relation van User aan
static get relationMappings() {
    return {
      meta: {
        // ...
      },
      pets: {
        // ...
      },
      interests: {
        relation: Model.ManyToManyRelation,
        modelClass: Interest,
        join: {
          from: "users.id",
          through: {
            from: "interest_user.user_id",
            to: "interest_user.interest_id",
          },
          to: "interests.id",
        },
      },
    };
  }

Many-To-Many

  • We passen de relation van Interest aan
static get relationMappings() {
    return {
      users: {
        relation: Model.ManyToManyRelation,
        modelClass: User,
        join: {
          from: "interests.id",
          through: {
            from: "interest_user.interest_id",
            to: "interest_user.user_id",
          },
          to: "users.id",
        },
      },
    };
}

Many-To-Many

  • Voorbeeld van een nieuwe gebruiker met nieuwe interests die direct aan de user gekoppeld zijn



     
  • Nadeel, via deze query wordt niet gecontroleerd of de interest al bestaat, dus risico op dubbele data
const insertedUser = await User.query().insertGraph({
  firstname: "John",
  lastname: "Doe",
  interests: [{ name: "coding" }, { name: "reading" }, { name: "hiking" }],
  meta: {
    quote: "Everywhere you go, I'll be watching you",
    location: "Behind you",
  },
});

Many-To-Many

  • Indien de interest al bestaat, synchroniseer, anders, maak een aan via  { relate: true}
     
const user = await User.query().insertGraph(
    [
      {
        firstname: "Jane",
        lastname: "Doe",
        interests: [
          {
            id: 29, // existing interest
          },
          {
            name: "music", // also interest that already exists
          },
          {
            name: "new interest", // new one
          },
        ],
      },
    ],
    { relate: true }
  );

Many-To-Many

  • Willen we nu de relaties ook ophalen met find(), dan moeten we dit ook hier expliciet meegeven. Bijv. getUsers()
 const users = await User.query().withGraphFetched(
      "[interests, meta, pets]"
 );

Many-To-Many

  • Maak een nieuwe tabel aan courses (opleidingen)
  • Elke course heeft minstens een naam en een aantal studiepunten
  • Zorg voor een many-to-many relatie met users
  • Toon op je homepagina ook de opleidingen die hij/zij (= user met id 1) volgt onder een aparte heading

Oefening

Databeheer met Knex.js & Objection.js

dotenv fix voor "Knex"

Waarom kreeg de database niet de naam "knex_demo.sqlite3"?

"Sinds Node v20.6.0 is de library dotenv niet meer nodig"

...

 

maar voor knex.js voorlopig wel

.env wordt anders genegeerd

          

sorry <--------------

/fix met dotenv

Sinds Node v20.6.0 is dotenv niet langer nodig...

maar Knex heeft dit (voorlopig) wel nodig

Installeer dotenv

npm install dotenv

/fix met dotenv

import dotenv from "dotenv";
dotenv.config();

const dbName = process.env.DATABASE_NAME || "database.sqlite3";

// ...

Voeg deze regels toe, bovenaan knexfile.js

/fix met dotenv

# Comment this out to exclude .env
.env

# Database
knex-demo.sqlite3
knex_demo.sqlite3
database.sqlite3

Update .gitignore

/fix met dotenv

...
DATABASE_NAME=knex_demo.sqlite3
...

Pas .env aan

/fix met dotenv

npx knex migrate:latest

Run de migratie opnieuw

En verwijder database.sqlite3

--> prullenmand

Made with Slides.com