MongoDB

Finch gebruikt het mongo_dart-pakket voor MongoDB en verpakt dit met een kleine set conventies — FinchDBConfig, DBCollection en de DQ-querybouwer — zodat je controllers nooit ruwe connection strings hoeven te bouwen of steeds opnieuw boilerplate CRUD-code hoeven te schrijven.

Deze handleiding behandelt:

  • Verbinden en de pool configureren
  • Een collectieklasse schrijven met DBCollection
  • De ingebouwde hulpmethoden die elke collectie gratis krijgt
  • Query's en aggregatiepipelines bouwen met DQ
  • Een collectie koppelen aan een model en een controller
  • De verbindingsstatus controleren (bijv. vanuit een cronjob)

1. Configuratie

MongoDB wordt geconfigureerd via FinchDBConfig, dat wordt doorgegeven aan dbConfig in FinchConfigs:

FinchConfigs configs = FinchConfigs(
  dbConfig: FinchDBConfig(
    enable: true,
    host: env.get('MONGODB_CONNECTION', 'localhost'),
    port: env.get('MONGODB_PORT', '27017'),
    user: env.get('MONGODB_USER', 'root'),
    pass: env.get('MONGODB_PASSWORD', 'password'),
    dbName: env.get('MONGODB_NAME', 'my_app'),
    auth: env.get('MONGODB_AUTH', 'admin'), // authenticatiebron-database
    maxConnections: 10, // grootte van de connectionpool (Db.pool)
  ),
);
Field Type Beschrijving
enable bool Wanneer false, wordt app.mongoDb nooit verbonden — handig om Mongo volledig over te slaan in apps die alleen MySQL/SQLite gebruiken
host, port, user, pass, dbName String Standaard verbindingsgegevens. Let op: port is een String, geen int
auth String De MongoDB authSource-database (meestal admin)
maxConnections int Hoeveel pooled verbindingen Db.pool opent (standaard 10)

Finch bouwt de connection string voor je op:

mongodb://user:pass@host:port/dbName/?authSource=auth

Als je een veld niet expliciet doorgeeft, valt FinchDBConfig terug op het zelf uitlezen ervan uit de omgeving (MONGO_CONNECTION, MONGO_PORT, MONGO_INITDB_DATABASE, MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD, MONGO_INITDB_ROOT_AUTH) — maar het is duidelijker om je eigen env.get(...)-aanroepen door te geven zoals hierboven getoond, zodat de variabelenamen overeenkomen met de rest van je .env-bestand. Zie Configuration voor de volledige lijst met app-brede omgevingsvariabelen.

Connection pooling

Finch opent nooit één enkele MongoDB-socket. Bij het opstarten roept DBManager het volgende aan:

Db.pool(List.filled(config.maxConnections, config.link))

Dat creëert een pool van maxConnections onafhankelijke verbindingen, zodat gelijktijdige verzoeken worden bediend vanuit verschillende sockets in plaats van in de wachtrij te staan achter één verbinding. Verhoog maxConnections voor apps met veel verkeer; de standaardwaarde van 10 is voor de meeste projecten voldoende.

2. Toegang tot de database

var db = app.mongoDb; // retourneert de mongo_dart Db-instantie (de pool)

Controleer de verbindingsstatus voordat je opstartlogica of geplande taken uitvoert:

if (app.mongoDb.isConnected) {
  // veilig om queries uit te voeren
}

Dit is hetzelfde patroon dat Finch's eigen cronjobs gebruiken — zie Commands voor de cron-API.

3. DBCollection — de basisklasse voor collecties

DBCollection (uit package:finch/finch_model.dart) is een abstracte klasse die je uitbreidt voor elke MongoDB-collectie in je app. Het geeft je toegang tot de ruwe mongo_dart-collectie plus een set kant-en-klare hulpmethoden, en het maakt de collectie automatisch aan de eerste keer dat deze wordt geïnstantieerd, als die nog niet bestaat.

import 'package:finch/finch_model.dart';
import '../app.dart';
import '../models/example_model.dart';

class ExampleCollections extends DBCollection {
  ExampleCollections() : super(db: app.mongoDb, name: 'example');

  Future<ExampleModel> insertExample(ExampleModel model) async {
    var res = await collection.insert(model.toJson());
    return ExampleModel.fromJson(res);
  }

  Future<List<ExampleModel>> getAllExample({int? start, int? count}) async {
    start = (start != null && start > 0) ? start : null;
    var rows = await collection
        .modernFind(limit: count, skip: start, sort: DQ.order('_id'))
        .toList();
    return ExampleModel.fromListJson(rows);
  }
}
  • collection is de onderliggende mongo_dart DbCollection (db.collection(name)), voor alles wat de onderstaande hulpmethoden niet dekken.
  • name en db zijn de constructorargumenten die je doorgeeft aan super(...).

