

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.

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 eenKnex.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 inreq.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
- Download de DBBrowser for SQLite via deze website.


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
Vul de nodige velden aan (ref: https://knexjs.org/guide/schema-builder.html#schema-building)
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:
- Open de database
- Rechterklik op navigation_items en blader door de gegevens
- Vul de velden url en label in
- url: https://www.arteveldehs.be
- text: Arteveldehogeschool
- Bewaar de wijzigingen

NavigationItem
- We halen de wijzigingen op in de PagesController
- Importeer de NavigationItem entity
- Haal alle data op uit de database
- Het resultaat is een Promise, dus je wacht tot de database is opgehaald (async - await)
- Importeer de NavigationItem entity
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:
-
index
-
show
-
update
-
destroy
-
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
-
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
- Maak een UserMeta model met daarin extra informatie over een user: quote, location.
- We maken de relatie in het model langs beide (twee) kanten kenbaar, dat betekent dus zowel in User als in UserMeta.
- Dit gebeurt via
relationMappings.
- https://vincit.github.io/objection.js/guide/relations.html#examples

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
-
Martha, houdt van
-
-
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:
- Martha
- Chantal
- Philippe
- En... 5 interesses
- Pilates
- Boksen
- Judo
- Snoeiharde Techno
- 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
PGM3/4 - Databeheer met Knex.js & Objection.js
By Frederick Roegiers
PGM3/4 - Databeheer met Knex.js & Objection.js
- 177