Håndtering af databasemigreringer er en stor udfordring i ethvert softwareprojekt. Heldigvis kommer Django fra version 1.7 med en indbygget migreringsramme. Rammen er meget kraftfuld og nyttig til at styre ændringer i databaser. Men den fleksibilitet, som rammen gav, krævede nogle kompromiser. For at forstå begrænsningerne ved Django-migreringer skal du løse et velkendt problem:at oprette et indeks i Django uden nedetid.
I dette selvstudie lærer du:
- Hvordan og hvornår Django genererer nye migreringer
- Sådan inspicerer du de kommandoer, som Django genererer for at udføre migreringer
- Sådan modificerer du migreringer sikkert, så de passer til dine behov
Denne selvstudie på mellemniveau er designet til læsere, der allerede er fortrolige med Django-migreringer. For en introduktion til dette emne, tjek Django Migrations:A Primer.
Gratis bonus: Klik her for at få gratis adgang til yderligere Django-tutorials og ressourcer, du kan bruge til at uddybe dine Python-webudviklingsfærdigheder.
Problemet med at oprette et indeks i Django-migreringer
En almindelig ændring, der normalt bliver nødvendig, når de data, der er lagret af din applikation vokser, er at tilføje et indeks. Indekser bruges til at fremskynde forespørgsler og få din app til at føles hurtig og lydhør.
I de fleste databaser kræver tilføjelse af et indeks en eksklusiv lås på bordet. En eksklusiv lås forhindrer datamodifikationsoperationer (DML) såsom UPDATE
, INSERT
og DELETE
, mens indekset oprettes.
Låse opnås implicit af databasen, når der udføres visse operationer. For eksempel, når en bruger logger ind på din app, opdaterer Django last_login
feltet i auth_user
bord. For at udføre opdateringen skal databasen først have en lås på rækken. Hvis rækken i øjeblikket låses af en anden forbindelse, får du muligvis en databaseundtagelse.
Låsning af en tabel kan udgøre et problem, når det er nødvendigt at holde systemet tilgængeligt under migreringer. Jo større tabellen er, jo længere tid kan det tage at oprette indekset. Jo længere tid det tager at oprette indekset, jo længere tid er systemet utilgængeligt eller reagerer ikke på brugerne.
Nogle databaseleverandører giver mulighed for at oprette et indeks uden at låse tabellen. For eksempel, for at oprette et indeks i PostgreSQL uden at låse en tabel, kan du bruge CONCURRENTLY
søgeord:
CREATE INDEX CONCURRENTLY ix ON table (column);
I Oracle er der en ONLINE
mulighed for at tillade DML-operationer på bordet, mens indekset oprettes:
CREATE INDEX ix ON table (column) ONLINE;
Ved generering af migreringer vil Django ikke bruge disse specielle søgeord. Kørs migreringen som den er, får databasen en eksklusiv lås på bordet og forhindrer DML-operationer, mens indekset oprettes.
Oprettelse af et indeks samtidigt har nogle forbehold. Det er vigtigt at forstå de problemer, der er specifikke for din database-backend på forhånd. For eksempel er en advarsel i PostgreSQL, at det tager længere tid at oprette et indeks samtidig, fordi det kræver en ekstra tabelscanning.
I dette selvstudie skal du bruge Django-migreringer til at oprette et indeks på en stor tabel uden at forårsage nedetid.
Bemærk: For at følge denne vejledning anbefales det, at du bruger en PostgreSQL-backend, Django 2.x og Python 3.
Det er også muligt at følge med andre database backends. På steder, hvor SQL-funktioner, der er unikke for PostgreSQL, bruges, skal du ændre SQL'en, så den matcher din database-backend.
Opsætning
Du kommer til at bruge et opdigtet Sale
model i en app kaldet app
. I en virkelig situation, modeller som Sale
er hovedtabellerne i databasen, og de vil normalt være meget store og gemme en masse data:
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
)
charged_amount = models.PositiveIntegerField()
For at oprette tabellen skal du generere den indledende migrering og anvende den:
$ python manage.py makemigrations
Migrations for 'app':
app/migrations/0001_initial.py
- Create model Sale
$ python manage migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0001_initial... OK
Efter et stykke tid bliver salgsbordet meget stort, og brugerne begynder at brokke sig over langsommelighed. Mens du overvågede databasen, bemærkede du, at mange forespørgsler bruger sold_at
kolonne. For at fremskynde tingene, beslutter du dig for, at du har brug for et indeks på kolonnen.
For at tilføje et indeks på sold_at
, foretager du følgende ændring af modellen:
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
)
charged_amount = models.PositiveIntegerField()
Hvis du kører denne migrering, som den er, vil Django oprette indekset på bordet, og det vil være låst, indtil indekset er gennemført. Det kan tage et stykke tid at oprette et indeks på en meget stor tabel, og du vil gerne undgå nedetid.
På et lokalt udviklingsmiljø med et lille datasæt og meget få forbindelser kan denne migrering føles øjeblikkelig. På store datasæt med mange samtidige forbindelser kan det dog tage et stykke tid at få en lås og oprette indekset.
I de næste trin skal du ændre migreringer oprettet af Django for at oprette indekset uden at forårsage nedetid.
Falsk migration
Den første tilgang er at oprette indekset manuelt. Du vil generere migrationen, men du vil faktisk ikke lade Django anvende den. I stedet vil du køre SQL manuelt i databasen og derefter få Django til at tro, at migreringen er fuldført.
Generer først migreringen:
$ python manage.py makemigrations --name add_index_fake
Migrations for 'app':
app/migrations/0002_add_index_fake.py
- Alter field sold_at on sale
Brug sqlmigrate
kommando for at se den SQL Django vil bruge til at udføre denne migrering:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
Du vil oprette indekset uden at låse tabellen, så du skal ændre kommandoen. Tilføj koden CONCURRENTLY
søgeord og udfør i databasen:
app=# CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
CREATE INDEX
Bemærk, at du udførte kommandoen uden BEGIN
og COMMIT
dele. Udeladelse af disse nøgleord vil udføre kommandoerne uden en databasetransaktion. Vi vil diskutere databasetransaktioner senere i artiklen.
Efter du har udført kommandoen, hvis du prøver at anvende migreringer, vil du få følgende fejlmeddelelse:
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake...Traceback (most recent call last):
File "venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
psycopg2.ProgrammingError: relation "app_sale_sold_at_b9438ae4" already exists
Django klager over, at indekset allerede eksisterer, så det kan ikke fortsætte med migreringen. Du har lige oprettet indekset direkte i databasen, så nu skal du få Django til at tro, at migreringen allerede var anvendt.
Sådan forfalsker du en migration
Django giver en indbygget måde at markere migreringer som udførte uden faktisk at udføre dem. For at bruge denne mulighed skal du indstille --fake
flag, når du anvender migreringen:
$ python manage.py migrate --fake
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake... FAKED
Django rejste ikke en fejl denne gang. Faktisk anvendte Django ikke rigtig nogen migration. Det har lige markeret det som udført (eller FAKED
).
Her er nogle problemer, du skal overveje, når du forfalsker migrationer:
-
Den manuelle kommando skal svare til den SQL, der genereres af Django: Du skal sikre dig, at den kommando, du udfører, svarer til den SQL, der genereres af Django. Brug
sqlmigrate
at producere SQL-kommandoen. Hvis kommandoerne ikke stemmer overens, kan du ende med uoverensstemmelser mellem databasen og modeltilstanden. -
Andre ikke-anvendte migreringer vil også blive forfalsket: Når du har flere uanvendte migreringer, vil de alle være falske. Før du anvender migreringer, er det vigtigt at sikre dig, at kun de migreringer, du vil forfalske, ikke er anvendt. Ellers kan du ende med uoverensstemmelser. En anden mulighed er at angive den nøjagtige migrering, du vil forfalske.
-
Der kræves direkte adgang til databasen: Du skal køre SQL-kommandoen i databasen. Dette er ikke altid en mulighed. Det er også farligt at udføre kommandoer direkte i en produktionsdatabase og bør undgås, når det er muligt.
-
Automatiske implementeringsprocesser skal muligvis justeres: Hvis du automatiserede implementeringsprocessen (ved hjælp af CI, CD eller andre automatiseringsværktøjer), skal du muligvis ændre processen til falske migreringer. Dette er ikke altid ønskeligt.
Oprydning
Før du går videre til næste afsnit, skal du bringe databasen tilbage til dens tilstand lige efter den indledende migrering. For at gøre det skal du migrere tilbage til den oprindelige migrering:
$ python manage.py migrate 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_fake... OK
Django annullerede ændringerne i den anden migrering, så nu er det sikkert også at slette filen:
$ rm app/migrations/0002_add_index_fake.py
For at sikre, at du har gjort alt rigtigt, skal du inspicere migreringerne:
$ python manage.py showmigrations app
app
[X] 0001_initial
Den indledende migrering blev anvendt, og der er ingen uanvendte migreringer.
Udfør rå SQL i migreringer
I det forrige afsnit udførte du SQL direkte i databasen og forfalskede migreringen. Dette får jobbet gjort, men der er en bedre løsning.
Django giver mulighed for at udføre rå SQL i migreringer ved hjælp af RunSQL
. Lad os prøve at bruge det i stedet for at udføre kommandoen direkte i databasen.
Generer først en ny tom migrering:
$ python manage.py makemigrations app --empty --name add_index_runsql
Migrations for 'app':
app/migrations/0002_add_index_runsql.py
Derefter skal du redigere migrationsfilen og tilføje en RunSQL
operation:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
),
]
Når du kører migreringen, får du følgende output:
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_runsql... OK
Det ser godt ud, men der er et problem. Lad os prøve at generere migreringer igen:
$ python manage.py makemigrations --name leftover_migration
Migrations for 'app':
app/migrations/0003_leftover_migration.py
- Alter field sold_at on sale
Django genererede den samme migration igen. Hvorfor gjorde den det?
Oprydning
Før vi kan besvare det spørgsmål, skal du rydde op og fortryde de ændringer, du har foretaget i databasen. Start med at slette den sidste migrering. Det blev ikke anvendt, så det er sikkert at slette:
$ rm app/migrations/0003_leftover_migration.py
Angiv derefter migreringerne for app
app:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
Den tredje migrering er væk, men den anden er anvendt. Du vil vende tilbage til staten lige efter den indledende migrering. Prøv at migrere tilbage til den oprindelige migrering, som du gjorde i forrige afsnit:
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql...Traceback (most recent call last):
NotImplementedError: You cannot reverse this operation
Django er ikke i stand til at vende migreringen.
Omvendt migration
For at vende en migrering udfører Django en modsat handling for hver operation. I dette tilfælde er det omvendte af at tilføje et indeks at droppe det. Som du allerede har set, når en migrering er reversibel, kan du annullere anvendelsen af den. Ligesom du kan bruge checkout
i Git kan du vende en migrering, hvis du udfører migrate
til en tidligere migrering.
Mange indbyggede migreringsoperationer definerer allerede en omvendt handling. For eksempel er den omvendte handling for at tilføje et felt at droppe den tilsvarende kolonne. Den omvendte handling for at oprette en model er at droppe den tilsvarende tabel.
Nogle migreringsoperationer er ikke reversible. For eksempel er der ingen omvendt handling for at fjerne et felt eller slette en model, for når først migreringen blev anvendt, er dataene væk.
I det forrige afsnit brugte du RunSQL
operation. Da du forsøgte at vende migreringen, stødte du på en fejl. Ifølge fejlen kan en af handlingerne i migreringen ikke tilbageføres. Django er som standard ikke i stand til at reversere rå SQL. Fordi Django ikke har nogen viden om, hvad der blev udført af operationen, kan den ikke generere en modsat handling automatisk.
Sådan gør man en migration reversibel
For at en migrering skal være reversibel, skal alle operationerne i den være reversible. Det er ikke muligt at vende en del af en migrering, så en enkelt ikke-reversibel handling vil gøre hele migreringen ikke-reversibel.
For at lave en RunSQL
operation reversibel, skal du angive SQL for at udføre, når operationen er reverseret. Den omvendte SQL findes i reverse_sql
argument.
Den modsatte handling til at tilføje et indeks er at droppe det. For at gøre din migrering reversibel skal du angive reverse_sql
for at slette indekset:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
reverse_sql='DROP INDEX "app_sale_sold_at_b9438ae4";',
),
]
Prøv nu at vende migreringen:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql... OK
$ python manage.py showmigrations app
app
[X] 0001_initial
[ ] 0002_add_index_runsql
Den anden migrering blev vendt, og indekset blev droppet af Django. Nu er det sikkert at slette migreringsfilen:
$ rm app/migrations/0002_add_index_runsql.py
Det er altid en god idé at give reverse_sql
. I situationer, hvor reversering af en rå SQL-handling ikke kræver nogen handling, kan du markere operationen som reversibel ved at bruge den særlige sentinel migrations.RunSQL.noop
:
migrations.RunSQL(
sql='...', # Your forward SQL here
reverse_sql=migrations.RunSQL.noop,
),
Forstå modeltilstand og databasetilstand
I dit tidligere forsøg på at oprette indekset manuelt ved hjælp af RunSQL
, genererede Django den samme migrering igen og igen, selvom indekset blev oprettet i databasen. For at forstå, hvorfor Django gjorde det, skal du først forstå, hvordan Django beslutter, hvornår der skal genereres nye migreringer.
Når Django genererer en ny migrering
I processen med at generere og anvende migreringer synkroniserer Django mellem databasens tilstand og modellernes tilstand. For eksempel, når du tilføjer et felt til en model, tilføjer Django en kolonne til tabellen. Når du fjerner et felt fra modellen, fjerner Django kolonnen fra tabellen.
For at synkronisere mellem modellerne og databasen opretholder Django en tilstand, der repræsenterer modellerne. For at synkronisere databasen med modellerne genererer Django migreringsoperationer. Migreringsoperationer oversættes til en leverandørspecifik SQL, der kan udføres i databasen. Når alle migreringsoperationer er udført, forventes databasen og modellerne at være konsistente.
For at få status for databasen, samler Django operationerne fra alle tidligere migreringer. Når den aggregerede tilstand af migrationerne ikke stemmer overens med modellernes tilstand, genererer Django en ny migrering.
I det forrige eksempel oprettede du indekset ved hjælp af rå SQL. Django vidste ikke, at du oprettede indekset, fordi du ikke brugte en velkendt migreringsoperation.
Da Django samlede alle migrationerne og sammenlignede dem med modellernes tilstand, fandt den ud af, at der manglede et indeks. Dette er grunden til, at selv efter at du har oprettet indekset manuelt, troede Django stadig, at det manglede og genererede en ny migrering til det.
Sådan adskiller du database og tilstand i migreringer
Da Django ikke er i stand til at oprette indekset, som du vil have det til, vil du give din egen SQL, men stadig lade Django vide, at du har oprettet det.
Med andre ord skal du udføre noget i databasen og give Django migreringsoperationen for at synkronisere dens interne tilstand. For at gøre det giver Django os en speciel migreringsoperation kaldet SeparateDatabaseAndState
. Denne operation er ikke velkendt og bør reserveres til særlige tilfælde som dette.
Det er meget nemmere at redigere migreringer end at skrive dem fra bunden, så start med at generere en migrering på den sædvanlige måde:
$ python manage.py makemigrations --name add_index_separate_database_and_state
Migrations for 'app':
app/migrations/0002_add_index_separate_database_and_state.py
- Alter field sold_at on sale
Dette er indholdet af migreringen genereret af Django, det samme som før:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
]
Django genererede et AlterField
handling på feltet sold_at
. Operationen vil oprette et indeks og opdatere tilstanden. Vi ønsker at beholde denne handling, men give en anden kommando til at udføre i databasen.
Endnu en gang, for at få kommandoen, skal du bruge SQL genereret af Django:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
Tilføj koden CONCURRENTLY
søgeord på det rigtige sted:
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
Derefter skal du redigere migrationsfilen og bruge SeparateDatabaseAndState
for at give din ændrede SQL-kommando til udførelse:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""", reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
Migreringsoperationen SeparateDatabaseAndState
accepterer 2 lister over operationer:
- state_operationer er operationer, der skal anvendes på den interne modeltilstand. De påvirker ikke databasen.
- databaseoperationer er operationer, der skal anvendes på databasen.
Du beholdt den oprindelige operation genereret af Django i state_operations
. Når du bruger SeparateDatabaseAndState
, det er det, du normalt vil gøre. Bemærk, at db_index=True
argument leveres til feltet. Denne migreringsoperation vil fortælle Django, at der er et indeks på feltet.
Du brugte SQL genereret af Django og tilføjede CONCURRENTLY
søgeord. Du brugte den specielle handling RunSQL
for at udføre rå SQL i migreringen.
Hvis du prøver at køre migreringen, får du følgende output:
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state...Traceback (most recent call last):
File "/venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 83, in _execute
return self.cursor.execute(sql)
psycopg2.InternalError: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
Ikke-atomare migrationer
I SQL, CREATE
, DROP
, ALTER
, og TRUNCATE
operationer omtales som Data Definition Language (DDL). I databaser, der understøtter transaktions-DDL, såsom PostgreSQL, udfører Django som standard migreringer inde i en databasetransaktion. Men ifølge fejlen ovenfor kan PostgreSQL ikke oprette et indeks samtidig inde i en transaktionsblok.
For at kunne oprette et indeks samtidigt inden for en migrering, skal du bede Django om ikke at udføre migreringen i en databasetransaktion. For at gøre det markerer du migrationen som ikke-atomisk ved at indstille atomic
til False
:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""",
reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
Når du har markeret migreringen som ikke-atomisk, kan du køre migreringen:
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state... OK
Du har lige udført migreringen uden at forårsage nedetid.
Her er nogle problemer, du skal overveje, når du bruger SeparateDatabaseAndState
:
-
Databaseoperationer skal svare til tilstandsoperationer: Uoverensstemmelser mellem databasen og modeltilstanden kan forårsage en masse problemer. Et godt udgangspunkt er at holde operationerne genereret af Django i
state_operations
og rediger outputtet afsqlmigrate
til brug idatabase_operations
. -
Ikke-atomare migrationer kan ikke rulle tilbage i tilfælde af fejl: Hvis der er en fejl under migreringen, vil du ikke være i stand til at rulle tilbage. Du skal enten rulle tilbage overflytningen eller fuldføre den manuelt. Det er en god idé at holde de operationer, der udføres inden for en ikke-atomare migration, på et minimum. Hvis du har yderligere handlinger i migreringen, skal du flytte dem til en ny migrering.
-
Migrering kan være leverandørspecifik: SQL'en genereret af Django er specifik for databasens backend, der bruges i projektet. Det fungerer muligvis med andre database-backends, men det er ikke garanteret. Hvis du har brug for at understøtte flere database-backends, skal du foretage nogle justeringer af denne tilgang.
Konklusion
Du startede denne øvelse med en stor tabel og et problem. Du ville gøre din app hurtigere for dine brugere, og du ville gøre det uden at forårsage nedetid for dem.
Ved slutningen af selvstudiet lykkedes det dig at generere og sikkert ændre en Django-migrering for at nå dette mål. Du tacklede forskellige problemer undervejs og formåede at overvinde dem ved hjælp af indbyggede værktøjer fra migreringsrammerne.
I dette selvstudie lærte du følgende:
- Hvordan Django-migreringer fungerer internt ved hjælp af model- og databasetilstand, og når nye migreringer genereres
- Sådan udføres brugerdefineret SQL i migreringer ved hjælp af
RunSQL
handling - Hvad reversible migreringer er, og hvordan man laver en
RunSQL
handling reversibel - Hvad atommigrationer er, og hvordan man ændrer standardadfærden i overensstemmelse med dine behov
- Sådan udføres komplekse migreringer sikkert i Django
Adskillelsen mellem model og databasetilstand er et vigtigt begreb. Når du først forstår det, og hvordan du bruger det, kan du overvinde mange begrænsninger ved de indbyggede migreringsoperationer. Nogle use cases, der kommer til at tænke på, omfatter tilføjelse af et indeks, der allerede var oprettet i databasen og levering af leverandørspecifikke argumenter til DDL-kommandoer.