Databasemigratie

Databasemigraties zijn geversioneerde scripts die wijzigingen in je databaseschema in de loop van de tijd bijhouden. In plaats van handmatig SQL-commando's op elke omgeving uit te voeren, zorgen migraties ervoor dat elke database — development, staging, productie — automatisch synchroon blijft.

Finch houdt toegepaste migraties bij in een wa_migration-tabel die het automatisch aanmaakt (per database), en biedt twee gerelateerde maar verschillende app-runtime commando's: migrate voor MySQL, en migrate_sqlite voor SQLite. Beide zijn app-runtime commando's — ze draaien binnen je daadwerkelijke FinchApp-instantie, niet als een kale top-level finch-vlag. Zie Finch CLI voor hoe de twee commandolagen zich tot elkaar verhouden.

Migratie-commando's

Voer deze uit via finch run --args="..." / finch serve --args="...", of rechtstreeks met dart run lib/app.dart ...:

Commando Optie Kort Beschrijving
migrate --init -i Alle openstaande MySQL-migraties in volgorde toepassen
migrate --init --sqlite Openstaande MySQL- en SQLite-migraties samen toepassen, in één aanroep
migrate --rollback -r De laatst toegepaste MySQL-migratie terugdraaien
migrate --list -l MySQL-migratiebestanden en hun toepassingsstatus weergeven
migrate_sqlite --init / --rollback / --list -i / -r / -l Dezelfde bewerkingen, beperkt tot alleen SQLite

Belangrijk: --sqlite op het migrate-commando heeft alleen effect op --init — het voert dan als gemak beide databases' openstaande migraties samen uit. Het wordt genegeerd door --rollback en --list; migrate --rollback --sqlite draait nog altijd een MySQL-migratie terug. Gebruik voor het terugdraaien of weergeven van SQLite-migraties altijd het specifieke migrate_sqlite-commando.

Het aanmaken van een nieuw migratiebestand (in tegenstelling tot het toepassen ervan) is juist een top-level finch CLI-commando — zie Creating a Migration File hieronder.

Eerste installatie

Voer --init uit om alle migraties toe te passen op een nieuwe database:

# Alleen MySQL
finch run --args="migrate --init"

# MySQL en SQLite samen
finch run --args="migrate --init --and migrate_sqlite --init"
# of, equivalent, in één migrate-aanroep:
finch run --args="migrate --init --sqlite"

# Alleen SQLite
finch run --args="migrate_sqlite --init"

Finch maakt bij de eerste uitvoering een wa_migration-tabel aan (per database) om bij te houden welke bestanden al zijn uitgevoerd. Volgende aanroepen van --init passen alleen de bestanden toe die nog niet zijn uitgevoerd.

Creating a Migration File

Het genereren van een nieuw migratiebestand is een top-level finch CLI-commando (de app hoeft hiervoor niet te draaien, aangezien het slechts een bestand opzet):

finch migrate --create --name add_books_table
# Maakt iets aan als: migrations/z1700000000000_add_books_table_migration.sql

finch migrate --create --name add_books_table --sqlite
# Maakt het SQLite-equivalent aan onder pathMigrationSQLite

De bestandsnaam krijgt het voorvoegsel z<millisecondsSinceEpoch>_ (de voorloop-z zorgt ervoor dat bestanden met een tijdstempel-voorvoegsel na eventuele niet-migratiebestanden gesorteerd blijven) en slugifieert de naam die je hebt opgegeven. Het bestand wordt geplaatst in de map pathMigrationMySQL (of pathMigrationSQLite voor SQLite), beide geconfigureerd in FinchConfigs:

FinchConfigs configs = FinchConfigs(
  pathMigrationMySQL:  pathTo('./migrations'),
  pathMigrationSQLite: pathTo('./migrations_sqlite'),
);

Of --create een .sql-bestand genereert of kant-en-klare boilerplate voor een Dart DartMigration-klasse, wordt bepaald door de instelling mysql_migrate.type / sqlite_migrate.type in je pubspec.yaml (standaard sql) — zie pubspec.yaml Configuration.

