Databeheer met Knex.js & Objection.js
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.
Installeer knex
npm install knex
Installeer sqlite3
npm install sqlite3
Installeer objection
npm install objection
Installeer dotenv
npm install dotenv
/*
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;
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);
}
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 };
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;
PORT=3000
DATABASE_TYPE=sqlite
DATABASE_NAME=knex_demo.sqlite3
NODE_ENV=development
express.json()
→ Ontleedt JSON in req.body
.express.urlencoded({ extended: true })
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
Databeheer met Knex.js & Objection.js
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
in (en druk enter)
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
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
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);
}
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)
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;
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.
import NavigationItem from "../models/NavigationItem.js";
const menuItems = await NavigationItem.query();
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
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
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
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 ⬇️
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
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.
📌 Verschil tussen PUT en PATCH:
Een overzicht van andere HTTP methods kan je hier vinden.
Databeheer met Knex.js & Objection.js
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
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
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
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.
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;
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
wordt gebruikt voor een volledige update (alle velden verplicht).PATCH
wordt gebruikt voor een gedeeltelijke update (enkele velden kunnen worden aangepast).index
show
update
destroy
store
/**
* 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) => {};
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);
import * as API_InterestController from "./controllers/api/InterestController.js";
const interests = await Interest.query();
const interestsStartingWithLetterB = await Interest.query()
.where('name', 'like', 'B%');
const interests = await Interest.query();
meerdere records ophalen
const interest = await Interest.query().findOne({
name: req.params.name
});
const interest = await Interest.query().findById(7);
één record ophalen
// insert the interests
const insertedInterest = await Interest.query().insert(req.body);
één record bewaren
// check if the name already exists
const interest = await Interest.query().findById(req.params.id).patch(req.body);
await Interest.query().patchAndFetchById(req.params.id, req.body);
één record wijzigen
await Interest.query().deleteById(id);
const id = req.params.id;
// OF
const { id } = req.params;
// zelfde maar via object destructuring
één record wissen
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
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.
It’s a relationship where a record in one entity (table) is associated with exactly one record in another entity (table).
Voorbeelden:
countries - capitals
(elk land heeft slechts en max 1 hoofdstad)
git remote add upstream https://github.com/pgmgent-pgm-3/knex-demo.git
git fetch upstream
git checkout checkpoint-relations
npm install
npm run db:up
npm run db:seed
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);
}
relationMappings.
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;
static get relationMappings() {
return {
meta: {
relation: Model.HasOneRelation,
modelClass: UserMeta,
join: {
from: "users.id",
to: "user_meta.user_id",
},
}
};
}
const userData = await User
.query()
.findById(1)
.withGraphFetched("meta");
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
One record in a table can be associated with one or more records in another table.
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);
}
// 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;
[...]
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",
},
},
};
}
[...]
[...]
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: "pets.owner_id",
to: "users.id",
},
},
};
}
[...]
const users = await User.query().withGraphFetched("[meta, pets]");
// &
const pets = await Pet.query().withGraphFetched("owner");
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
A many-to-many relationship occurs when multiple records in a table are associated with multiple records in another table.
8 streepjes --> relaties
user_interest
1-1
1-2
1-4
2-3
2-4
3-3
3-4
3-5
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);
}
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",
},
},
};
}
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",
},
},
};
}
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",
},
});
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 }
);
const users = await User.query().withGraphFetched(
"[interests, meta, pets]"
);
Databeheer met Knex.js & Objection.js
"Sinds Node v20.6.0 is de library dotenv niet meer nodig"
...
maar voor knex.js voorlopig wel
.env wordt anders genegeerd
sorry <--------------
Sinds Node v20.6.0 is dotenv niet langer nodig...
maar Knex heeft dit (voorlopig) wel nodig
Installeer dotenv
npm install dotenv
import dotenv from "dotenv";
dotenv.config();
const dbName = process.env.DATABASE_NAME || "database.sqlite3";
// ...
Voeg deze regels toe, bovenaan knexfile.js
# Comment this out to exclude .env
.env
# Database
knex-demo.sqlite3
knex_demo.sqlite3
database.sqlite3
Update .gitignore
...
DATABASE_NAME=knex_demo.sqlite3
...
Pas .env aan
npx knex migrate:latest
Run de migratie opnieuw
En verwijder database.sqlite3
--> prullenmand