Ingebouwde hulpmethoden

Elke subklasse van DBCollection krijgt deze methoden zonder dat je hiervoor code hoeft te schrijven:

Method Signature Beschrijving
existId Future<bool> existId(String idField) true als er een document met die _id bestaat. Retourneert false voor een ongeldige ObjectId-string in plaats van een fout te werpen
exist Future<bool> exist(String field, Object value) true als er een document is met field == value
getCount Future<int> getCount({String? field, Object? value, Map<String,Object?>? filter}) Telt documenten, optioneel gefilterd op een enkel veld/waarde-paar of een ruwe filter-map
isEmpty / isNotEmpty Future<bool> get Snelkoppeling rond getCount() == 0
delete Future<bool> delete(String id) Verwijdert één document op basis van _id
deleteAll Future<bool> deleteAll() Verwijdert alle documenten in de collectie — gebruik met zorg
copy Future<void> copy(String id) Dupliceert een document (verwijdert _id zodat Mongo er een nieuwe toewijst)
updateField Future<void> updateField(String id, String field, Object? value) Stelt één veld in op één document, alleen als het id bestaat
updateFields Future<void> updateFields(String id, Map<String, dynamic> fields) Voegt meerdere velden samen in één document
updateAllForField Future<void> updateAllForField({required String field, required Object? value, required Map<String,Object?>? filter}) Werkt in bulk één veld bij over alle documenten die overeenkomen met filter

Voorbeeld — de ingebouwde methoden rechtstreeks gebruiken vanuit een controller zonder extra querycode te schrijven:

var col = ExampleCollections();

if (await col.exist('slug', 'my-slug')) {
  return rq.renderError(409, message: 'Slug already exists');
}

var total = await col.getCount();
var isFirstRun = await col.isEmpty;

await col.updateField(id, 'title', 'New title');
await col.delete(id);

4. Query's uitvoeren met modernFind

modernFind is de aanbevolen manier om een collectie te query'en — het accepteert een filter, sort, limit en skip in één aanroep:

// Alle documenten, nieuwste eerst
var rows = await collection
    .modernFind(sort: DQ.order('_id', true))
    .toList();

// Gefilterd, gepagineerd
var rows = await collection
    .modernFind(
      filter: where.eq('slug', 'my-slug'),
      limit: 10,
      skip: 0,
    )
    .toList();

where is de eigen selectorbouwer van mongo_dart (where.eq, where.id, ...) en werkt naast DQ.

5. De DQ-querybouwer

DQ is een statische hulpklasse (opnieuw geëxporteerd vanuit finch_model.dart) die eenvoudige Map<String, Object?> MongoDB-query-/aggregatiefragmenten produceert — het bestaat puur om query's leesbaarder te maken dan handgeschreven maps met $-voorvoegselsleutels.

Vergelijkings- en logische operatoren

DQ.eq('value')                       // 'value' — voor directe gelijkheid
DQ.gt(18)                            // { '$gt': 18 }
DQ.gte(18)                           // { '$gte': 18 }
DQ.lt(65)                            // { '$lt': 65 }
DQ.lte(65)                           // { '$lte': 65 }
DQ.hasIn(['a', 'b'])                 // { '$in': ['a', 'b'] }
DQ.hasNin(['a', 'b'])                // { '$nin': ['a', 'b'] }
DQ.and([cond1, cond2])               // { '$and': [cond1, cond2] }
DQ.or([cond1, cond2])                // { '$or': [cond1, cond2] }
DQ.field('age', DQ.gte(18))          // { 'age': { '$gte': 18 } }

Tekst matchen

DQ.like('john')                      // { '$regex': 'john', '$options': 'i' } — hoofdletterongevoelig, bevat
DQ.uncase('john')                    // { '$regex': '^john$', '$options': 'i' } — hoofdletterongevoelige exacte match

Beide escapen automatisch speciale regex-tekens in de invoer, zodat door gebruikers opgegeven zoektermen veilig rechtstreeks kunnen worden doorgegeven.

ID-hulpmiddelen

DQ.id('507f191e810c19729de860ea')    // { '_id': ObjectId(...) } — vanaf een String
DQ.oid(objectId)                     // { '_id': ObjectId(...) } — vanaf een bestaande ObjectId

Als we dit samenvoegen, een typische gefilterde zoekopdracht:

var rows = await collection
    .modernFind(
      filter: DQ.and([
        DQ.field('status', 'active'),
        DQ.field('name', DQ.like(searchTerm)),
      ]),
      sort: DQ.order('createdAt'),
      limit: 20,
    )
    .toList();

Sorteren, pagineren en tellen