Migratiebestandsstructuur

Elk .sql-migratiebestand bestaat uit twee secties: de voorwaartse migratie (## NEW VERSION) en de terugdraaisectie (## ROLL BACK):

-- 2024-01-15 12:00:00.000
-- MySQL Migration File
-- Name: add_books_table
-- ## NEW VERSION:

CREATE TABLE IF NOT EXISTS `books` (
  `id`             INT          NOT NULL AUTO_INCREMENT,
  `title`          VARCHAR(255) NOT NULL,
  `author`         VARCHAR(255) NOT NULL,
  `published_date` DATE         NOT NULL,
  `category_id`    INT          NULL,
  PRIMARY KEY (`id`)
);

-- ## ROLL BACK:

DROP TABLE IF EXISTS `books`;

Elke migratie (bestands- of Dart-gebaseerd) draait binnen een databasetransactie: als een instructie mislukt, wordt de transactie automatisch teruggedraaid en wordt de migratie niet vastgelegd in wa_migration, zodat een gecorrigeerde versie van hetzelfde bestand wordt opgepikt door de volgende --init.

Dart-gebaseerde migraties

In plaats van SQL-bestanden kun je migraties in Dart definiëren door DartMigration te extenden. Bouw de SQL voor up()/down() op met addSql(), en stel target in om MigrationTarget.mysql of MigrationTarget.sqlite te kiezen:

import 'package:finch/mysql.dart';

class M1CreateUsers extends DartMigration {
  @override
  MigrationTarget get target => MigrationTarget.sqlite;

  M1CreateUsers() : super('m1_create_users');

  @override
  void up() {
    addSql('''
      CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        email TEXT
      );
    ''');
  }

  @override
  void down() {
    addSql('DROP TABLE IF EXISTS users;');
  }
}

Registreer migraties op de FinchApp-instantie. De naam die aan super(...) wordt doorgegeven, moet uniek zijn onder alle geregistreerde migraties — dit is wat wordt vastgelegd in wa_migration, dus als je deze later hernoemt, denkt Finch dat de migratie nooit is toegepast:

final app = FinchApp(configs: configs)
  ..registerDartMigration([
    M1CreateUsers(),
    M2InsertUsers(),
  ]);

Eenmaal geregistreerd, voeren migrate --init/--rollback/--list en hun migrate_sqlite-tegenhangers de Dart-migraties uit die overeenkomen met dat target, naast eventuele .sql-bestanden in de migratiesmap — je kunt beide stijlen vrij combineren binnen hetzelfde project.

Terugdraaien

# De laatst toegepaste MySQL-migratie ongedaan maken
finch run --args="migrate --rollback"

# De laatst toegepaste SQLite-migratie ongedaan maken — hiervoor moet je migrate_sqlite gebruiken
finch run --args="migrate_sqlite --rollback"

# De twee meest recente MySQL-migraties ongedaan maken
finch run --args="migrate --rollback 2"

Het terugdraaien voert de SQL uit de ## ROLL BACK:-sectie van het laatst toegepaste bestand uit (of de down()-methode van de laatst toegepaste Dart-migratie).

Migratiestatus weergeven

finch run --args="migrate --list"        # MySQL
finch run --args="migrate_sqlite --list" # SQLite

Dit print een tabel met elk migratiebestand, of het is uitgevoerd, en wanneer.

Migraties uitvoeren bij opstarten

Voer migraties automatisch uit als onderdeel van je opstartcommando — dit is precies wat de Docker-image van het voorbeeldproject doet (zie Docker for Finch):

# Bij het opstarten in productie (bijv. Docker CMD)
finch serve --args="migrate --init --and migrate_sqlite --init"

--and koppelt meerdere app-runtime commando's aan elkaar in één --args-string, zodat beide databases gemigreerd zijn voordat de server verzoeken begint te verwerken.

Maak altijd een back-up van je productiedatabase voordat je migraties uitvoert.