MongoDB
Finch 使用 mongo_dart 包来支持 MongoDB,并通过一小套约定——FinchDBConfig、DBCollection 以及 DQ 查询构建器——对其进行了封装,因此你的控制器永远不需要手动拼接连接字符串,也不必重复编写样板化的 CRUD 代码。
本指南涵盖以下内容:
- 连接并配置连接池
- 使用
DBCollection编写集合类 - 每个集合都自带的内置辅助方法
- 使用
DQ构建查询和聚合管道 - 将集合接入模型和控制器
- 检查连接状态(例如在定时任务中)
1. 配置
MongoDB 通过 FinchDBConfig 进行配置,并传递给 FinchConfigs 中的 dbConfig:
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'), // 认证源数据库
maxConnections: 10, // 连接池的大小(Db.pool)
),
);
| 字段 | 类型 | 描述 |
|---|---|---|
enable |
bool |
当为 false 时,app.mongoDb 永远不会建立连接——对于只使用 MySQL/SQLite、完全不需要 Mongo 的应用很有用 |
host、port、user、pass、dbName |
String |
标准连接凭据。注意 port 是 String 类型,而不是 int |
auth |
String |
MongoDB 的 authSource 数据库(通常为 admin) |
maxConnections |
int |
Db.pool 打开的连接池连接数量(默认值为 10) |
Finch 会为你构建连接字符串:
mongodb://user:pass@host:port/dbName/?authSource=auth
如果你没有显式传入某个字段,FinchDBConfig 会回退到自行从环境变量中读取(MONGO_CONNECTION、MONGO_PORT、MONGO_INITDB_DATABASE、MONGO_INITDB_ROOT_USERNAME、MONGO_INITDB_ROOT_PASSWORD、MONGO_INITDB_ROOT_AUTH)——但更清晰的做法是像上面那样自行传入 env.get(...) 调用,使变量名与你 .env 文件中的其余部分保持一致。完整的应用级环境变量列表请参阅 Configuration。
连接池
Finch 从不会只打开单个 MongoDB 套接字。启动时,DBManager 会调用:
Db.pool(List.filled(config.maxConnections, config.link))
这会创建一个包含 maxConnections 个独立连接的连接池,因此并发请求会由不同的套接字处理,而不必排队等待同一个连接。对于高流量应用,可以调高 maxConnections;对大多数项目来说,默认值 10 已经足够。
2. 访问数据库
var db = app.mongoDb; // 返回 mongo_dart 的 Db 实例(即连接池)
在运行启动逻辑或定时任务之前,先检查连接状态:
if (app.mongoDb.isConnected) {
// 可以安全地执行查询
}
这与 Finch 自身的定时任务所使用的模式相同——完整的定时任务 API 请参阅 Commands。
3. DBCollection——集合基类
DBCollection(来自 package:finch/finch_model.dart)是一个抽象类,你需要为应用中的每一个 MongoDB 集合继承它。它为你提供了原始的 mongo_dart 集合,外加一组现成的辅助方法;并且如果该集合尚不存在,会在首次实例化时自动创建它。
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是底层的mongo_dartDbCollection(即db.collection(name)),用于处理下方辅助方法未覆盖的任何操作。name和db是你传给super(...)的构造函数参数。
内置辅助方法
DBCollection 的每个子类都会自动获得以下方法,无需编写任何代码:
| 方法 | 签名 | 描述 |
|---|---|---|
existId |
Future<bool> existId(String idField) |
如果存在具有该 _id 的文档,则返回 true。对于无效的 ObjectId 字符串会返回 false,而不是抛出异常 |
exist |
Future<bool> exist(String field, Object value) |
如果任意文档满足 field == value,则返回 true |
getCount |
Future<int> getCount({String? field, Object? value, Map<String,Object?>? filter}) |
统计文档数量,可选地通过单个字段/值组合或原始过滤器 map 进行过滤 |
isEmpty / isNotEmpty |
Future<bool> get |
getCount() == 0 的快捷方式 |
delete |
Future<bool> delete(String id) |
按 _id 删除一个文档 |
deleteAll |
Future<bool> deleteAll() |
删除集合中的所有文档——请谨慎使用 |
copy |
Future<void> copy(String id) |
复制一个文档(会去掉 _id,以便 Mongo 分配一个新的) |
updateField |
Future<void> updateField(String id, String field, Object? value) |
仅当该 id 存在时,为一个文档设置单个字段 |
updateFields |
Future<void> updateFields(String id, Map<String, dynamic> fields) |
将多个字段合并写入一个文档 |
updateAllForField |
Future<void> updateAllForField({required String field, required Object? value, required Map<String,Object?>? filter}) |
对所有匹配 filter 的文档批量更新某个字段 |
示例——直接在控制器中使用这些内置方法,无需编写任何额外的查询代码:
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. 使用 modernFind 查询
modernFind 是查询集合的推荐方式——它可以在一次调用中同时接受 filter、sort、limit 和 skip:
// 所有文档,按最新排序
var rows = await collection
.modernFind(sort: DQ.order('_id', true))
.toList();
// 过滤并分页
var rows = await collection
.modernFind(
filter: where.eq('slug', 'my-slug'),
limit: 10,
skip: 0,
)
.toList();
where 是 mongo_dart 自带的选择器构建器(where.eq、where.id 等),可以与 DQ 配合使用。
5. DQ 查询构建器
DQ 是一个静态辅助类(从 finch_model.dart 中重新导出),用于生成普通的 Map<String, Object?> 形式的 MongoDB 查询/聚合片段——它存在的唯一目的,就是让查询比手写带 $ 前缀键的 map 更具可读性。
比较与逻辑运算符
DQ.eq('value') // 'value' — 直接相等
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 } }
文本匹配
DQ.like('john') // { '$regex': 'john', '$options': 'i' } — 不区分大小写的包含匹配
DQ.uncase('john') // { '$regex': '^john$', '$options': 'i' } — 不区分大小写的精确匹配
两者都会自动转义输入中的正则表达式特殊字符,因此可以安全地直接传入用户提供的搜索词。
ID 辅助方法
DQ.id('507f191e810c19729de860ea') // { '_id': ObjectId(...) } — 从 String 构建
DQ.oid(objectId) // { '_id': ObjectId(...) } — 从已有的 ObjectId 构建
综合起来,一个典型的过滤搜索如下:
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();
排序、分页与计数
DQ.order('createdAt') // { 'createdAt': -1 } — 默认降序
DQ.order('createdAt', false) // { 'createdAt': 1 } — 升序
DQ.sortField('createdAt', true) // { 'createdAt': -1 } — 结果相同,适用于聚合阶段
DQ.limit(20) // { '$limit': 20 }
DQ.skip(40) // { '$skip': 40 }
DQ.count('total') // { '$count': 'total' }
聚合管道
对于简单过滤之外的需求——例如连接、分组、计算字段——可以使用 DQ.pipeline(...) 构建聚合管道,并通过 mongo_dart 提供的 collection.aggregateToStream 或 collection.modernAggregate 来运行它:
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();
可用的聚合阶段构建方法:
| 方法 | 阶段 | 用途 |
|---|---|---|
DQ.match(List<Map>) |
$match |
过滤进入管道的文档 |
DQ.group(Map) |
$group |
对文档分组并计算聚合值 |
DQ.lookup(from:, localField:, foreignField:, as:) |
$lookup |
与另一个集合进行左外连接 |
DQ.unwind(path:, as:, preserveNullAndEmptyArrays:) |
$unwind |
将数组字段展开为多个独立文档 |
DQ.project(Map) |
$project |
包含/排除/计算输出字段 |
DQ.sort(Map) / DQ.sortList(List<Map>) / DQ.sortOne(field, desc) |
$sort |
对管道结果排序 |
DQ.limit(int) / DQ.skip(int) |
$limit / $skip |
管道内的分页 |
DQ.count(field) |
$count |
统计该管道阶段的文档数量 |
DQ.sum(field) / DQ.sumQuery(query) |
$sum |
在 $group 内对字段(或计算表达式)求和 |
DQ.cond(ifCond:, thenCond:, elseCond:) |
$cond |
条件表达式 |
DQ.dateToString(field:, format:, timezone:) |
$dateToString |
将日期字段格式化为字符串 |
DQ.toDate(field) |
$toDate |
将字段转换为日期类型 |
DQ.$field(name) |
— | 为字段名添加 $ 前缀(例如 '$name'),以便在表达式中使用 |
6. 模型
Finch 的模型只是带有 toJson/fromJson 的普通 Dart 类——不涉及任何代码生成,因此你可以完全控制文档的结构:
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. 在控制器中使用集合
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
由于 app.mongoDb 是一个共享的连接池,你可以在应用的任何地方安全地访问它,包括定时任务。请始终先用 isConnected 进行保护性检查:
app.registerCron(
FinchCron(
schedule: FinchCron.evryDay(2),
onCron: (index, cron) async {
if (app.mongoDb.isConnected) {
await ExampleCollections().deleteAll();
}
},
).start(),
);
完整的定时任务调度 API 请参阅 Commands;同样适用于 MongoDB 的模式/数据迁移,请参阅 Database Migration。