DQ.order('createdAt')                // { 'createdAt': -1 } — standaard aflopend
DQ.order('createdAt', false)         // { 'createdAt': 1 }  — oplopend
DQ.sortField('createdAt', true)      // { 'createdAt': -1 } — hetzelfde, geschikt voor aggregatiestages
DQ.limit(20)                         // { '$limit': 20 }
DQ.skip(40)                          // { '$skip': 40 }
DQ.count('total')                    // { '$count': 'total' }

Aggregatiepipelines

Voor alles wat verder gaat dan een eenvoudig filter — joins, groeperen, berekende velden — bouw je een aggregatiepipeline met DQ.pipeline(...) en voer je deze uit met collection.aggregateToStream of collection.modernAggregate uit mongo_dart:

var pipeline = DQ.pipeline([
  DQ.match([
    DQ.field('status', 'active'),
  ]),
  DQ.lookup(
    from: 'users',
    localField: 'userId',
    foreignField: '_id',
    as: 'user',
  ),
  DQ.unwind(path: 'user', preserveNullAndEmptyArrays: true),
  DQ.group({
    '_id': DQ.$field('user.country'),
    'total': DQ.sum('amount'),
  }),
  DQ.sort({'total': -1}),
  DQ.limit(10),
]);

var results = await collection.aggregateToStream(pipeline).toList();

Beschikbare aggregatiestage-bouwers:

Method Stage Doel
DQ.match(List<Map>) $match Filtert documenten die de pipeline binnenkomen
DQ.group(Map) $group Groepeert documenten en berekent aggregaten
DQ.lookup(from:, localField:, foreignField:, as:) $lookup Left-outer join met een andere collectie
DQ.unwind(path:, as:, preserveNullAndEmptyArrays:) $unwind Vlakt een array-veld af tot afzonderlijke documenten
DQ.project(Map) $project Neemt uitvoervelden op/sluit ze uit/berekent ze
DQ.sort(Map) / DQ.sortList(List<Map>) / DQ.sortOne(field, desc) $sort Sorteert pipelineresultaten
DQ.limit(int) / DQ.skip(int) $limit / $skip Paginering binnen een pipeline
DQ.count(field) $count Telt documenten op die pipelinestage
DQ.sum(field) / DQ.sumQuery(query) $sum Telt een veld op (of een berekende expressie) binnen $group
DQ.cond(ifCond:, thenCond:, elseCond:) $cond Conditionele expressie
DQ.dateToString(field:, format:, timezone:) $dateToString Formatteert een datumveld als string
DQ.toDate(field) $toDate Zet een veld om naar een datum
DQ.$field(name) — Zet een $ vóór een veldnaam (bijv. '$name') voor gebruik binnen expressies

6. Model

Finch-modellen zijn gewone Dart-klassen met toJson/fromJson — er komt geen codegeneratie aan te pas, zodat je volledige controle hebt over de vorm van het document:

class ExampleModel {
  String? id;
  String title;
  String slug;

  ExampleModel({this.id, required this.title, required this.slug});

  Map<String, dynamic> toJson() => {
    if (id != null) '_id': id,
    'title': title,
    'slug': slug,
  };

  factory ExampleModel.fromJson(Map<String, dynamic> json) => ExampleModel(
    id: json['_id']?.toString(),
    title: json['title'] ?? '',
    slug: json['slug'] ?? '',
  );

  static List<ExampleModel> fromListJson(List<Map<String, dynamic>> list) =>
      list.map(ExampleModel.fromJson).toList();
}

7. De collectie gebruiken in een controller

class HomeController extends Controller {
  Future<String> exampleDatabase() async {
    var col = ExampleCollections();

    if (rq.isPost) {
      var model = ExampleModel(
        title: rq.get<String>('title', def: ''),
        slug: rq.get<String>('slug', def: ''),
      );
      await col.insertExample(model);
      return rq.redirect('/example/database');
    }

    var items = await col.getAllExample(count: 20);
    rq.addParam('items', items.map((e) => e.toJson()).toList());
    return rq.renderView(path: 'example/database');
  }
}

8. MongoDB gebruiken vanuit een cronjob

Omdat app.mongoDb één gedeelde pool is, kun je er veilig vanuit elk deel van je app bij, inclusief geplande taken. Controleer altijd eerst met isConnected:

app.registerCron(
  FinchCron(
    schedule: FinchCron.evryDay(2),
    onCron: (index, cron) async {
      if (app.mongoDb.isConnected) {
        await ExampleCollections().deleteAll();
      }
    },
  ).start(),
);

Zie Commands voor de volledige API voor het plannen van cronjobs, en Database Migration voor schema-/datamigraties die ook tegen MongoDB draaien.