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);
}
}
collectionis de onderliggendemongo_dartDbCollection(db.collection(name)), voor alles wat de onderstaande hulpmethoden niet dekken.nameendbzijn de constructorargumenten die je doorgeeft aansuper(...).
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.