Eerste upload
@@ -0,0 +1,59 @@
|
|||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.localized
|
||||||
|
__MACOSX/
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon[
|
||||||
|
]
|
||||||
|
|
||||||
|
# Resource forks
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files and directories that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.com.apple.timemachine.supported
|
||||||
|
.PKInstallSandboxManager
|
||||||
|
.PKInstallSandboxManager-SystemSoftware
|
||||||
|
.hotfiles.btree
|
||||||
|
.vol
|
||||||
|
.file
|
||||||
|
.disk_label*
|
||||||
|
lost+found
|
||||||
|
.HFS+ Private Directory Data[
|
||||||
|
]
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Mac OS 6 to 9
|
||||||
|
Desktop DB
|
||||||
|
Desktop DF
|
||||||
|
TheFindByContentFolder
|
||||||
|
TheVolumeSettingsFolder
|
||||||
|
.FBCIndex
|
||||||
|
.FBCSemaphoreFile
|
||||||
|
.FBCLockFolder
|
||||||
|
|
||||||
|
# Quota system
|
||||||
|
.quota.group
|
||||||
|
.quota.user
|
||||||
|
.quota.ops.group
|
||||||
|
.quota.ops.user
|
||||||
|
|
||||||
|
# TimeMachine
|
||||||
|
Backups.backupdb
|
||||||
|
.MobileBackups
|
||||||
|
.MobileBackups.trash
|
||||||
|
MobileBackups.trash
|
||||||
|
tmbootpicker.efi
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
# OnlineAcademy Handover
|
||||||
|
|
||||||
|
## Doel
|
||||||
|
|
||||||
|
Automatiseren van het opbouwen van elearningmodules in `create.onlineacademy.nl` met Markdown als bron, maar bewust via de zichtbare editor-UI met Playwright in plaats van via backend write-API's.
|
||||||
|
|
||||||
|
De gebruiker wil meekijken tijdens de browserrun en de oplossing moet veilig blijven in een productieomgeving.
|
||||||
|
|
||||||
|
## Onderzoeksuitkomst
|
||||||
|
|
||||||
|
### Platformstructuur
|
||||||
|
|
||||||
|
Live bevestigd:
|
||||||
|
- links: hoofdstukken/pagina's
|
||||||
|
- midden: paginacanvas met contentblokken
|
||||||
|
- rechts: blokbibliotheek en instellingen
|
||||||
|
|
||||||
|
Belangrijke UI-ankers die zijn aangetroffen:
|
||||||
|
- `data-testid="editor-main"`
|
||||||
|
- `data-testid="page-button"`
|
||||||
|
- blokken rechts zoals:
|
||||||
|
- `drag-block-text`
|
||||||
|
- `drag-block-heading`
|
||||||
|
- `drag-block-quote`
|
||||||
|
- `drag-block-table`
|
||||||
|
- `drag-block-multiple-choice`
|
||||||
|
- `drag-block-multiple-response`
|
||||||
|
- `drag-block-open-question`
|
||||||
|
- `drag-block-matching-pairs`
|
||||||
|
|
||||||
|
### Bloktypes
|
||||||
|
|
||||||
|
In UI gezien:
|
||||||
|
- `text`
|
||||||
|
- `heading`
|
||||||
|
- `quote`
|
||||||
|
- `table`
|
||||||
|
- `image`
|
||||||
|
- `video`
|
||||||
|
- `audio`
|
||||||
|
- `downloads`
|
||||||
|
- `links`
|
||||||
|
- `expert`
|
||||||
|
- `typeform`
|
||||||
|
- `multiple-choice`
|
||||||
|
- `multiple-response`
|
||||||
|
- `open-question`
|
||||||
|
- `matching-pairs`
|
||||||
|
|
||||||
|
### Backend read-only bevindingen
|
||||||
|
|
||||||
|
Er is bewust alleen read-only gekeken naar de data die de editor zelf ophaalt.
|
||||||
|
|
||||||
|
Belangrijke GET endpoints:
|
||||||
|
- `GET /authoring/v1.0/contents/course/{contentId}`
|
||||||
|
- `GET /authoring/v1.0/contents/version?externalId=...`
|
||||||
|
|
||||||
|
De echte cursusinhoud zit in `jsonContent` en heeft grofweg deze vorm:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"title": "...",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"pageType": "number",
|
||||||
|
"title": "...",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"type": "text",
|
||||||
|
"data": { "...": "..." }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"examPage": {
|
||||||
|
"enabled": false,
|
||||||
|
"blocks": [],
|
||||||
|
"examSettings": {
|
||||||
|
"showFeedbackAfterExam": false,
|
||||||
|
"questionPool": false,
|
||||||
|
"numberOfQuestions": 0,
|
||||||
|
"minimumPassingScore": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Deze kennis is alleen gebruikt voor begrip en mapping, niet voor writes.
|
||||||
|
|
||||||
|
## Gekozen oplossingsrichting
|
||||||
|
|
||||||
|
Niet API-first.
|
||||||
|
|
||||||
|
Wel:
|
||||||
|
- Markdown parser
|
||||||
|
- intern trainingsmodel
|
||||||
|
- UI-plan
|
||||||
|
- zichtbare Playwright runner
|
||||||
|
- reviewstop vóór save
|
||||||
|
|
||||||
|
Reden:
|
||||||
|
- minder risico in productie
|
||||||
|
- platformvalidaties blijven actief
|
||||||
|
- gebruiker kan meekijken
|
||||||
|
- geen directe backend writes
|
||||||
|
|
||||||
|
## Huidige bestanden
|
||||||
|
|
||||||
|
### Input / referentie
|
||||||
|
|
||||||
|
- [create-md-format.md](/Users/nico/skillstown/create-md-format.md)
|
||||||
|
- [body.html](/Users/nico/skillstown/body.html)
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
- [inspect-onlineacademy.mjs](/Users/nico/skillstown/inspect-onlineacademy.mjs)
|
||||||
|
Read-only inspectie van login, editor, requests en payloads.
|
||||||
|
|
||||||
|
- [markdown-to-onlineacademy-json.mjs](/Users/nico/skillstown/markdown-to-onlineacademy-json.mjs)
|
||||||
|
Parser en dry-run converter van Markdown naar platform-compatibele `jsonContent`.
|
||||||
|
|
||||||
|
- [onlineacademy-playwright-runner.mjs](/Users/nico/skillstown/onlineacademy-playwright-runner.mjs)
|
||||||
|
Zichtbare Playwright runner met:
|
||||||
|
- reviewmodus
|
||||||
|
- execute-modus
|
||||||
|
- login
|
||||||
|
- editorverificatie
|
||||||
|
- pagina-aanmaak
|
||||||
|
- blokinsertie/invulling
|
||||||
|
- DOM-snapshot voor detectie van nieuw ingevoegde blokken
|
||||||
|
- drop-targeting op de onderkant van het laatste bestaande blok
|
||||||
|
- reviewstop vóór save
|
||||||
|
|
||||||
|
### Testdata
|
||||||
|
|
||||||
|
- [pilot-mvp.md](/Users/nico/skillstown/pilot-mvp.md)
|
||||||
|
- [pilot-multi-response.md](/Users/nico/skillstown/pilot-multi-response.md)
|
||||||
|
- [pilot-image.md](/Users/nico/skillstown/pilot-image.md)
|
||||||
|
- [pilot-table.md](/Users/nico/skillstown/pilot-table.md)
|
||||||
|
- [pilot-table-large.md](/Users/nico/skillstown/pilot-table-large.md)
|
||||||
|
- [pilot-open-question.md](/Users/nico/skillstown/pilot-open-question.md)
|
||||||
|
- [pilot-chapters.md](/Users/nico/skillstown/pilot-chapters.md)
|
||||||
|
- [pilot-chapters-large.md](/Users/nico/skillstown/pilot-chapters-large.md)
|
||||||
|
- [create-md-format.onlineacademy.json](/Users/nico/skillstown/create-md-format.onlineacademy.json)
|
||||||
|
- [pilot-mvp.onlineacademy.json](/Users/nico/skillstown/pilot-mvp.onlineacademy.json)
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
- [package.json](/Users/nico/skillstown/package.json)
|
||||||
|
|
||||||
|
## Wat werkt al
|
||||||
|
|
||||||
|
### Lokaal
|
||||||
|
|
||||||
|
- Markdown parsing werkt
|
||||||
|
- dry-run JSON generatie werkt
|
||||||
|
- plan/validatie-export werkt
|
||||||
|
|
||||||
|
### Live in de zichtbare browser
|
||||||
|
|
||||||
|
Bewezen:
|
||||||
|
- login werkt
|
||||||
|
- editorherkenning werkt
|
||||||
|
- reviewmodus zonder mutaties werkt
|
||||||
|
- nieuwe pagina `Intro` aanmaken werkt
|
||||||
|
- paginatitel invullen werkt
|
||||||
|
- drag/drop insertie van blokken werkt aantoonbaar stabieler wanneer op de onderkant van het laatste bestaande blok wordt gedropt
|
||||||
|
- execute-run in natuurlijke volgorde van boven naar beneden werkt nu voor:
|
||||||
|
- `text`
|
||||||
|
- `heading`
|
||||||
|
- `quote`
|
||||||
|
- `multiple-choice` blokinsertie werkt en de vraag wordt ingevuld
|
||||||
|
- `multiple-choice` werkt nu end-to-end voor de pilot:
|
||||||
|
- vraag invullen
|
||||||
|
- aantal antwoorden afstemmen op Markdown
|
||||||
|
- juiste antwoord selecteren
|
||||||
|
- feedback invullen
|
||||||
|
- `multiple-response` werkt nu end-to-end voor de pilot:
|
||||||
|
- vraag invullen
|
||||||
|
- aantal antwoorden afstemmen op Markdown
|
||||||
|
- alle checkboxen resetten
|
||||||
|
- meerdere correcte antwoorden selecteren
|
||||||
|
- feedback invullen
|
||||||
|
- `image` werkt nu voor de pilot:
|
||||||
|
- upload via lokaal bestandspad
|
||||||
|
- titel invullen
|
||||||
|
- onderschrift invullen
|
||||||
|
- alternatieve tekst invullen
|
||||||
|
- `table` werkt nu end-to-end voor de pilot:
|
||||||
|
- titel invullen
|
||||||
|
- `Top header` aan/uit zetten
|
||||||
|
- `Left header` aan/uit zetten
|
||||||
|
- aantal kolommen uitbreiden op basis van Markdown
|
||||||
|
- aantal rijen uitbreiden op basis van Markdown
|
||||||
|
- header- en bodycellen invullen
|
||||||
|
- `open-question` werkt nu end-to-end voor de pilot:
|
||||||
|
- vraag invullen
|
||||||
|
- toelichting invullen
|
||||||
|
- hoofdstukaanmaak werkt nu voor de pilot:
|
||||||
|
- de execute-flow maakt nu altijd een nieuw hoofdstuk aan via `Nieuw hoofdstuk`
|
||||||
|
- automatisch aangemaakte eerste pagina in nieuwe hoofdstukken hernoemen en hergebruiken
|
||||||
|
- extra pagina's per hoofdstuk aanmaken wanneer Markdown meer pagina's bevat
|
||||||
|
- er is niet opgeslagen
|
||||||
|
|
||||||
|
## Artifacts / bewijs
|
||||||
|
|
||||||
|
Belangrijkste artifacts:
|
||||||
|
|
||||||
|
- [editor-ready.png](/Users/nico/skillstown/artifacts/oa-runner/editor-ready.png)
|
||||||
|
Editor succesvol geladen.
|
||||||
|
|
||||||
|
- [page-1-structure.png](/Users/nico/skillstown/artifacts/oa-runner/page-1-structure.png)
|
||||||
|
Nieuwe pagina `Intro` succesvol aangemaakt.
|
||||||
|
|
||||||
|
- [page-1-block-1-text.png](/Users/nico/skillstown/artifacts/oa-runner/page-1-block-1-text.png)
|
||||||
|
Tekstblok zichtbaar ingevoegd en gevuld.
|
||||||
|
|
||||||
|
- [page-1-block-2-heading.png](/Users/nico/skillstown/artifacts/oa-runner/page-1-block-2-heading.png)
|
||||||
|
Oud artifact uit een eerdere run waarin blokvolgorde/targeting nog niet stabiel was.
|
||||||
|
|
||||||
|
- [page-1-block-2-quote.png](/Users/nico/skillstown/artifacts/oa-runner/page-1-block-2-quote.png)
|
||||||
|
Quoteblok succesvol ingevuld in een latere geharde run.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner/review-stop.png)
|
||||||
|
Laat de huidige reviewstop zien vóór save, met de blokken in de goede volgorde bovenaan zichtbaar.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner/validation.json)
|
||||||
|
|
||||||
|
Pilot na drag/drop-hardening:
|
||||||
|
|
||||||
|
- [page-1-block-3-quote.png](/Users/nico/skillstown/artifacts/oa-runner-pilot/page-1-block-3-quote.png)
|
||||||
|
Laat zien dat `text`, `heading` en `quote` in natuurlijke volgorde onder elkaar staan.
|
||||||
|
|
||||||
|
- [page-1-block-4-multiple-choice.png](/Users/nico/skillstown/artifacts/oa-runner-pilot/page-1-block-4-multiple-choice.png)
|
||||||
|
Laat zien dat daarna ook `multiple-choice` onder het quote-blok is ingevoegd en dat de vraag is ingevuld.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-pilot/review-stop.png)
|
||||||
|
Bevestigt de pagina-opbouw in volgorde `text` -> `heading` -> `quote` -> `multiple-choice`, zonder save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-pilot/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-pilot/validation.json)
|
||||||
|
|
||||||
|
Pilot voor `multiple-choice` afwerking:
|
||||||
|
|
||||||
|
- [page-1-block-4-multiple-choice.png](/Users/nico/skillstown/artifacts/oa-runner-mc/page-1-block-4-multiple-choice.png)
|
||||||
|
Laat zien dat de meerkeuzevraag nu met exact 2 antwoorden uit Markdown is ingevuld, met het correcte antwoord geselecteerd.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-mc/review-stop.png)
|
||||||
|
Laat zien dat ook de feedback/toelichting is ingevuld en dat de run weer stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-mc/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-mc/validation.json)
|
||||||
|
|
||||||
|
Pilot voor `multiple-response` afwerking:
|
||||||
|
|
||||||
|
- [page-1-block-1-multiple-response.png](/Users/nico/skillstown/artifacts/oa-runner-mr/page-1-block-1-multiple-response.png)
|
||||||
|
Laat zien dat de multi-antwoordvraag nu exact 3 antwoorden uit Markdown overneemt, met 2 correcte checkboxen geselecteerd.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-mr/review-stop.png)
|
||||||
|
Laat zien dat ook de toelichting is ingevuld en dat de run weer stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-mr/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-mr/validation.json)
|
||||||
|
|
||||||
|
Pilot voor `image` afwerking:
|
||||||
|
|
||||||
|
- [page-1-block-1-image.png](/Users/nico/skillstown/artifacts/oa-runner-image/page-1-block-1-image.png)
|
||||||
|
Laat zien dat de afbeelding is geüpload en dat `Titel`, `Onderschrift` en `Alternatieve tekst` zijn ingevuld.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-image/review-stop.png)
|
||||||
|
Laat zien dat de image-run ook weer stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-image/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-image/validation.json)
|
||||||
|
|
||||||
|
Pilot voor `table` verkenning:
|
||||||
|
|
||||||
|
- [page-1-block-1-table.png](/Users/nico/skillstown/artifacts/oa-runner-table/page-1-block-1-table.png)
|
||||||
|
Laat zien dat `Titel`, `Top header`, kolomuitbreiding, rijuitbreiding en de volledige celvulling nu werken voor de tabelpilot.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-table/review-stop.png)
|
||||||
|
Laat zien dat de tabel-run ook stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-table/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-table/validation.json)
|
||||||
|
- [failure-state.png](/Users/nico/skillstown/artifacts/oa-runner-table/failure-state.png)
|
||||||
|
Artifact uit een tussenrun waarin de plusknoppen voor kolom/rij-uitbreiding nog niet betrouwbaar werden geraakt.
|
||||||
|
|
||||||
|
Pilot voor grotere `table` validatie:
|
||||||
|
|
||||||
|
- [page-1-block-1-table.png](/Users/nico/skillstown/artifacts/oa-runner-table-large/page-1-block-1-table.png)
|
||||||
|
Laat zien dat de tabel ook opschaalt naar 4 kolommen en 10 regels totaal, inclusief volledige celvulling uit Markdown.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-table-large/review-stop.png)
|
||||||
|
Laat zien dat ook de grotere tabel-run stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-table-large/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-table-large/validation.json)
|
||||||
|
|
||||||
|
Pilot voor `open-question` afwerking:
|
||||||
|
|
||||||
|
- [page-1-block-1-open-question.png](/Users/nico/skillstown/artifacts/oa-runner-open-question/page-1-block-1-open-question.png)
|
||||||
|
Laat zien dat de open vraag en de toelichting correct uit Markdown zijn ingevuld.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-open-question/review-stop.png)
|
||||||
|
Laat zien dat ook de open-vraag-run stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-open-question/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-open-question/validation.json)
|
||||||
|
|
||||||
|
Pilot voor hoofdstukaanmaak:
|
||||||
|
|
||||||
|
- [chapter-2-structure.png](/Users/nico/skillstown/artifacts/oa-runner-chapters/chapter-2-structure.png)
|
||||||
|
Laat zien dat een tweede hoofdstuk wordt toegevoegd en dat de automatisch aangemaakte eerste pagina daarvan wordt hernoemd en gevuld.
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-chapters/review-stop.png)
|
||||||
|
Laat zien dat de tweekoppige hoofdstukpilot stopt vóór save.
|
||||||
|
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-chapters/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-chapters/validation.json)
|
||||||
|
|
||||||
|
Pilot voor grotere hoofdstukvalidatie:
|
||||||
|
|
||||||
|
- [review-stop.png](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/review-stop.png)
|
||||||
|
Laat zien dat 4 hoofdstukken met wisselende aantallen pagina's goed worden opgebouwd:
|
||||||
|
- `Hoofdstuk Alfa`: 1 pagina
|
||||||
|
- `Hoofdstuk Bravo`: 3 pagina's
|
||||||
|
- `Hoofdstuk Charlie`: 2 pagina's
|
||||||
|
- `Hoofdstuk Delta`: 4 pagina's
|
||||||
|
|
||||||
|
- [chapter-2-structure.png](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/chapter-2-structure.png)
|
||||||
|
- [chapter-3-structure.png](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/chapter-3-structure.png)
|
||||||
|
- [chapter-4-structure.png](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/chapter-4-structure.png)
|
||||||
|
- [plan.json](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/plan.json)
|
||||||
|
- [validation.json](/Users/nico/skillstown/artifacts/oa-runner-chapters-large/validation.json)
|
||||||
|
|
||||||
|
## Belangrijkste observatie over drag/drop
|
||||||
|
|
||||||
|
De eerdere generieke drop in het bovenste deel van `editor-main` gaf instabiele invoegposities.
|
||||||
|
|
||||||
|
Verbetering die live is gevalideerd:
|
||||||
|
- wanneer het nieuwe blok naar het onderste deel van het laatste bestaande blok wordt gedropt, wordt het in de pilot-run onder dat blok ingevoegd
|
||||||
|
- de runner vult niet meer "eerste" of "laatste" blok blind in, maar detecteert het nieuwe blok via een DOM-snapshot vóór en na insertie
|
||||||
|
|
||||||
|
Gevolg:
|
||||||
|
- omgekeerde insertievolgorde is niet meer nodig voor de huidige MVP-blokken
|
||||||
|
- de gebruiker ziet nu een natuurlijke opbouw van boven naar beneden tijdens de zichtbare run
|
||||||
|
|
||||||
|
## Belangrijke observatie over verwijderen
|
||||||
|
|
||||||
|
Tijdens de grotere hoofdstukpilot verscheen zichtbaar een bevestigingspopup bij het leegmaken van de bestaande eerste pagina.
|
||||||
|
|
||||||
|
Waarschijnlijk oorzaak:
|
||||||
|
- de runner hergebruikt voor hoofdstuk 1 de al bestaande geopende pagina
|
||||||
|
- daarvoor probeert `clearCurrentPageBlocks(...)` bestaande blokken via `Verwijderen` weg te halen
|
||||||
|
- de huidige implementatie klikt wel op `Verwijderen`, maar handelt de bevestigingspopup nog niet expliciet en stap-voor-stap af
|
||||||
|
|
||||||
|
Gevolg:
|
||||||
|
- de popup kan enkele seconden knipperen of opnieuw focus krijgen
|
||||||
|
- dit oogt voor de gebruiker onrustig
|
||||||
|
- het is een risicovoller stuk gedrag dan de overige blokinvullingen, omdat het om verwijderacties gaat
|
||||||
|
|
||||||
|
Pragmatische koerswijziging:
|
||||||
|
- de execute-flow verwijdert nu geen bestaande hoofdstukken, pagina's of blokken meer
|
||||||
|
- de runner hergebruikt hoofdstuk 1 niet meer
|
||||||
|
- voor elk Markdown-hoofdstuk wordt nu altijd een nieuw hoofdstuk aangemaakt
|
||||||
|
- daarmee verdwijnt de delete-confirmation popup uit het normale execute-pad
|
||||||
|
|
||||||
|
## Huidige tekortkomingen
|
||||||
|
|
||||||
|
Nog niet robuust:
|
||||||
|
- de nieuwe drop-strategie en execute-flow zijn live gevalideerd voor pilots met `text`, `heading`, `quote`, `multiple-choice`, `multiple-response`, `image`, `table`, `open-question` en hoofdstukken/pagina's
|
||||||
|
- scoped targeting per bloktype blijft verder hardening nodig hebben voor bredere bloksets en afwijkende editorstates
|
||||||
|
- de nieuwe "altijd nieuw hoofdstuk"-strategie moet nog live opnieuw gevalideerd worden
|
||||||
|
|
||||||
|
Nog niet opgepakt:
|
||||||
|
- matching-pairs UI-flow
|
||||||
|
- save-flow
|
||||||
|
- opschonen van oude, ongebruikte delete-helpers in de runner
|
||||||
|
|
||||||
|
## Veiligheidsafspraken
|
||||||
|
|
||||||
|
Tot nu toe gehanteerd:
|
||||||
|
- headed browser
|
||||||
|
- slow motion
|
||||||
|
- reviewmodus zonder mutaties
|
||||||
|
- execute stopt vóór save
|
||||||
|
- geen directe backend writes
|
||||||
|
- artifacts per stap
|
||||||
|
- execute-flow gebruikt nu geen delete-acties meer
|
||||||
|
|
||||||
|
Bewust nog niet gedaan:
|
||||||
|
- automatisch opslaan
|
||||||
|
- API writes
|
||||||
|
- bulkimport in productie
|
||||||
|
|
||||||
|
Aanbevolen aanvullende veiligheidsafspraak:
|
||||||
|
- als een run bestaande content zou moeten verwijderen, laat dat alleen gebeuren na expliciete gebruikersbevestiging
|
||||||
|
- voor het standaard execute-pad heeft een flow zonder delete-acties de voorkeur
|
||||||
|
|
||||||
|
## Run-instructies
|
||||||
|
|
||||||
|
In `/Users/nico/skillstown`:
|
||||||
|
|
||||||
|
Installeren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry-run parser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node markdown-to-onlineacademy-json.mjs pilot-mvp.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Zichtbare review-run zonder mutaties:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# verwacht een lokaal `.env` bestand met ten minste:
|
||||||
|
# OA_EMAIL='...'
|
||||||
|
# OA_PASSWORD='...'
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials laden uit `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
```
|
||||||
|
|
||||||
|
Zichtbare review-run zonder mutaties:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node onlineacademy-playwright-runner.mjs \
|
||||||
|
--markdown pilot-mvp.md \
|
||||||
|
--url 'https://create.onlineacademy.nl/8602e048-d7ee-4764-aafb-43412d0f65f3/edit?idsVersion=6'
|
||||||
|
```
|
||||||
|
|
||||||
|
Zichtbare execute-run tot reviewstop, zonder save:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a; source .env; set +a
|
||||||
|
node onlineacademy-playwright-runner.mjs \
|
||||||
|
--markdown pilot-mvp.md \
|
||||||
|
--url 'https://create.onlineacademy.nl/8602e048-d7ee-4764-aafb-43412d0f65f3/edit?idsVersion=6' \
|
||||||
|
--execute \
|
||||||
|
--output-dir artifacts/oa-runner-pilot
|
||||||
|
```
|
||||||
|
|
||||||
|
Zichtbare execute-run zonder save:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
node onlineacademy-playwright-runner.mjs \
|
||||||
|
--markdown pilot-mvp.md \
|
||||||
|
--url 'https://create.onlineacademy.nl/8602e048-d7ee-4764-aafb-43412d0f65f3/edit?idsVersion=6' \
|
||||||
|
--execute
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directe volgende stap
|
||||||
|
|
||||||
|
Niet meer werken met "laatste blok = nieuw blok".
|
||||||
|
|
||||||
|
Hardeningvoorstel:
|
||||||
|
|
||||||
|
1. `multiple-choice` volledig afmaken:
|
||||||
|
Status: afgerond voor de pilot-run.
|
||||||
|
2. `multiple-response`
|
||||||
|
Status: afgerond voor de pilot-run.
|
||||||
|
3. `image`
|
||||||
|
Status: afgerond voor de pilot-run.
|
||||||
|
4. `table`
|
||||||
|
Status: afgerond voor de pilot-run.
|
||||||
|
5. `open-question`
|
||||||
|
Status: afgerond voor de pilot-run.
|
||||||
|
6. hoofdstukaanmaak
|
||||||
|
Status: afgerond voor de pilot-run, maar execute-flow is inmiddels vereenvoudigd naar "altijd nieuw hoofdstuk" en moet daarop opnieuw live gevalideerd worden.
|
||||||
|
7. verdere hardening voor andere bloktypes en editorvarianten
|
||||||
|
8. save-flow
|
||||||
|
|
||||||
|
## Eindconclusie
|
||||||
|
|
||||||
|
De drag/drop Playwright-route is technisch haalbaar en veiliger passend bij deze productiecontext dan direct backend writes.
|
||||||
|
|
||||||
|
De basis is gelegd en live gevalideerd. De veiligste huidige koers is om in execute-runs alleen nieuwe hoofdstukken op te bouwen en geen bestaande content te verwijderen. Verdere hardening blijft nodig op blokpositie, veldtargeting en uiteindelijk de save-flow voordat dit betrouwbaar genoeg is voor herhaalbaar gebruik.
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
# Hoofdstuk 4 - Basisqueries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terugblik
|
||||||
|
|
||||||
|
In hoofdstuk 3 heb je kennisgemaakt met SQL als declaratieve taal: je schrijft wat je wilt, niet hoe de database dat uitrekent. Je hebt de drie groepen van SQL leren kennen - DML, DDL en DCL - en de fitworks-database geïnstalleerd. Die database staat nu klaar.
|
||||||
|
|
||||||
|
> **Neem even 30 seconden.** In hoofdstuk 3 stond al een korte SELECT-query als voorbeeld. Kon je op dat moment al raden wat hij deed? En nu, na het lezen van de beschrijving van DDL en DML, zou je dan anders antwoorden? Bewaar dat gevoel - aan het einde van dit hoofdstuk kijk je terug op je eigen redenering.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opening
|
||||||
|
|
||||||
|
Je bent systeembeheerder bij FitWorks. De vestigingsmanager loopt op je af: "Kun jij me snel een lijst geven van alle leden die geen telefoonnummer hebben ingevuld? We willen ze via e-mail aanschrijven." Een simpele vraag. Maar hoe stel je die aan een database?
|
||||||
|
|
||||||
|
Je kunt niet op een knop klikken. Er bestaat geen menu-optie "geef me leden zonder telefoon". Je hebt een instructie nodig die de database begrijpt - precies, ondubbelzinnig, in de juiste volgorde. Dat is een query. En er zit een addertje onder het gras in die vraag over telefoonnummers. Dat addertje heet NULL. Je ontdekt hem in sectie 4.3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Leerdoelen
|
||||||
|
|
||||||
|
Na dit hoofdstuk kun je:
|
||||||
|
|
||||||
|
- een query schrijven met `SELECT`, `FROM` en `WHERE` en uitleggen wat elk onderdeel doet;
|
||||||
|
- resultaten filteren met vergelijkingsoperatoren, `BETWEEN`, `IN` en logische operators `AND`, `OR` en `NOT`;
|
||||||
|
- uitleggen wat NULL is, waarom je er niet op kunt filteren met `=`, en hoe je dat correct doet met `IS NULL`;
|
||||||
|
- resultaten sorteren met `ORDER BY` en dubbele waarden verwijderen met `DISTINCT`;
|
||||||
|
- kolomaliassen toewijzen met `AS` en vaste tekst (literals) in een query opnemen;
|
||||||
|
- voor- en achternamen samenvoegen met `CONCAT` en datatypes omzetten met `CAST`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.1 SELECT en FROM - je eerste query
|
||||||
|
|
||||||
|
### De balie vraagt, de database antwoordt
|
||||||
|
|
||||||
|
Het is dinsdagochtend bij FitWorks en een trainer vraagt aan de receptie: "Wie staat er allemaal ingeschreven?" De receptioniste opent phpMyAdmin, typt een instructie en krijgt binnen een seconde een lijst op het scherm. Die instructie is klein maar compleet:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit is een volledige SQL-query. Ze bestaat uit twee onderdelen. `SELECT` zegt welke kolommen je wilt zien, gescheiden door komma's. `FROM` zegt uit welke tabel die kolommen komen. Zonder `FROM` weet de database niet waar hij moet zoeken. Zonder `SELECT` weet hij niet wat hij moet tonen. Ze zijn onlosmakelijk verbonden.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
De volgorde is vastgelegd: `SELECT` altijd eerst, dan `FROM`, dan pas eventuele aanvullingen zoals `WHERE`. SQL klaagt direct als je die volgorde omdraait. Het is geen aanbeveling - het is een syntaxregel.
|
||||||
|
|
||||||
|
### Alle kolommen tegelijk: de asterisk
|
||||||
|
|
||||||
|
Soms wil je snel alle kolommen zien die een tabel bevat, zonder ze één voor één op te noemen. Dat doe je met de asterisk (`*`):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT *
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
De `*` is een verkorting voor "geef me alles". Handig bij het verkennen van een tabel. In de praktijk gebruik je `*` spaarzaam: het haalt onnodig veel data op, maakt je query minder leesbaar en kan vertragen als een tabel tientallen kolommen heeft. Maar als leergereedschap bij het doorzoeken van fitworks is het prima.
|
||||||
|
|
||||||
|
> **Kolomnamen in fitworks zijn altijd lowercase.** Schrijf dus `voornaam`, niet `Voornaam` of `VOORNAAM`. MySQL maakt op de meeste systemen geen onderscheid tussen hoofd- en kleine letters in kolomnamen, maar consequent lowercase schrijven voorkomt verwarring.
|
||||||
|
|
||||||
|
### Mini-oefening
|
||||||
|
|
||||||
|
Je wilt de e-mailadressen en geboortedatums van alle leden zien. Welke query schrijf je?
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Schrijf hier jouw query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwoord:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT email, geboortedatum
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 WHERE - filteren op voorwaarden
|
||||||
|
|
||||||
|
### Niet 400 rijen, maar precies de rijen die je nodig hebt
|
||||||
|
|
||||||
|
De tabel `leden` bevat zo'n 400 rijen. Als je de manager een overzicht stuurt van actieve leden, heb je die 400 rijen niet nodig. Je wilt alleen de rijen waarbij `actief` gelijk is aan `1`. Daarvoor gebruik je `WHERE`.
|
||||||
|
|
||||||
|
`WHERE` is een filter. De database loopt alle rijen van de opgegeven tabel langs en geeft alleen de rijen terug die voldoen aan de voorwaarde die jij opgeeft. Rijen die er niet aan voldoen, worden stilletjes weggelaten.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam, email
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
`WHERE` staat altijd na `FROM`. De volgorde `SELECT - FROM - WHERE` is onwrikbaar. Typ je `WHERE` voor `FROM`, dan krijg je een foutmelding.
|
||||||
|
|
||||||
|
### Vergelijkingsoperatoren
|
||||||
|
|
||||||
|
In een `WHERE`-clausule vergelijk je een kolom met een waarde. Voor die vergelijking gebruik je een operator. Het diagram hieronder geeft een overzicht van de meestgebruikte operators met voorbeelden uit de fitworks-database.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Twee operators verdienen extra toelichting.
|
||||||
|
|
||||||
|
**BETWEEN** filtert op een bereik, inclusief de grenswaarden zelf. Dit is handig als je iets zoekt binnen een prijsklasse of een tijdvak:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT naam, prijs_per_maand
|
||||||
|
FROM abonnementen
|
||||||
|
WHERE prijs_per_maand BETWEEN 20 AND 40;
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit geeft abonnementen terug waarvan de prijs 20, 40 of iets daartussenin bedraagt. Let op: beide grenzen tellen mee.
|
||||||
|
|
||||||
|
**IN** laat je meerdere toegestane waarden opgeven als een lijst, zonder een reeks `OR`-voorwaarden te schrijven. Het resultaat is hetzelfde, maar de query is compacter en beter leesbaar:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status IN ('betaald', 'openstaand');
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit is identiek aan `WHERE status = 'betaald' OR status = 'openstaand'`. Bij twee waarden maakt het weinig uit, maar bij vijf of zes wordt `IN` een stuk overzichtelijker.
|
||||||
|
|
||||||
|
### Logische operators: AND, OR en NOT
|
||||||
|
|
||||||
|
Voorwaarden kun je combineren. `AND` eist dat beide voorwaarden gelden:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1
|
||||||
|
AND locatie_id = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
Alleen rijen waarbij actief gelijk is aan 1 én locatie_id gelijk aan 2, komen door het filter. Als één van beide niet klopt, valt de rij af.
|
||||||
|
|
||||||
|
`OR` is ruimer: minstens één voorwaarde moet kloppen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status = 'mislukt'
|
||||||
|
OR status = 'openstaand';
|
||||||
|
```
|
||||||
|
|
||||||
|
Elke betaling die mislukt is, of openstaand is, of allebei - al die rijen verschijnen in het resultaat.
|
||||||
|
|
||||||
|
`NOT` keert een voorwaarde om. Gebruik het spaarzaam, want `!=` of `<>` is in de meeste gevallen leesbaarder:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE NOT actief = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit is logisch gelijk aan `WHERE actief = 1` - maar minder direct leesbaar.
|
||||||
|
|
||||||
|
### Tekst en getallen: aanhalingstekens
|
||||||
|
|
||||||
|
Er is een belangrijk verschil in notatie. Getallen schrijf je zonder aanhalingstekens: `actief = 1`. Tekst schrijf je altijd tussen enkele aanhalingstekens: `status = 'betaald'`. Gebruik je dubbele aanhalingstekens voor tekst, dan klaagt MySQL. In de fitworks-database zijn kolomnamen nooit geciteerd - ze staan altijd zonder aanhalingstekens in de query.
|
||||||
|
|
||||||
|
### Reflectievraag
|
||||||
|
|
||||||
|
De vestigingsmanager wil weten welke trainers bij FitWorks locatie 1 werken en ook een specialisatie hebben opgegeven. Welke query schrijf je? Denk goed na over de voorwaarde voor "heeft een specialisatie".
|
||||||
|
|
||||||
|
*(Schrijf je antwoord op voordat je verder gaat.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Modelantwoord - reflectievraag 4.2
|
||||||
|
|
||||||
|
> **Mogelijk antwoord**
|
||||||
|
>
|
||||||
|
> De tabel `trainers` bevat een kolom `specialisatie`. Die kolom kan leeg zijn - dat betekent in een database niet een lege string, maar NULL. Om alleen de trainers te tonen die een specialisatie hebben ingevuld, gebruik je `IS NOT NULL`. Dat concept leer je in de volgende sectie uitgebreid kennen.
|
||||||
|
>
|
||||||
|
> ```sql
|
||||||
|
> SELECT voornaam, achternaam, specialisatie
|
||||||
|
> FROM trainers
|
||||||
|
> WHERE locatie_id = 1
|
||||||
|
> AND specialisatie IS NOT NULL;
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> `AND` zorgt ervoor dat beide voorwaarden tegelijk waar moeten zijn. Een trainer zonder specialisatie of van een andere locatie valt af.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 NULL - de onzichtbare valkuil
|
||||||
|
|
||||||
|
### Een waarde die geen waarde is
|
||||||
|
|
||||||
|
Terug naar de vraag van de vestigingsmanager: leden zonder telefoonnummer. Wat staat er in de database als iemand geen nummer heeft opgegeven? Geen nul. Geen lege string. Er staat iets anders: **NULL**.
|
||||||
|
|
||||||
|
NULL is de afwezigheid van een waarde. Het is geen getal, geen tekst, geen spatie. Het is simpelweg: er is geen informatie. De database zegt letterlijk "ik weet het niet." Dat is een fundamenteel ander concept dan "de waarde is nul" of "de waarde is leeg".
|
||||||
|
|
||||||
|
In fitworks kom je NULL op meerdere plekken tegen. De kolom `tussenvoegsel` in de tabel `leden` is NULL voor iedereen zonder tussenvoegsel - niet "", maar NULL. De kolom `telefoon` is NULL als een lid geen nummer heeft opgegeven. De kolom `specialisatie` bij `trainers` is NULL als dat veld bij indiensttreding niet is ingevuld.
|
||||||
|
|
||||||
|
### Waarom werkt `= NULL` niet?
|
||||||
|
|
||||||
|
Dit is de meest gemaakte fout bij beginners, en de meest gevaarlijke - want je krijgt geen foutmelding. Je schrijft:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Dit werkt NIET zoals je denkt
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon = NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
De query wordt uitgevoerd. MySQL klaagt niet. Maar je krijgt nul resultaten, ook als er tientallen leden zonder telefoonnummer zijn. Hoe kan dat?
|
||||||
|
|
||||||
|
In SQL is NULL geen gewone waarde die je kunt vergelijken. De uitdrukking `NULL = NULL` is niet waar. Ze is ook niet onwaar. Ze is onbekend. En een vergelijking met een onbekende uitkomst beschouwt SQL als niet-waar. Elke rij valt dan af, ook als het telefoonnummer werkelijk leeg is.
|
||||||
|
|
||||||
|
De database redeneert zo: "Is het telefoonnummer van dit lid gelijk aan NULL? Ik weet het niet - want NULL is onbekend." En als het onbekend is, wordt de rij niet meegenomen.
|
||||||
|
|
||||||
|
### De juiste aanpak: IS NULL en IS NOT NULL
|
||||||
|
|
||||||
|
De database heeft speciale commando's gemaakt voor NULL-controles, omdat de gewone vergelijkingsoperator niet werkt:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Leden zonder telefoonnummer
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon IS NULL;
|
||||||
|
|
||||||
|
-- Leden met telefoonnummer
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
`IS NULL` is de enige correcte manier om te controleren of een kolom geen waarde bevat. `IS NOT NULL` doet het omgekeerde: die filtert op rijen waar wél een waarde staat.
|
||||||
|
|
||||||
|
### Wat er misgaat als je dit negeert
|
||||||
|
|
||||||
|
Het probleem met `= NULL` is verraderlijk: de query geeft geen foutmelding en lijkt normaal te werken - maar geeft stilletjes een leeg resultaat. Stel, je levert als systeembeheerder een rapport op basis van die query aan de manager. Ze ziet een lege lijst en denkt: "Alle leden hebben een telefoonnummer." Niemand ziet dat de data ontbreekt. Beslissingen op basis van dat rapport zijn daardoor onjuist, zonder dat iemand het weet.
|
||||||
|
|
||||||
|
> **Onthoud:** NULL vergelijk je nooit met `=`. Altijd `IS NULL` of `IS NOT NULL`.
|
||||||
|
|
||||||
|
### Mini-oefening
|
||||||
|
|
||||||
|
De vestigingsmanager wil een lijst van alle lestypes waarbij de beschrijving niet is ingevuld. Schrijf de query.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Schrijf hier jouw query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwoord:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT naam, duur_minuten, niveau
|
||||||
|
FROM lestypes
|
||||||
|
WHERE beschrijving IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.4 ORDER BY en DISTINCT - sorteren en ontdubbelen
|
||||||
|
|
||||||
|
### ORDER BY: jij bepaalt de volgorde
|
||||||
|
|
||||||
|
De database geeft rijen terug in de volgorde die hem uitkomt - dat is meestal de volgorde waarin de rijen zijn opgeslagen, maar daar kun je niet op rekenen. Als je een ledenlijst wilt gesorteerd op inschrijfdatum, heb je `ORDER BY` nodig.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY lid_sinds;
|
||||||
|
```
|
||||||
|
|
||||||
|
Standaard sorteert `ORDER BY` **oplopend** (van vroeg naar laat, van klein naar groot). Dat heet `ASC` (ascending). Wil je de nieuwste inschrijvingen bovenaan, draai je de volgorde om met `DESC` (descending):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY lid_sinds DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
Je kunt ook op meerdere kolommen sorteren. De database sorteert eerst op de eerste kolom en, bij gelijke waarden daarin, op de tweede:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT achternaam, voornaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY achternaam, voornaam;
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit levert een alfabetische ledenlijst op achternaam, met bij gelijke achternaam een alfabetische sortering op voornaam. `ORDER BY` staat altijd als laatste in de query - na `WHERE`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1
|
||||||
|
ORDER BY lid_sinds DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### DISTINCT: elk resultaat slechts één keer
|
||||||
|
|
||||||
|
Stel, je wilt weten welke specialisaties er bij FitWorks-trainers voorkomen. Je schrijft:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT specialisatie
|
||||||
|
FROM trainers;
|
||||||
|
```
|
||||||
|
|
||||||
|
Je krijgt 12 rijen - één per trainer. Maar meerdere trainers kunnen dezelfde specialisatie hebben: "Yoga" verschijnt misschien drie keer. Als je alleen de unieke specialisaties wilt zien, gebruik je `DISTINCT`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT DISTINCT specialisatie
|
||||||
|
FROM trainers;
|
||||||
|
```
|
||||||
|
|
||||||
|
`DISTINCT` zet je direct na `SELECT`. Het verwijdert rijen die in alle geselecteerde kolommen identiek zijn. Staat er vier keer "Yoga" in de resultaten, dan zie je er nog maar één.
|
||||||
|
|
||||||
|
Een veelgemaakte misvatting: `DISTINCT` werkt niet per kolom apart, maar op de combinatie van alle geselecteerde kolommen. `SELECT DISTINCT specialisatie, voornaam` geeft unieke combinaties van specialisatie én voornaam - niet unieke specialisaties per kolom. Wil je alleen unieke specialisaties, selecteer dan ook alleen die kolom.
|
||||||
|
|
||||||
|
### Reflectievraag
|
||||||
|
|
||||||
|
Je wilt een lijst van alle abonnementsnamen met hun maandprijs, gesorteerd van duurste naar goedkoopste. Welke query schrijf je? En wat verandert er als je `DISTINCT` toevoegt - en wanneer maakt dat verschil?
|
||||||
|
|
||||||
|
*(Schrijf je antwoord op voordat je verder gaat.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Modelantwoord - reflectievraag 4.4
|
||||||
|
|
||||||
|
> **Mogelijk antwoord**
|
||||||
|
>
|
||||||
|
> Abonnementsnamen en prijzen staan in de tabel `abonnementen`. Sorteren van duurste naar goedkoopste vraagt om `ORDER BY prijs_per_maand DESC`.
|
||||||
|
>
|
||||||
|
> ```sql
|
||||||
|
> SELECT naam, prijs_per_maand
|
||||||
|
> FROM abonnementen
|
||||||
|
> ORDER BY prijs_per_maand DESC;
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> `DISTINCT` toevoegen verandert hier niets, want in fitworks zijn abonnementsnamen uniek. Maar als je alleen `SELECT DISTINCT naam` zou schrijven zonder prijs, dan filtert `DISTINCT` wel dubbele namen eruit. Het verschil zit hem dus in wat je selecteert: voeg je meer kolommen toe, dan worden meer combinaties als uniek beschouwd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.5 Kolomaliassen en literals
|
||||||
|
|
||||||
|
### Een kolom een andere naam geven
|
||||||
|
|
||||||
|
Stel, je draait een rapport voor de vestigingsmanager. Ze ziet een overzicht met kolomnamen als `voornaam`, `lid_sinds` en `locatie_id`. Die namen zijn prima voor een databaseontwikkelaar - maar minder leesbaar voor iemand die gewend is aan Excel. Met een **alias** geef je een kolom in het resultaat een andere naam, zonder de tabel te veranderen.
|
||||||
|
|
||||||
|
Een alias schrijf je met het sleutelwoord `AS`, direct na de kolomnaam:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam AS Voornaam,
|
||||||
|
achternaam AS Achternaam,
|
||||||
|
lid_sinds AS 'Lid sinds'
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
Het resultaat toont de kolomkoppen "Voornaam", "Achternaam" en "Lid sinds". De tabeldefinitie in de database verandert niet - de alias bestaat alleen in dit resultaat. Als een alias een spatie bevat, zet je hem tussen enkele aanhalingstekens.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Waarom is dit nuttig? Omdat dezelfde query in de ene context voor een manager draait en in een andere context voor een ontwikkelaar. De alias laat je het resultaat aanpassen aan de lezer, zonder de query zelf fundamenteel te veranderen.
|
||||||
|
|
||||||
|
### Literals: vaste tekst in elk resultaat
|
||||||
|
|
||||||
|
Een **literal** is een vaste waarde die je direct in een query schrijft - geen kolom, maar een stuk tekst of een getal dat voor elke rij hetzelfde is. Literals zijn handig om context mee te geven aan een resultaat.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT voornaam,
|
||||||
|
achternaam,
|
||||||
|
'FitWorks lid' AS type
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Elke rij in het resultaat krijgt een extra kolom "type" met de waarde `FitWorks lid`. De database vult die waarde automatisch in voor elke rij die het filter doorkomt. Een literal verandert nooit per rij - hij is altijd hetzelfde, vandaar de naam. Dit is nuttig als je twee queryresultaten samenvoegt en ze wilt markeren met een label, of als je een rapport met vaste context wilt exporteren.
|
||||||
|
|
||||||
|
### Mini-oefening
|
||||||
|
|
||||||
|
Schrijf een query die de naam en maandprijs van alle abonnementen toont, met als kolomkoppen "Abonnementsnaam" en "Maandprijs". Sorteer op prijs, laagste eerst.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Schrijf hier jouw query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwoord:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT naam AS Abonnementsnaam,
|
||||||
|
prijs_per_maand AS Maandprijs
|
||||||
|
FROM abonnementen
|
||||||
|
ORDER BY prijs_per_maand;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.6 Concatenatie en CAST
|
||||||
|
|
||||||
|
### Kolommen samenvoegen: CONCAT
|
||||||
|
|
||||||
|
Bij FitWorks is de voornaam opgeslagen in de kolom `voornaam` en de achternaam in `achternaam`. Dat is goed ontwerp - want dan kun je sorteren op achternaam, zoeken op voornaam, en filteren op elk deel afzonderlijk. Maar als je een overzicht wilt met de volledige naam als één kolom, heb je `CONCAT` nodig.
|
||||||
|
|
||||||
|
`CONCAT` plakt twee of meer waarden achter elkaar. Je geeft de waarden als argumenten mee, gescheiden door komma's:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT CONCAT(voornaam, ' ', achternaam) AS volledige_naam
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
Het resultaat bevat één kolom "volledige_naam" met waarden als `Sophie Bakker` of `Tom Jansen`. De spatie tussen voornaam en achternaam voeg je zelf toe als literal - de database doet dat niet automatisch.
|
||||||
|
|
||||||
|
Maar in fitworks hebben sommige leden een tussenvoegsel. Je wilt dan iets als `Tom van der Berg`, niet `Tom van der Berg` met een dubbele spatie of `Tom Berg` zonder tussenvoegsel. De oplossing gebruikt `COALESCE`, een functie die NULL vervangt door een opgegeven standaardwaarde:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT CONCAT(
|
||||||
|
voornaam, ' ',
|
||||||
|
COALESCE(CONCAT(tussenvoegsel, ' '), ''),
|
||||||
|
achternaam
|
||||||
|
) AS volledige_naam
|
||||||
|
FROM leden;
|
||||||
|
```
|
||||||
|
|
||||||
|
`COALESCE(CONCAT(tussenvoegsel, ' '), '')` zegt: als `tussenvoegsel` NULL is, gebruik dan een lege string. Als het er wél is, plak er een spatie achter. Zo staat de naam altijd correct opgemaakt.
|
||||||
|
|
||||||
|
> **Even terzijde:** `COALESCE` is een handige functie voor NULL-situaties. Je ziet hem vaker terugkomen in latere hoofdstukken. Voor nu is het genoeg om te weten dat hij bestaat en wat hij doet.
|
||||||
|
|
||||||
|
### Datatypes omzetten: CAST
|
||||||
|
|
||||||
|
Elke waarde in een database heeft een datatype: tekst (`VARCHAR`, `CHAR`), getal (`INT`, `DECIMAL`), datum (`DATE`), enzovoort. Soms wil je een waarde als een ander type gebruiken - een getal weergeven als tekst zodat je er iets voor kunt plakken, of een datum omzetten naar een leesbaar formaat. Daarvoor is `CAST`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT naam,
|
||||||
|
CONCAT('Prijs: ', CAST(prijs_per_maand AS CHAR)) AS prijsweergave
|
||||||
|
FROM abonnementen;
|
||||||
|
```
|
||||||
|
|
||||||
|
`CAST(prijs_per_maand AS CHAR)` zet het decimale getal `29.95` om naar de tekst `'29.95'`. Daarna kan `CONCAT` er de vaste tekst `'Prijs: '` voor plakken. Zonder `CAST` zou MySQL in sommige situaties klagen dat je een getal en een string probeert samen te voegen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
De meestgebruikte CAST-conversies in MySQL zijn:
|
||||||
|
|
||||||
|
| Van | Naar | Gebruik |
|
||||||
|
|-----|------|---------|
|
||||||
|
| Getal | Tekst | `CAST(prijs_per_maand AS CHAR)` |
|
||||||
|
| Tekst | Geheel getal | `CAST('42' AS UNSIGNED)` |
|
||||||
|
| Datum | Tekst | `CAST(geboortedatum AS CHAR)` |
|
||||||
|
|
||||||
|
### Mini-oefening
|
||||||
|
|
||||||
|
Schrijf een query die de volledige naam van elke trainer toont (voornaam, spatie, achternaam) in één kolom genaamd "Trainer". Gebruik de tabel `trainers`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Schrijf hier jouw query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwoord:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT CONCAT(voornaam, ' ', achternaam) AS Trainer
|
||||||
|
FROM trainers;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quiz
|
||||||
|
|
||||||
|
### Vraag 1
|
||||||
|
|
||||||
|
Je wilt alle betalingen zien met status `mislukt` of `openstaand`, gesorteerd van hoog naar laag bedrag. Welke query is correct?
|
||||||
|
|
||||||
|
**A)**
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status = 'mislukt' OR 'openstaand'
|
||||||
|
ORDER BY bedrag DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**B)**
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status IN ('mislukt', 'openstaand')
|
||||||
|
ORDER BY bedrag DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**C)**
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status IN ('mislukt', 'openstaand')
|
||||||
|
ORDER BY bedrag ASC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**D)**
|
||||||
|
```sql
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status = 'mislukt' OR status = 'openstaand'
|
||||||
|
ORDER BY bedrag ASC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct antwoord: B**
|
||||||
|
|
||||||
|
*Toelichting:* Optie B gebruikt `IN` voor de statusfilter - dat is syntactisch correct en compact. `ORDER BY bedrag DESC` sorteert van hoog naar laag, zoals gevraagd. Optie A is syntactisch fout: `OR 'openstaand'` zonder kolomnaam werkt niet. Opties C en D gebruiken `ASC`, waardoor de volgorde omgekeerd is aan wat gevraagd wordt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Vraag 2
|
||||||
|
|
||||||
|
Welke query geeft correct alle leden terug die geen tussenvoegsel hebben?
|
||||||
|
|
||||||
|
**A)**
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE tussenvoegsel = NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**B)**
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE tussenvoegsel = '';
|
||||||
|
```
|
||||||
|
|
||||||
|
**C)**
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE tussenvoegsel IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**D)**
|
||||||
|
```sql
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE tussenvoegsel IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct antwoord: C**
|
||||||
|
|
||||||
|
*Toelichting:* Optie C is de enige correcte aanpak. NULL vergelijk je nooit met `=` - die vergelijking levert altijd "onbekend" op en geeft nul resultaten, ook als er tientallen rijen zonder tussenvoegsel zijn. Optie B zoekt naar een lege string, maar in fitworks is een ontbrekend tussenvoegsel opgeslagen als NULL, niet als een lege string - die zijn fundamenteel anders. Optie D geeft het tegenovergestelde: leden mét tussenvoegsel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Vraag 3
|
||||||
|
|
||||||
|
Een collega schrijft deze query en ziet dat de kolom `specialisatie` nog steeds herhaalde waarden bevat:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT DISTINCT specialisatie, voornaam
|
||||||
|
FROM trainers;
|
||||||
|
```
|
||||||
|
|
||||||
|
Wat is de oorzaak?
|
||||||
|
|
||||||
|
**A)** `DISTINCT` werkt alleen op de eerste geselecteerde kolom.
|
||||||
|
|
||||||
|
**B)** `DISTINCT` verwijdert dubbele rijen op basis van alle geselecteerde kolommen tegelijk. Twee trainers met dezelfde specialisatie maar een andere voornaam zijn geen duplicaat.
|
||||||
|
|
||||||
|
**C)** `DISTINCT` moet altijd gecombineerd worden met `ORDER BY`.
|
||||||
|
|
||||||
|
**D)** `DISTINCT` werkt niet op tekst-kolommen als `specialisatie`.
|
||||||
|
|
||||||
|
**Correct antwoord: B**
|
||||||
|
|
||||||
|
*Toelichting:* `DISTINCT` kijkt naar de combinatie van alle geselecteerde kolommen. Als twee trainers dezelfde specialisatie maar een andere voornaam hebben, beschouwt MySQL ze als twee unieke rijen. Wil je alleen unieke specialisaties zien, selecteer dan ook alleen die kolom: `SELECT DISTINCT specialisatie FROM trainers;`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Samenvatting
|
||||||
|
|
||||||
|
`SELECT` en `FROM` vormen het hart van elke query. `SELECT` bepaalt welke kolommen je ziet, `FROM` bepaalt de tabel. Ze zijn verplicht en staan altijd in die volgorde.
|
||||||
|
|
||||||
|
Met `WHERE` filter je welke rijen je terugkrijgt. Je gebruikt vergelijkingsoperatoren (`=`, `!=`, `<`, `>`, `BETWEEN`, `IN`) en logische operators (`AND`, `OR`, `NOT`) om voorwaarden te combineren. Tekst staat altijd tussen enkele aanhalingstekens.
|
||||||
|
|
||||||
|
NULL is de afwezigheid van een waarde - niet nul, niet leeg, maar onbekend. Je kunt er nooit op filteren met `=`. Gebruik altijd `IS NULL` of `IS NOT NULL`.
|
||||||
|
|
||||||
|
Met `ORDER BY` sorteer je resultaten op een of meerdere kolommen, oplopend (`ASC`) of aflopend (`DESC`). `DISTINCT` verwijdert rijen die op alle geselecteerde kolommen identiek zijn.
|
||||||
|
|
||||||
|
Kolomaliassen (`AS`) geven een kolom een andere naam in het resultaat. Literals zijn vaste waarden die je direct in de query opneemt. `CONCAT` plakt meerdere waarden aaneen. `CAST` zet een waarde om naar een ander datatype.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vooruitblik
|
||||||
|
|
||||||
|
Je hebt nu de basis in handen om de fitworks-database te bevragen. Maar er is meer. Wat als je leden wilt zoeken op een deel van hun achternaam - iedereen wiens naam begint met "van"? Of wil weten hoe oud iemand is op basis van hun geboortedatum? Daarvoor heb je gereedschap nodig dat verder gaat dan de basisoperatoren. In hoofdstuk 5 leer je werken met tekstbewerkingen (waaronder `LIKE` voor zoeken op patronen) en datumfuncties. Je gaat de data niet alleen ophalen - je gaat er echt mee werken.
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
---
|
||||||
|
training: SQL met FitWorks
|
||||||
|
---
|
||||||
|
|
||||||
|
# Module: Hoofdstuk 4 - Basisqueries
|
||||||
|
|
||||||
|
## Pagina: Terugblik en opening
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Terugblik
|
||||||
|
|
||||||
|
In hoofdstuk 3 heb je kennisgemaakt met SQL als declaratieve taal: je schrijft wat je wilt, niet hoe de database dat uitrekent. Je hebt de drie groepen van SQL leren kennen - DML, DDL en DCL - en de fitworks-database geinstalleerd. Die database staat nu klaar.
|
||||||
|
|
||||||
|
Neem even 30 seconden. In hoofdstuk 3 stond al een korte `SELECT`-query als voorbeeld. Kon je op dat moment al raden wat hij deed? En nu, na het lezen van de beschrijving van DDL en DML, zou je dan anders antwoorden? Bewaar dat gevoel; aan het einde van dit hoofdstuk kijk je terug op je eigen redenering.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Opening
|
||||||
|
|
||||||
|
Je bent systeembeheerder bij FitWorks. De vestigingsmanager loopt op je af: "Kun jij me snel een lijst geven van alle leden die geen telefoonnummer hebben ingevuld? We willen ze via e-mail aanschrijven."
|
||||||
|
|
||||||
|
Een simpele vraag. Maar hoe stel je die aan een database?
|
||||||
|
|
||||||
|
Je kunt niet op een knop klikken. Er bestaat geen menu-optie "geef me leden zonder telefoon". Je hebt een instructie nodig die de database begrijpt: precies, ondubbelzinnig en in de juiste volgorde. Dat is een query. En er zit een addertje onder het gras in die vraag over telefoonnummers. Dat addertje heet `NULL`. Je ontdekt het in sectie 4.3.
|
||||||
|
|
||||||
|
## Pagina: Leerdoelen
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Leerdoelen
|
||||||
|
|
||||||
|
Na dit hoofdstuk kun je:
|
||||||
|
|
||||||
|
- een query schrijven met `SELECT`, `FROM` en `WHERE` en uitleggen wat elk onderdeel doet;
|
||||||
|
- resultaten filteren met vergelijkingsoperatoren, `BETWEEN`, `IN` en logische operators `AND`, `OR` en `NOT`;
|
||||||
|
- uitleggen wat `NULL` is, waarom je er niet op kunt filteren met `=`, en hoe je dat correct doet met `IS NULL`;
|
||||||
|
- resultaten sorteren met `ORDER BY` en dubbele waarden verwijderen met `DISTINCT`;
|
||||||
|
- kolomaliassen toewijzen met `AS` en vaste tekst, literals, in een query opnemen;
|
||||||
|
- voor- en achternamen samenvoegen met `CONCAT` en datatypes omzetten met `CAST`.
|
||||||
|
|
||||||
|
## Pagina: 4.1 SELECT en FROM
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** De balie vraagt, de database antwoordt
|
||||||
|
|
||||||
|
Het is dinsdagochtend bij FitWorks en een trainer vraagt aan de receptie: "Wie staat er allemaal ingeschreven?" De receptioniste opent phpMyAdmin, typt een instructie en krijgt binnen een seconde een lijst op het scherm. Die instructie is klein maar compleet:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden;
|
||||||
|
|
||||||
|
Dit is een volledige SQL-query. Ze bestaat uit twee onderdelen. `SELECT` zegt welke kolommen je wilt zien, gescheiden door komma's. `FROM` zegt uit welke tabel die kolommen komen. Zonder `FROM` weet de database niet waar hij moet zoeken. Zonder `SELECT` weet hij niet wat hij moet tonen. Ze zijn onlosmakelijk verbonden.
|
||||||
|
|
||||||
|
De volgorde is vastgelegd: `SELECT` altijd eerst, dan `FROM`, dan pas eventuele aanvullingen zoals `WHERE`. SQL klaagt direct als je die volgorde omdraait. Het is geen aanbeveling, maar een syntaxregel.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_01_select_from_anatomie.png
|
||||||
|
**Titel:** Anatomie van een SELECT-query
|
||||||
|
**Onderschrift:** Overzicht van de onderdelen `SELECT`, `FROM` en later `WHERE`.
|
||||||
|
**Alt:** Diagram van een SELECT-query met de onderdelen SELECT, FROM en WHERE.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Alle kolommen tegelijk
|
||||||
|
|
||||||
|
Soms wil je snel alle kolommen zien die een tabel bevat, zonder ze een voor een op te noemen. Dat doe je met de asterisk `*`:
|
||||||
|
|
||||||
|
SELECT *
|
||||||
|
FROM leden;
|
||||||
|
|
||||||
|
De `*` is een verkorting voor "geef me alles". Handig bij het verkennen van een tabel. In de praktijk gebruik je `*` spaarzaam: het haalt onnodig veel data op, maakt je query minder leesbaar en kan vertragen als een tabel tientallen kolommen heeft. Maar als leergereedschap bij het doorzoeken van fitworks is het prima.
|
||||||
|
|
||||||
|
Kolomnamen in fitworks zijn altijd lowercase. Schrijf dus `voornaam`, niet `Voornaam` of `VOORNAAM`. MySQL maakt op de meeste systemen geen onderscheid tussen hoofd- en kleine letters in kolomnamen, maar consequent lowercase schrijven voorkomt verwarring.
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Je wilt de e-mailadressen en geboortedatums van alle leden zien. Welke query schrijf je?
|
||||||
|
**Toelichting:** Modelantwoord: `SELECT email, geboortedatum FROM leden;`
|
||||||
|
|
||||||
|
## Pagina: 4.2 WHERE
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Filteren op voorwaarden
|
||||||
|
|
||||||
|
De tabel `leden` bevat zo'n 400 rijen. Als je de manager een overzicht stuurt van actieve leden, heb je die 400 rijen niet nodig. Je wilt alleen de rijen waarbij `actief` gelijk is aan `1`. Daarvoor gebruik je `WHERE`.
|
||||||
|
|
||||||
|
`WHERE` is een filter. De database loopt alle rijen van de opgegeven tabel langs en geeft alleen de rijen terug die voldoen aan de voorwaarde die jij opgeeft. Rijen die er niet aan voldoen, worden stilletjes weggelaten.
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam, email
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1;
|
||||||
|
|
||||||
|
`WHERE` staat altijd na `FROM`. De volgorde `SELECT`, `FROM`, `WHERE` is onwrikbaar. Typ je `WHERE` voor `FROM`, dan krijg je een foutmelding.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Vergelijkingsoperatoren
|
||||||
|
|
||||||
|
In een `WHERE`-clausule vergelijk je een kolom met een waarde. Voor die vergelijking gebruik je een operator.
|
||||||
|
|
||||||
|
`BETWEEN` filtert op een bereik, inclusief de grenswaarden zelf:
|
||||||
|
|
||||||
|
SELECT naam, prijs_per_maand
|
||||||
|
FROM abonnementen
|
||||||
|
WHERE prijs_per_maand BETWEEN 20 AND 40;
|
||||||
|
|
||||||
|
`IN` laat je meerdere toegestane waarden opgeven als een lijst:
|
||||||
|
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status IN ('betaald', 'openstaand');
|
||||||
|
|
||||||
|
Dit is identiek aan `WHERE status = 'betaald' OR status = 'openstaand'`, maar compacter en beter leesbaar.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_02_where_operatoren.png
|
||||||
|
**Titel:** WHERE-operatoren
|
||||||
|
**Onderschrift:** Overzicht van veelgebruikte vergelijkingsoperatoren in fitworks.
|
||||||
|
**Alt:** Overzicht van WHERE-operatoren met voorbeelden uit de fitworks-database.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Logische operators
|
||||||
|
|
||||||
|
Voorwaarden kun je combineren.
|
||||||
|
|
||||||
|
Met `AND` moeten beide voorwaarden gelden:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1
|
||||||
|
AND locatie_id = 2;
|
||||||
|
|
||||||
|
Met `OR` hoeft minimaal een voorwaarde te kloppen:
|
||||||
|
|
||||||
|
SELECT betaling_id, bedrag, status
|
||||||
|
FROM betalingen
|
||||||
|
WHERE status = 'mislukt'
|
||||||
|
OR status = 'openstaand';
|
||||||
|
|
||||||
|
`NOT` keert een voorwaarde om:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE NOT actief = 0;
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Tekst en getallen
|
||||||
|
|
||||||
|
Getallen schrijf je zonder aanhalingstekens: `actief = 1`.
|
||||||
|
|
||||||
|
Tekst schrijf je tussen enkele aanhalingstekens: `status = 'betaald'`.
|
||||||
|
|
||||||
|
Gebruik in MySQL geen dubbele aanhalingstekens voor tekstwaarden. In de fitworks-database zijn kolomnamen nooit geciteerd; ze staan altijd zonder aanhalingstekens in de query.
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Welke query schrijf je om trainers op locatie 1 te tonen die ook een specialisatie hebben opgegeven?
|
||||||
|
**Toelichting:** Mogelijk modelantwoord: `SELECT voornaam, achternaam, specialisatie FROM trainers WHERE locatie_id = 1 AND specialisatie IS NOT NULL;` Met `AND` moeten beide voorwaarden tegelijk waar zijn. Een trainer zonder specialisatie of van een andere locatie valt dan af.
|
||||||
|
|
||||||
|
## Pagina: 4.3 NULL
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Een waarde die geen waarde is
|
||||||
|
|
||||||
|
Terug naar de vraag van de vestigingsmanager: leden zonder telefoonnummer. Wat staat er in de database als iemand geen nummer heeft opgegeven? Geen nul. Geen lege string. Er staat iets anders: `NULL`.
|
||||||
|
|
||||||
|
`NULL` is de afwezigheid van een waarde. Het is geen getal, geen tekst en geen spatie. Het betekent: er is geen informatie. In fitworks kom je `NULL` op meerdere plekken tegen, bijvoorbeeld bij `tussenvoegsel`, `telefoon` en `specialisatie`.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Waarom werkt = NULL niet
|
||||||
|
|
||||||
|
Dit is een veelgemaakte fout:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon = NULL;
|
||||||
|
|
||||||
|
De query wordt uitgevoerd zonder foutmelding, maar geeft nul resultaten terug. In SQL is `NULL` geen gewone waarde die je kunt vergelijken. De uitdrukking `NULL = NULL` is niet waar, maar onbekend. En een onbekende vergelijking telt niet als waar.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** De juiste aanpak
|
||||||
|
|
||||||
|
Gebruik speciale `NULL`-controles:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon IS NULL;
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam
|
||||||
|
FROM leden
|
||||||
|
WHERE telefoon IS NOT NULL;
|
||||||
|
|
||||||
|
`IS NULL` is de enige correcte manier om te controleren of een kolom geen waarde bevat. `IS NOT NULL` doet het omgekeerde.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_03_null_afhandeling.png
|
||||||
|
**Titel:** NULL-afhandeling
|
||||||
|
**Onderschrift:** Vergelijking tussen `= NULL` en `IS NULL`.
|
||||||
|
**Alt:** Diagram dat het verschil laat zien tussen = NULL en IS NULL.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Wat gaat er mis als je dit negeert
|
||||||
|
|
||||||
|
Het probleem met `= NULL` is verraderlijk: de query geeft geen foutmelding en lijkt normaal te werken, maar geeft stilletjes een leeg resultaat. Daardoor kan een rapport foutieve conclusies oproepen, bijvoorbeeld dat alle leden een telefoonnummer hebben ingevuld.
|
||||||
|
|
||||||
|
Onthoud: `NULL` vergelijk je nooit met `=`. Altijd met `IS NULL` of `IS NOT NULL`.
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Schrijf de query die alle lestypes toont waarvan de beschrijving niet is ingevuld.
|
||||||
|
**Toelichting:** Modelantwoord: `SELECT naam, duur_minuten, niveau FROM lestypes WHERE beschrijving IS NULL;`
|
||||||
|
|
||||||
|
## Pagina: 4.4 ORDER BY en DISTINCT
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** ORDER BY
|
||||||
|
|
||||||
|
De database geeft rijen terug in de volgorde die hem uitkomt. Als je een ledenlijst wilt sorteren op inschrijfdatum, heb je `ORDER BY` nodig.
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY lid_sinds;
|
||||||
|
|
||||||
|
Standaard sorteert `ORDER BY` oplopend. Dat heet `ASC`. Wil je de nieuwste inschrijvingen bovenaan, gebruik dan `DESC`:
|
||||||
|
|
||||||
|
SELECT voornaam, achternaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY lid_sinds DESC;
|
||||||
|
|
||||||
|
Je kunt ook op meerdere kolommen sorteren:
|
||||||
|
|
||||||
|
SELECT achternaam, voornaam, lid_sinds
|
||||||
|
FROM leden
|
||||||
|
ORDER BY achternaam, voornaam;
|
||||||
|
|
||||||
|
`ORDER BY` staat altijd als laatste in de query, dus na `WHERE` als die ook aanwezig is.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_04_order_by_distinct.png
|
||||||
|
**Titel:** ORDER BY en DISTINCT
|
||||||
|
**Onderschrift:** Sorteren van resultaten en verwijderen van duplicaten.
|
||||||
|
**Alt:** Diagram dat laat zien hoe ORDER BY sorteert en DISTINCT duplicaten verwijdert.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** DISTINCT
|
||||||
|
|
||||||
|
Wil je alleen unieke specialisaties zien, dan gebruik je `DISTINCT`:
|
||||||
|
|
||||||
|
SELECT DISTINCT specialisatie
|
||||||
|
FROM trainers;
|
||||||
|
|
||||||
|
`DISTINCT` zet je direct na `SELECT`. Het verwijdert rijen die in alle geselecteerde kolommen identiek zijn. `SELECT DISTINCT specialisatie, voornaam` geeft dus unieke combinaties van specialisatie en voornaam, niet per se unieke specialisaties.
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Welke query schrijf je om alle abonnementsnamen met maandprijs te tonen, gesorteerd van duurste naar goedkoopste? Wat verandert er als je DISTINCT toevoegt?
|
||||||
|
**Toelichting:** Modelantwoord: `SELECT naam, prijs_per_maand FROM abonnementen ORDER BY prijs_per_maand DESC;` `DISTINCT` verandert hier niets zolang namen uniek zijn. Het effect hangt af van de geselecteerde kolommen; DISTINCT werkt op de combinatie van alle geselecteerde velden.
|
||||||
|
|
||||||
|
## Pagina: 4.5 Kolomaliassen en literals
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Kolomaliassen
|
||||||
|
|
||||||
|
Met een alias geef je een kolom in het resultaat een andere naam, zonder de tabel te veranderen.
|
||||||
|
|
||||||
|
SELECT voornaam AS Voornaam,
|
||||||
|
achternaam AS Achternaam,
|
||||||
|
lid_sinds AS 'Lid sinds'
|
||||||
|
FROM leden;
|
||||||
|
|
||||||
|
Het resultaat toont leesbaardere kolomkoppen. De alias bestaat alleen in dit queryresultaat. Als een alias een spatie bevat, zet je hem tussen enkele aanhalingstekens.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_05_aliassen_literals.png
|
||||||
|
**Titel:** Aliassen en literals
|
||||||
|
**Onderschrift:** Kolomaliassen maken queryresultaten leesbaarder.
|
||||||
|
**Alt:** Voorbeeld van aliassen en literals in een SQL-resultaat.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Literals
|
||||||
|
|
||||||
|
Een literal is een vaste waarde die je direct in een query schrijft. Dat kan tekst of een getal zijn dat voor elke rij hetzelfde blijft.
|
||||||
|
|
||||||
|
SELECT voornaam,
|
||||||
|
achternaam,
|
||||||
|
'FitWorks lid' AS type
|
||||||
|
FROM leden
|
||||||
|
WHERE actief = 1;
|
||||||
|
|
||||||
|
Zo krijgt elke rij een extra kolom `type` met dezelfde vaste waarde.
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Schrijf een query die de naam en maandprijs van alle abonnementen toont, met als kolomkoppen Abonnementsnaam en Maandprijs. Sorteer op prijs, laagste eerst.
|
||||||
|
**Toelichting:** Modelantwoord: `SELECT naam AS Abonnementsnaam, prijs_per_maand AS Maandprijs FROM abonnementen ORDER BY prijs_per_maand;`
|
||||||
|
|
||||||
|
## Pagina: 4.6 CONCAT en CAST
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** CONCAT
|
||||||
|
|
||||||
|
Bij FitWorks zijn voornaam en achternaam in aparte kolommen opgeslagen. Wil je een volledige naam als een kolom tonen, dan gebruik je `CONCAT`:
|
||||||
|
|
||||||
|
SELECT CONCAT(voornaam, ' ', achternaam) AS volledige_naam
|
||||||
|
FROM leden;
|
||||||
|
|
||||||
|
Sommige leden hebben ook een tussenvoegsel. Dan helpt `COALESCE` om `NULL` op te vangen:
|
||||||
|
|
||||||
|
SELECT CONCAT(
|
||||||
|
voornaam, ' ',
|
||||||
|
COALESCE(CONCAT(tussenvoegsel, ' '), ''),
|
||||||
|
achternaam
|
||||||
|
) AS volledige_naam
|
||||||
|
FROM leden;
|
||||||
|
|
||||||
|
`COALESCE` vervangt `NULL` door een standaardwaarde, hier een lege string.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** CAST
|
||||||
|
|
||||||
|
Elke waarde in een database heeft een datatype. Soms wil je een waarde als een ander type gebruiken. Daarvoor is `CAST`.
|
||||||
|
|
||||||
|
SELECT naam,
|
||||||
|
CONCAT('Prijs: ', CAST(prijs_per_maand AS CHAR)) AS prijsweergave
|
||||||
|
FROM abonnementen;
|
||||||
|
|
||||||
|
`CAST(prijs_per_maand AS CHAR)` zet een getal om naar tekst, zodat je het veilig kunt combineren met andere tekst.
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/hoofdstuk_04_scherm_06_concatenatie_cast.png
|
||||||
|
**Titel:** CONCAT en CAST
|
||||||
|
**Onderschrift:** Kolommen samenvoegen en datatypes omzetten.
|
||||||
|
**Alt:** Diagram dat laat zien hoe CONCAT waarden samenvoegt en CAST datatypes omzet.
|
||||||
|
|
||||||
|
### Tabel
|
||||||
|
**Titel:** Veelgebruikte CAST-conversies
|
||||||
|
**Top header:** Aan
|
||||||
|
**Left header:** Uit
|
||||||
|
| Van | Naar | Gebruik |
|
||||||
|
|-----|------|---------|
|
||||||
|
| Getal | Tekst | `CAST(prijs_per_maand AS CHAR)` |
|
||||||
|
| Tekst | Geheel getal | `CAST('42' AS UNSIGNED)` |
|
||||||
|
| Datum | Tekst | `CAST(geboortedatum AS CHAR)` |
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Schrijf een query die de volledige naam van elke trainer toont in een kolom genaamd Trainer.
|
||||||
|
**Toelichting:** Modelantwoord: `SELECT CONCAT(voornaam, ' ', achternaam) AS Trainer FROM trainers;`
|
||||||
|
|
||||||
|
## Pagina: Quiz
|
||||||
|
|
||||||
|
### Meerkeuze
|
||||||
|
**Vraag:** Je wilt alle betalingen zien met status `mislukt` of `openstaand`, gesorteerd van hoog naar laag bedrag. Welke query is correct?
|
||||||
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status = 'mislukt' OR 'openstaand' ORDER BY bedrag DESC;`
|
||||||
|
- [x] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status IN ('mislukt', 'openstaand') ORDER BY bedrag DESC;`
|
||||||
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status IN ('mislukt', 'openstaand') ORDER BY bedrag ASC;`
|
||||||
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status = 'mislukt' OR status = 'openstaand' ORDER BY bedrag ASC;`
|
||||||
|
**Toelichting:** Optie B is correct. `IN` maakt de statusfilter compact en `ORDER BY bedrag DESC` sorteert van hoog naar laag. Optie A is syntactisch fout, en opties C en D sorteren in de verkeerde richting.
|
||||||
|
|
||||||
|
### Meerkeuze
|
||||||
|
**Vraag:** Welke query geeft correct alle leden terug die geen tussenvoegsel hebben?
|
||||||
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel = NULL;`
|
||||||
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel = '';`
|
||||||
|
- [x] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel IS NULL;`
|
||||||
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel IS NOT NULL;`
|
||||||
|
**Toelichting:** Optie C is correct. `NULL` vergelijk je niet met `=`, maar met `IS NULL`. Een lege string is bovendien iets anders dan `NULL`.
|
||||||
|
|
||||||
|
### Meerkeuze
|
||||||
|
**Vraag:** Een collega ziet met `SELECT DISTINCT specialisatie, voornaam FROM trainers;` nog steeds herhaalde waarden in `specialisatie`. Wat is de oorzaak?
|
||||||
|
- [ ] `DISTINCT` werkt alleen op de eerste geselecteerde kolom.
|
||||||
|
- [x] `DISTINCT` verwijdert dubbele rijen op basis van alle geselecteerde kolommen tegelijk.
|
||||||
|
- [ ] `DISTINCT` moet altijd gecombineerd worden met `ORDER BY`.
|
||||||
|
- [ ] `DISTINCT` werkt niet op tekstkolommen zoals `specialisatie`.
|
||||||
|
**Toelichting:** `DISTINCT` kijkt naar de combinatie van alle geselecteerde kolommen. Twee trainers met dezelfde specialisatie maar een andere voornaam zijn dus nog steeds twee unieke rijen. Voor alleen unieke specialisaties moet je alleen die kolom selecteren.
|
||||||
|
|
||||||
|
## Pagina: Samenvatting en vooruitblik
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Samenvatting
|
||||||
|
|
||||||
|
`SELECT` en `FROM` vormen het hart van elke query. Met `WHERE` filter je rijen, met `ORDER BY` sorteer je resultaten en met `DISTINCT` verwijder je duplicaten.
|
||||||
|
|
||||||
|
`NULL` is de afwezigheid van een waarde. Je filtert erop met `IS NULL` en `IS NOT NULL`, nooit met `=`.
|
||||||
|
|
||||||
|
Kolomaliassen met `AS` maken resultaten leesbaarder. Literals voegen vaste waarden toe. `CONCAT` plakt waarden aan elkaar en `CAST` zet waarden om naar een ander datatype.
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Vooruitblik
|
||||||
|
|
||||||
|
Je hebt nu de basis in handen om de fitworks-database te bevragen. In hoofdstuk 5 ga je verder met tekstbewerkingen, zoals `LIKE` voor zoeken op patronen, en met datumfuncties. Je gaat de data dan niet alleen ophalen, maar er echt mee werken.
|
||||||
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,219 @@
|
|||||||
|
# Instructie Voor AI-Model: OnlineAcademy Markdown
|
||||||
|
|
||||||
|
Gebruik onderstaande instructie letterlijk of bijna letterlijk als prompt voor een willekeurig AI-model dat content moet opleveren in het Markdown-format voor de OnlineAcademy-runner.
|
||||||
|
|
||||||
|
## Aanbevolen prompt
|
||||||
|
|
||||||
|
Je schrijft content in een strikt Markdown-template voor import naar een OnlineAcademy-automatisering. Volg onderstaande regels exact.
|
||||||
|
|
||||||
|
Doel:
|
||||||
|
- Lever alleen geldige Markdown op.
|
||||||
|
- Gebruik exact de koppen en veldnamen uit dit format.
|
||||||
|
- Voeg geen uitleg, geen inleiding, geen analyse en geen code fences toe.
|
||||||
|
- Geef dus alleen het uiteindelijke Markdown-document terug.
|
||||||
|
|
||||||
|
Algemene structuur:
|
||||||
|
- Optioneel bovenaan frontmatter:
|
||||||
|
- `---`
|
||||||
|
- `training: Naam van de training`
|
||||||
|
- `---`
|
||||||
|
- Daarna één of meer modules:
|
||||||
|
- `# Module: <titel>`
|
||||||
|
- Binnen elke module één of meer pagina's:
|
||||||
|
- `## Pagina: <titel>`
|
||||||
|
- Binnen elke pagina één of meer blokken:
|
||||||
|
- `### Tekst`
|
||||||
|
- `### Kop`
|
||||||
|
- `### Quote`
|
||||||
|
- `### Tabel`
|
||||||
|
- `### Afbeelding`
|
||||||
|
- `### Meerkeuze`
|
||||||
|
- `### Multi antwoord`
|
||||||
|
- `### Open vraag`
|
||||||
|
- `### Koppelvraag`
|
||||||
|
|
||||||
|
Gebruik alleen deze bloktypes. Verzin geen andere blokkoppen.
|
||||||
|
|
||||||
|
Veldnamen moeten exact zo worden geschreven:
|
||||||
|
- `**Titel:**`
|
||||||
|
- `**Tekst:**`
|
||||||
|
- `**Auteur:**`
|
||||||
|
- `**Top header:**`
|
||||||
|
- `**Left header:**`
|
||||||
|
- `**Bestand:**`
|
||||||
|
- `**Onderschrift:**`
|
||||||
|
- `**Alt:**`
|
||||||
|
- `**Vraag:**`
|
||||||
|
- `**Instructie:**`
|
||||||
|
- `**Toelichting:**`
|
||||||
|
|
||||||
|
Belangrijke inhoudsregels:
|
||||||
|
- Gebruik voor een meerkeuze- of multi-antwoordvraag antwoordregels in exact dit formaat:
|
||||||
|
- `- [x] Correct antwoord`
|
||||||
|
- `- [ ] Fout antwoord`
|
||||||
|
- Bij `Meerkeuze` moet exact 1 antwoord gemarkeerd zijn met `[x]`.
|
||||||
|
- Bij `Multi antwoord` moeten 1 of meer antwoorden gemarkeerd zijn met `[x]`.
|
||||||
|
- Bij `Multi antwoord` mogen maximaal 10 antwoorden worden gebruikt.
|
||||||
|
- Bij `Meerkeuze` en `Multi antwoord` moeten minimaal 2 antwoorden aanwezig zijn.
|
||||||
|
- Bij `Open vraag`, `Meerkeuze` en `Multi antwoord` mag de vraag maximaal 350 karakters lang zijn.
|
||||||
|
- Bij `Open vraag`, `Meerkeuze`, `Multi antwoord` en `Koppelvraag` mag de toelichting maximaal 999 karakters lang zijn.
|
||||||
|
- Bij `Tabel` mogen maximaal 5 kolommen en maximaal 20 rijen worden gebruikt.
|
||||||
|
- Bij `Tabel` moet `**Top header:**` of `Aan` of `Uit` zijn.
|
||||||
|
- Bij `Tabel` moet `**Left header:**` of `Aan` of `Uit` zijn.
|
||||||
|
- Bij `Afbeelding` moet `**Bestand:**` altijd aanwezig zijn met een relatief pad, bijvoorbeeld `afbeeldingen/diagram-1.png`.
|
||||||
|
- Gebruik alleen afbeeldingen die expliciet zijn opgegeven in de broninformatie.
|
||||||
|
- Laat optionele velden weg als er geen waarde is. Voeg dan geen lege placeholdertekst toe.
|
||||||
|
|
||||||
|
Opmaakregels:
|
||||||
|
- Gebruik geen codeblokken met backticks.
|
||||||
|
- Gebruik geen HTML.
|
||||||
|
- Gebruik geen tabellen buiten een `### Tabel` blok.
|
||||||
|
- Gebruik geen geneste module- of paginakoppen.
|
||||||
|
- Gebruik geen extra commentaarregels.
|
||||||
|
- Houd de volgorde logisch: module -> pagina -> blokken.
|
||||||
|
- Lever complete inhoud op, geen notities zoals `nog invullen`.
|
||||||
|
|
||||||
|
Syntax per blok:
|
||||||
|
|
||||||
|
Tekstblok:
|
||||||
|
`### Tekst`
|
||||||
|
Optioneel:
|
||||||
|
`**Titel:** <titel>`
|
||||||
|
Daaronder vrije Markdown-tekst.
|
||||||
|
|
||||||
|
Kopblok:
|
||||||
|
`### Kop`
|
||||||
|
Daaronder alleen de koptekst.
|
||||||
|
|
||||||
|
Quoteblok:
|
||||||
|
`### Quote`
|
||||||
|
`**Tekst:** <quote>`
|
||||||
|
Optioneel:
|
||||||
|
`**Auteur:** <naam>`
|
||||||
|
|
||||||
|
Tabelblok:
|
||||||
|
`### Tabel`
|
||||||
|
Optioneel:
|
||||||
|
`**Titel:** <titel>`
|
||||||
|
`**Top header:** Aan of Uit`
|
||||||
|
`**Left header:** Aan of Uit`
|
||||||
|
Daarna een normale Markdown-tabel.
|
||||||
|
|
||||||
|
Afbeeldingsblok:
|
||||||
|
`### Afbeelding`
|
||||||
|
`**Bestand:** afbeeldingen/bestandsnaam.png`
|
||||||
|
Optioneel:
|
||||||
|
`**Titel:** <titel>`
|
||||||
|
`**Onderschrift:** <onderschrift>`
|
||||||
|
`**Alt:** <alternatieve tekst>`
|
||||||
|
|
||||||
|
Meerkeuzeblok:
|
||||||
|
`### Meerkeuze`
|
||||||
|
`**Vraag:** <vraag>`
|
||||||
|
Daarna antwoordregels met `- [x]` of `- [ ]`
|
||||||
|
Afsluiten met:
|
||||||
|
`**Toelichting:** <uitleg>`
|
||||||
|
|
||||||
|
Multi-antwoordblok:
|
||||||
|
`### Multi antwoord`
|
||||||
|
`**Vraag:** <vraag>`
|
||||||
|
Daarna antwoordregels met `- [x]` of `- [ ]`
|
||||||
|
Afsluiten met:
|
||||||
|
`**Toelichting:** <uitleg>`
|
||||||
|
|
||||||
|
Open-vraagblok:
|
||||||
|
`### Open vraag`
|
||||||
|
`**Vraag:** <vraag>`
|
||||||
|
`**Toelichting:** <modelantwoord of uitleg>`
|
||||||
|
|
||||||
|
Koppelvraagblok:
|
||||||
|
`### Koppelvraag`
|
||||||
|
`**Instructie:** <opdrachttekst>`
|
||||||
|
Daarna koppelregels in exact dit formaat:
|
||||||
|
`- linker item = rechter item`
|
||||||
|
Afsluiten met:
|
||||||
|
`**Toelichting:** <uitleg>`
|
||||||
|
|
||||||
|
Gebruik dit voorbeeldformat:
|
||||||
|
|
||||||
|
---
|
||||||
|
training: Voorbeeldtraining
|
||||||
|
---
|
||||||
|
|
||||||
|
# Module: Moduletitel
|
||||||
|
|
||||||
|
## Pagina: Paginatitel
|
||||||
|
|
||||||
|
### Tekst
|
||||||
|
**Titel:** Optionele titel
|
||||||
|
|
||||||
|
Vrije tekst met **vet**, *cursief* en lijsten.
|
||||||
|
|
||||||
|
### Kop
|
||||||
|
Tussenkop
|
||||||
|
|
||||||
|
### Quote
|
||||||
|
**Tekst:** Dit is een quote.
|
||||||
|
**Auteur:** Auteur Naam
|
||||||
|
|
||||||
|
### Tabel
|
||||||
|
**Titel:** Overzicht
|
||||||
|
**Top header:** Aan
|
||||||
|
**Left header:** Uit
|
||||||
|
| Kolom A | Kolom B |
|
||||||
|
|---------|---------|
|
||||||
|
| Waarde 1 | Waarde 2 |
|
||||||
|
| Waarde 3 | Waarde 4 |
|
||||||
|
|
||||||
|
### Afbeelding
|
||||||
|
**Bestand:** afbeeldingen/voorbeeld.png
|
||||||
|
**Titel:** Voorbeeldafbeelding
|
||||||
|
**Onderschrift:** Korte toelichting
|
||||||
|
**Alt:** Beschrijving van de afbeelding
|
||||||
|
|
||||||
|
### Meerkeuze
|
||||||
|
**Vraag:** Welke optie is juist?
|
||||||
|
- [ ] Antwoord A
|
||||||
|
- [x] Antwoord B
|
||||||
|
- [ ] Antwoord C
|
||||||
|
**Toelichting:** Antwoord B is juist omdat ...
|
||||||
|
|
||||||
|
### Multi antwoord
|
||||||
|
**Vraag:** Welke opties zijn juist?
|
||||||
|
- [x] Optie 1
|
||||||
|
- [x] Optie 2
|
||||||
|
- [ ] Optie 3
|
||||||
|
**Toelichting:** Optie 1 en 2 zijn juist omdat ...
|
||||||
|
|
||||||
|
### Open vraag
|
||||||
|
**Vraag:** Leg in eigen woorden uit wat het verschil is.
|
||||||
|
**Toelichting:** Een goed antwoord benoemt ...
|
||||||
|
|
||||||
|
### Koppelvraag
|
||||||
|
**Instructie:** Koppel de juiste begrippen.
|
||||||
|
- Begrip 1 = Uitleg 1
|
||||||
|
- Begrip 2 = Uitleg 2
|
||||||
|
**Toelichting:** Dit zijn de correcte combinaties.
|
||||||
|
|
||||||
|
Extra eindcontrole voordat je antwoord geeft:
|
||||||
|
- Heeft elke pagina minimaal 1 blok?
|
||||||
|
- Heeft elke module minimaal 1 pagina?
|
||||||
|
- Zijn alle koppen exact gespeld?
|
||||||
|
- Heeft elke afbeelding een `Bestand`?
|
||||||
|
- Heeft elke meerkeuzevraag exact 1 correct antwoord?
|
||||||
|
- Heeft elke multi-antwoordvraag minimaal 1 correct antwoord en maximaal 10 antwoorden?
|
||||||
|
- Blijft elke tabel binnen 5 kolommen en 20 rijen?
|
||||||
|
- Geef alleen het definitieve Markdown-document terug.
|
||||||
|
|
||||||
|
## Praktisch gebruik
|
||||||
|
|
||||||
|
Voeg boven deze prompt eventueel nog brontekst toe zoals:
|
||||||
|
- doelgroep
|
||||||
|
- leerdoelen
|
||||||
|
- ruwe inhoud
|
||||||
|
- lijst met toegestane afbeeldingsbestanden
|
||||||
|
- gewenste hoofdstukindeling
|
||||||
|
|
||||||
|
Vraag het model daarna expliciet:
|
||||||
|
|
||||||
|
`Zet onderstaande inhoud om naar exact dit OnlineAcademy-Markdown-format. Geef alleen de definitieve Markdown terug.`
|
||||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"training": "Pilot Multi Chapter Run",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"title": "Hoofdstuk Alfa",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Alfa 1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Alfa 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hoofdstuk Bravo",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Bravo 1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Bravo 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bravo 2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Bravo 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bravo 3",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Bravo 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hoofdstuk Charlie",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Charlie 1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Charlie 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Charlie 2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Charlie 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hoofdstuk Delta",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Delta 1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Delta 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Delta 2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Delta 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Delta 3",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Delta 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Delta 4",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Delta 4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"training": "Pilot Chapter Run",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"title": "Eerste hoofdstuk",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Intro hoofdstuk 1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Hoofdstuk 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tweede hoofdstuk",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"title": "Intro hoofdstuk 2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"summary": "Hoofdstuk 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 152 KiB |