SQLite

فینچ از پکیج sqlite3 برای SQLite استفاده می‌کند. SQLite یک پایگاه داده مبتنی بر فایل است — هیچ سرور پایگاه داده جداگانه‌ای برای مدیریت وجود ندارد. تمام داده‌ها در یک فایل واحد روی دیسک ذخیره می‌شوند.

SQLite زمانی انتخاب خوبی است که:

  • در حال ساخت یک اپلیکیشن کوچک یا متوسط هستید که به همزمانی (concurrency) بالا نیاز ندارد
  • می‌خواهید استقرار (deployment) بدون وابستگی (zero-dependency) داشته باشید (بدون نیاز به سرور پایگاه داده خارجی)
  • به‌صورت محلی در حال توسعه هستید و به راه‌اندازی سریع نیاز دارید

یکپارچه‌سازی SQLite در فینچ از همان API مربوط به MTable، MField* و Sqler که در MySQL استفاده می‌شود بهره می‌برد (برای مرجع کامل schema/query-builder به MySQL مراجعه کنید) — همان فراخوانی Sqler بسته به اینکه DatabaseDriver واقعاً کدام پایگاه داده را در دست دارد، SQL معتبر تولید می‌کند، زیرا DatabaseDriver نوع اتصال زیرین را در زمان اجرا تشخیص می‌دهد. تفاوت‌ها عبارت‌اند از: پیکربندی، ویژگی دسترسی به driver، و — نکته مهم‌تر — نحوه خواندن مقادیر از یک result set (به Result Handling در ادامه مراجعه کنید).

پیکربندی

sqliteConfig را به FinchConfigs اضافه کنید. تنها پارامتر الزامی، مسیر فایل .sqlite است:

FinchConfigs configs = FinchConfigs(
  sqliteConfig: FinchSqliteConfig(
    enable: true,
    filePath: env.get('SQLITE_PATH', './app.sqlite'),
  ),
);

اگر فایل وجود نداشته باشد، به‌صورت خودکار ایجاد می‌شود. مسیر می‌تواند مطلق یا نسبت به دایرکتوری کاری (working directory) باشد.

دسترسی به Driver

// DatabaseDriver<Database> — از این برای کوئری‌های Sqler استفاده کنید
var driver = app.sqliteDriver;

// دسترسی مستقیم sqlite3.Database — فقط زمانی استفاده کنید که به دسترسی سطح‌پایین نیاز دارید
var db = app.sqliteDb;

از app.sqliteDriver در کلاس‌های لایه داده خود استفاده کنید. آن را به‌عنوان یک آرگومان سازنده (constructor) پاس دهید تا controllerها سبک باقی بمانند:

class BooksData {
  final DatabaseDriver db;
  BooksData(this.db);

  // متدهای کوئری شما...
}

// در یک controller:
var books = BooksData(app.sqliteDriver);

تعریف یک جدول

schemaهای جدول دقیقاً مانند MySQL با استفاده از MTable و MField* تعریف می‌شوند. فینچ هر دو را از finch_mysql.dart import می‌کند — این‌ها بین MySQL و SQLite مشترک هستند:

import 'package:finch/finch_mysql.dart'; // MTable و MField* بین MySQL و SQLite مشترک هستند
import 'package:finch/finch_ui.dart';    // FieldValidator

final table = MTable(
  name: 'books',
  fields: [
    MFieldInt(name: 'id', isPrimaryKey: true, isAutoIncrement: true, isNullable: false),
    MFieldVarchar(
      name: 'title',
      isNullable: false,
      validators: [
        FieldValidator.requiredField().toSimple(),
        FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
      ],
    ),
    MFieldVarchar(name: 'author', isNullable: false),
    MFieldDate(name: 'published_date', isNullable: false),
    MFieldInt(name: 'category_id', isNullable: true),
  ],
);

MFieldInt هنگامی که SQL برای یک اتصال SQLite تولید می‌شود (toSQL<Sqlite>() در برابر toSQL<Mysql>()) به‌صورت خودکار به INTEGER در SQLite نگاشت می‌شود — نیازی به یک نوع فیلد مخصوص SQLite ندارید. برای فهرست کامل، از جمله MFieldBoolean (نه MFieldBool) و MFieldDecimal/MFieldFloat (که MFieldDouble وجود ندارد)، به MySQL — Available Field Types مراجعه کنید.

کوئری‌نویسی با Sqler

کوئری‌ها با همان fluent API مربوط به Sqler که در MySQL استفاده می‌شود ساخته می‌شوند. فینچ تفاوت‌های دیالکت SQL را به‌صورت داخلی مدیریت می‌کند:

import 'package:finch/finch_mysql.dart';

Future<SqlDatabaseResult> getAllBooks(DatabaseDriver db) async {
  var query = Sqler()
    ..from(QField(table.name, as: 'b'))
    ..selects([
      QSelect('b.id'),
      QSelect('b.title'),
      QSelect('b.author'),
    ])
    ..orderBy(QOrder('b.id', desc: true))
    ..limit(20);

  return db.execute(query);
}

درج

Sqler.insert() جدول مقصد و یک لیست از map‌های ردیف را می‌گیرد:

await db.execute(
  Sqler().insert(QField(table.name), [
    {'title': QVar('Dart in Action'), 'author': QVar('Alice')},
  ]),
);

به‌روزرسانی

از .update(table) استفاده کنید، سپس .updateSet(field, value) را برای هر فیلد فراخوانی کنید، سپس .where() را اضافه کنید:

await db.execute(
  Sqler()
    ..update(QField(table.name))
    ..updateSet('title', QVar('Updated Title'))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

حذف

.delete() را با .from() ترکیب کنید:

await db.execute(
  Sqler()
    ..delete()
    ..from(QField(table.name))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(1))),
);

همان متدهای کمکی MTable و کلاس پایه repository به نام SqliteTable (مشابه MysqlTable، با deleteBy/deleteById/findById/countBy) نیز در دسترس هستند — برای فهرست کامل متدها به MySQL — Table Convenience Methods مراجعه کنید؛ همه آن‌ها بدون تغییر روی app.sqliteDriver کار می‌کنند.

Result Handling

این تنها جایی است که SQLite واقعاً با MySQL تفاوت دارد: SqlDatabaseResult.rows در SQLite یک List<List<Object?>> ساده است (مقادیر موقعیتی برای هر ردیف) — و نه آبجکت‌هایی با متد .colByName() مانند ردیف‌های نتیجه در MySQL. همیشه برای خواندن ستون‌ها بر اساس نام، به‌جای اندیس‌گذاری موقعیتی در .rows، از .assoc/.assocFirst استفاده کنید:

var result = await getAllBooks(app.sqliteDriver);

for (var row in result.assoc) {
  var title = row['title']; // Map<String, String?> — هر مقدار به‌صورت String برمی‌گردد
}

var firstRow = result.assocFirst; // Map<String, String?>? — اولین ردیف یا null
var numRows  = result.numRows;
var newId    = result.insertId;     // آخرین insert row id مربوط به sqlite3
var affected = result.affectedRows; // تعداد ردیف‌های تغییریافته توسط آخرین دستور

نکته: برخلاف .assoc در MySQL (که انواع بومی Dart را حفظ می‌کند)، .assoc/.assocFirst در SQLite هر مقدار را به رشته تبدیل می‌کند (row[i]?.toString()). در صورت نیاز به مقادیر typed، اعداد/بولین‌ها را به‌صراحت parse کنید (مثلاً int.parse(row['id']!)).

برای یک کوئری صفحه‌بندی از نوع شمارش کل ردیف‌ها، دقیقاً مانند MySQL، COUNT(*) را با نام مستعار count_records بسازید و .countRecords را بخوانید:

var countQuery = Sqler()..from(table.qName)..addSelect(SQL.count(QField('id', as: 'count_records')));
var total = (await table.execute(driver, countQuery)).countRecords;

Migrationها

migrationهای SQLite به‌صورت جداگانه از migrationهای MySQL پیگیری می‌شوند و دستور app-runtime اختصاصی خود یعنی migrate_sqlite را دارند:

# اعمال تمام migrationهای معلق SQLite
finch run --args="migrate_sqlite --init"

# ساخت یک فایل migration جدید برای SQLite
finch migrate --create --name add_books_table --sqlite

دستور finch migrate --init --sqlite (دستور ترکیبی MySQL همراه با flag به نام --sqlite) migrationهای معلق SQLite را نیز، در همان فراخوانی MySQL، اعمال می‌کند — اما این کار فقط برای --init صادق است. migrate --rollback --sqlite و migrate --list --sqlite به‌صورت خاموش (silently) flag --sqlite را نادیده می‌گیرند و فقط روی MySQL عمل می‌کنند؛ برای این دو مورد از migrate_sqlite --rollback / migrate_sqlite --list استفاده کنید. برای مرجع کامل دستورات و فرمت فایل migration به Database Migration مراجعه کنید.

SQLite برای توسعه، تست، و استقرارهای production کوچک که نیازی به سرور پایگاه داده جداگانه ندارند، گزینه‌ای ایده‌آل است.