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 مراجعه کنید.