SQLite

Finch gebruikt het sqlite3-pakket voor SQLite. SQLite is een bestandsgebaseerde database — er is geen aparte databaseserver om te beheren. Alle gegevens worden opgeslagen in één enkel bestand op schijf.

SQLite is een goede keuze wanneer:

  • Je een kleine of middelgrote applicatie bouwt die geen hoge mate van concurrency nodig heeft
  • Je een deployment zonder externe afhankelijkheden wilt (geen externe databaseserver nodig)
  • Je lokaal ontwikkelt en een snelle opzet wilt

De SQLite-integratie in Finch gebruikt dezelfde MTable-, MField*- en Sqler-API als MySQL (zie MySQL voor de volledige referentie van schema en querybouwer) — dezelfde Sqler-aanroep produceert geldige SQL voor welke database DatabaseDriver ook daadwerkelijk vasthoudt, omdat DatabaseDriver tijdens runtime het onderliggende verbindingstype detecteert. De verschillen zitten in: de configuratie, de eigenschap waarmee je bij de driver kunt, en — belangrijker nog — hoe je waarden weer uitleest uit een resultset (zie Result Handling hieronder).

Configuratie

Voeg sqliteConfig toe aan FinchConfigs. De enige verplichte parameter is het pad naar het .sqlite-bestand:

FinchConfigs configs = FinchConfigs(
  sqliteConfig: FinchSqliteConfig(
    enable: true,
    filePath: env.get('SQLITE_PATH', './app.sqlite'),
  ),
);

Het bestand wordt automatisch aangemaakt als het nog niet bestaat. Het pad kan absoluut zijn of relatief ten opzichte van de werkmap.

Toegang tot de driver

// DatabaseDriver<Database> — gebruik dit voor Sqler-queries
var driver = app.sqliteDriver;

// Directe sqlite3.Database — gebruik dit alleen wanneer je low-level toegang nodig hebt
var db = app.sqliteDb;

Gebruik app.sqliteDriver in je datalaag-klassen. Geef deze door als constructorargument om controllers dun te houden:

class BooksData {
  final DatabaseDriver db;
  BooksData(this.db);

  // je querymethoden...
}

// In een controller:
var books = BooksData(app.sqliteDriver);

Een tabel definiëren

Tabelschema's worden op identieke wijze gedefinieerd als bij MySQL, met MTable en MField*. Finch importeert beide vanuit finch_mysql.dart — ze worden gedeeld tussen MySQL en SQLite:

import 'package:finch/finch_mysql.dart'; // MTable, MField* worden gedeeld door MySQL en SQLite
import 'package:finch/finch_ui.dart';    // FieldValidator

final table = MTable(
  name: 'books',
  fields: [
    MFieldInt(name: 'id', isPrimaryKey: true, isAutoIncrement: true, isNullable: false),
    MFieldVarchar(
      name: 'title',
      isNullable: false,
      validators: [
        FieldValidator.requiredField().toSimple(),
        FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
      ],
    ),
    MFieldVarchar(name: 'author', isNullable: false),
    MFieldDate(name: 'published_date', isNullable: false),
    MFieldInt(name: 'category_id', isNullable: true),
  ],
);

MFieldInt wordt automatisch gemapt naar SQLite's INTEGER wanneer de SQL wordt gegenereerd voor een SQLite-verbinding (toSQL<Sqlite>() versus toSQL<Mysql>()) — je hebt geen SQLite-specifiek veldtype nodig. Zie MySQL — Available Field Types voor de volledige lijst, inclusief MFieldBoolean (niet MFieldBool) en MFieldDecimal/MFieldFloat (er bestaat geen MFieldDouble).

Queries met Sqler

Queries worden opgebouwd met dezelfde fluent Sqler-API als bij MySQL. Finch handelt de verschillen in SQL-dialect intern af:

import 'package:finch/finch_mysql.dart';

