MongoDB
فینچ از پکیج mongo_dart برای MongoDB استفاده میکند و آن را با مجموعه کوچکی از قراردادها — FinchDBConfig، DBCollection، و query builder به نام DQ — بستهبندی میکند، بهطوریکه controllerهای شما هرگز مجبور نیستند connection string خام بسازند یا کد CRUD تکراری (boilerplate) بنویسند.
این راهنما موارد زیر را پوشش میدهد:
- اتصال و پیکربندی pool
- نوشتن یک کلاس کالکشن با
DBCollection - متدهای کمکی داخلی که هر کالکشن بهصورت رایگان دریافت میکند
- ساخت کوئریها و pipelineهای aggregation با
DQ - اتصال یک کالکشن به یک model و یک controller
- بررسی سلامت اتصال (مثلاً از یک cron job)
1. پیکربندی
MongoDB از طریق FinchDBConfig پیکربندی میشود که به dbConfig در 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'), // دیتابیس منبع احراز هویت
maxConnections: 10, // اندازه connection pool (Db.pool)
),
);
| فیلد | نوع | توضیح |
|---|---|---|
enable |
bool |
وقتی false باشد، app.mongoDb هرگز متصل نمیشود — برای اپلیکیشنهایی که فقط از MySQL/SQLite استفاده میکنند و میخواهند Mongo را کاملاً نادیده بگیرند مفید است |
host, port, user, pass, dbName |
String |
اعتبارنامههای استاندارد اتصال. توجه کنید port از نوع String است، نه int |
auth |
String |
دیتابیس authSource در MongoDB (معمولاً admin) |
maxConnections |
int |
تعداد اتصالات poolشدهای که Db.pool باز میکند (پیشفرض 10) |
فینچ connection string را برای شما میسازد:
mongodb://user:pass@host:port/dbName/?authSource=auth
اگر یک فیلد را بهصراحت پاس ندهید، FinchDBConfig بهصورت پیشفرض آن را از خودِ environment میخواند (MONGO_CONNECTION، MONGO_PORT، MONGO_INITDB_DATABASE، MONGO_INITDB_ROOT_USERNAME، MONGO_INITDB_ROOT_PASSWORD، MONGO_INITDB_ROOT_AUTH) — اما واضحتر است که همانطور که در بالا نشان داده شد، خودتان فراخوانیهای env.get(...) را پاس دهید تا نام متغیرها با بقیه فایل .env شما مطابقت داشته باشد. برای فهرست کامل متغیرهای محیطی سراسر اپلیکیشن به Configuration مراجعه کنید.
Pool کردن اتصالها
فینچ هرگز یک سوکت تکی به MongoDB باز نمیکند. در زمان راهاندازی، DBManager این را فراخوانی میکند:
Db.pool(List.filled(config.maxConnections, config.link))
این کار یک pool شامل maxConnections اتصال مستقل ایجاد میکند، بنابراین درخواستهای همزمان از سوکتهای متفاوت سرویسدهی میشوند، بهجای اینکه پشت یک اتصال واحد در صف بمانند. برای اپلیکیشنهای پرترافیک، maxConnections را افزایش دهید؛ مقدار پیشفرض 10 برای بیشتر پروژهها کافی است.
2. دسترسی به پایگاه داده
var db = app.mongoDb; // نمونه Db از mongo_dart را برمیگرداند (همان pool)
قبل از اجرای منطق راهاندازی یا jobهای زمانبندیشده، وضعیت اتصال را بررسی کنید:
if (app.mongoDb.isConnected) {
// اجرای کوئری امن است
}
این همان الگویی است که cron jobهای خودِ فینچ استفاده میکنند — برای API مربوط به cron به Commands مراجعه کنید.
3. DBCollection — کلاس پایه کالکشن
DBCollection (از package:finch/finch_model.dart) یک کلاس abstract است که برای هر کالکشن MongoDB در اپلیکیشن خود از آن extend میکنید. این کلاس، کالکشن خام mongo_dart را بههمراه مجموعهای از متدهای کمکی آماده در اختیار شما قرار میدهد و اگر کالکشن از قبل وجود نداشته باشد، هنگام اولین instantiate شدن آن را بهصورت خودکار میسازد.
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همانDbCollectionزیرین ازmongo_dartاست (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}) |
اسناد را میشمارد، بهصورت اختیاری با فیلتر بر اساس یک جفت field/value یا یک نگاشت (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 یک _id جدید اختصاص دهد) |
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}) |
یک فیلد را بهصورت دستهای (bulk) روی همه اسنادی که با filter مطابقت دارند بهروزرسانی میکند |
مثال — استفاده مستقیم از متدهای built-in از یک controller، بدون نوشتن هیچ کد کوئری اضافهای:
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 سازنده selector خودِ mongo_dart است (where.eq، where.id، ...) و همراه با DQ کار میکند.
5. سازنده کوئری DQ
DQ یک کلاس کمکی static است (که از finch_model.dart دوباره export میشود) و قطعات کوئری/aggregation ساده MongoDB را بهصورت Map<String, Object?> تولید میکند — این کلاس صرفاً برای خواناتر کردن کوئریها نسبت به نگاشتهای دستی با کلیدهای پیشوند $ وجود دارد.
عملگرهای مقایسهای و منطقی
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' } — تطابق دقیق، بدون حساسیت به حروف بزرگ/کوچک
هر دو متد کاراکترهای خاص regex را در ورودی برای شما escape میکنند، بنابراین عبارات جستجوی واردشده توسط کاربر برای پاس دادن مستقیم امن هستند.
کمککنندههای 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 } — همان، مناسب برای مرحله aggregation
DQ.limit(20) // { '$limit': 20 }
DQ.skip(40) // { '$skip': 40 }
DQ.count('total') // { '$count': 'total' }
Pipelineهای Aggregation
برای هر چیزی فراتر از یک فیلتر ساده — joinها، گروهبندی، فیلدهای محاسبهشده — یک aggregation pipeline با DQ.pipeline(...) بسازید و آن را با collection.aggregateToStream یا collection.modernAggregate از 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();
سازندههای مرحله aggregation موجود:
| متد | مرحله | هدف |
|---|---|---|
DQ.match(List<Map>) |
$match |
فیلتر کردن اسنادی که وارد pipeline میشوند |
DQ.group(Map) |
$group |
گروهبندی اسناد و محاسبه مقادیر تجمیعی |
DQ.lookup(from:, localField:, foreignField:, as:) |
$lookup |
Left-outer join با یک کالکشن دیگر |
DQ.unwind(path:, as:, preserveNullAndEmptyArrays:) |
$unwind |
صاف کردن (flatten) یک فیلد آرایهای به اسناد جداگانه |
DQ.project(Map) |
$project |
شامل کردن/حذف کردن/محاسبه فیلدهای خروجی |
DQ.sort(Map) / DQ.sortList(List<Map>) / DQ.sortOne(field, desc) |
$sort |
مرتبسازی نتایج pipeline |
DQ.limit(int) / DQ.skip(int) |
$limit / $skip |
صفحهبندی درون یک pipeline |
DQ.count(field) |
$count |
شمارش اسناد در آن مرحله از pipeline |
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. Model
مدلهای فینچ کلاسهای ساده Dart با toJson/fromJson هستند — هیچ code generationای درگیر نیست، بنابراین شما بهطور کامل شکل سند را کنترل میکنید:
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. استفاده از کالکشن در یک 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 از یک cron job
از آنجا که app.mongoDb یک pool مشترک واحد است، دسترسی به آن از هر جای اپلیکیشن، از جمله taskهای زمانبندیشده، امن است. همیشه ابتدا با isConnected محافظت کنید:
app.registerCron(
FinchCron(
schedule: FinchCron.evryDay(2),
onCron: (index, cron) async {
if (app.mongoDb.isConnected) {
await ExampleCollections().deleteAll();
}
},
).start(),
);
برای API کامل زمانبندی cron به Commands مراجعه کنید، و برای migrationهای schema/data که روی MongoDB هم اجرا میشوند به Database Migration مراجعه کنید.