Future<SqlDatabaseResult> getAllBooks(DatabaseDriver db) async {
  var query = Sqler()
    ..from(QField(table.name, as: 'b'))
    ..selects([
      QSelect('b.id'),
      QSelect('b.title'),
      QSelect('b.author'),
    ])
    ..orderBy(QOrder('b.id', desc: true))
    ..limit(20);

  return db.execute(query);
}

Invoegen

Sqler.insert() neemt de doeltabel en een lijst van rij-maps:

await db.execute(
  Sqler().insert(QField(table.name), [
    {'title': QVar('Dart in Action'), 'author': QVar('Alice')},
  ]),
);

Bijwerken

Gebruik .update(table), vervolgens .updateSet(field, value) per veld, en daarna .where():

await db.execute(
  Sqler()
    ..update(QField(table.name))
    ..updateSet('title', QVar('Updated Title'))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

Verwijderen

Combineer .delete() met .from():

await db.execute(
  Sqler()
    ..delete()
    ..from(QField(table.name))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

Dezelfde MTable-hulpmethoden en de repositorybasisklasse SqliteTable (die MysqlTable weerspiegelt, met deleteBy/deleteById/findById/countBy) zijn hier ook beschikbaar — zie MySQL — Table Convenience Methods voor de volledige methodenlijst; alles daar werkt ongewijzigd tegen app.sqliteDriver.

Result Handling

Dit is de ene plek waar SQLite echt verschilt van MySQL: een SQLite SqlDatabaseResult.rows is een gewone List<List<Object?>> (positionele waarden per rij) — geen objecten met een .colByName()-methode zoals de resultaatrijen van MySQL die hebben. Gebruik altijd .assoc/.assocFirst om kolommen op naam te lezen in plaats van op positie te indexeren in .rows:

var result = await getAllBooks(app.sqliteDriver);

for (var row in result.assoc) {
  var title = row['title']; // Map<String, String?> — elke waarde komt terug als een String
}

var firstRow = result.assocFirst; // Map<String, String?>? — eerste rij of null
var numRows  = result.numRows;
var newId    = result.insertId;     // laatste insert row id van sqlite3
var affected = result.affectedRows; // rijen gewijzigd door de laatste instructie

Let op: in tegenstelling tot MySQL's .assoc (dat de native Dart-typen behoudt), zetten SQLite's .assoc/.assocFirst elke waarde om naar een string (row[i]?.toString()). Parse getallen/booleans expliciet terug als je ze getypeerd nodig hebt (bijv. int.parse(row['id']!)).

Voor een pagineringsquery met een totaal-rijenaantal alias je COUNT(*) als count_records en lees je .countRecords, precies zoals bij MySQL:

var countQuery = Sqler()..from(table.qName)..addSelect(SQL.count(QField('id', as: 'count_records')));
var total = (await table.execute(driver, countQuery)).countRecords;

Migraties

SQLite-migraties worden apart bijgehouden van MySQL-migraties, met hun eigen dedicated app-runtime commando, migrate_sqlite:

# Alle openstaande SQLite-migraties toepassen
finch run --args="migrate_sqlite --init"

# Nieuw SQLite-migratiebestand aanmaken
finch migrate --create --name add_books_table --sqlite

finch migrate --init --sqlite (het gecombineerde MySQL-commando met een --sqlite-vlag) past ook openstaande SQLite-migraties toe, in dezelfde aanroep als die van MySQL — maar alleen voor --init. migrate --rollback --sqlite en migrate --list --sqlite negeren de --sqlite-vlag stilzwijgend en werken alleen op MySQL; gebruik hiervoor migrate_sqlite --rollback / migrate_sqlite --list. Zie Database Migration voor de volledige commandoreferentie en het migratiebestandsformaat.

SQLite is ideaal voor ontwikkeling, testen en kleine productie-implementaties waarvoor geen aparte databaseserver nodig is.