MySQL

فینچ از پکیج mysql_client_plus برای MySQL استفاده می‌کند. اتصال به‌صورت خودکار توسط FinchApp در زمان راه‌اندازی و بر اساس تنظیمات FinchMysqlConfig شما برقرار می‌شود، و از طریق app.mysqlDriver به‌صورت DatabaseDriver<MySQLConnectionPool> در دسترس قرار می‌گیرد.

یکپارچه‌سازی SQL فینچ (که بین MySQL و SQLite مشترک است) سه لایه دارد که با هم کار می‌کنند و همگی از پکیج sqler می‌آیند:

  1. MTable / MField* — کلاس‌های Dart که schema پایگاه داده شما را توصیف می‌کنند. این کلاس‌ها برای migrationها SQL مربوط به CREATE TABLE را تولید می‌کنند و همچنین به‌عنوان اعتبارسنج فرم (form validator) برای هر فیلد عمل می‌کنند.
  2. Sqler — یک query builder روان (fluent) که SQL پارامتری‌شده را به‌صورت امن می‌سازد.
  3. DatabaseDriver — یک کلاس واحد پوشش‌دهنده‌ی اتصال (connection-wrapper) که یک کوئری ساخته‌شده را روی MySQL یا SQLite اجرا می‌کند (نوع اتصال زیرین را در زمان اجرا بررسی می‌کند) و یک SqlDatabaseResult برمی‌گرداند.

پیکربندی

mysqlConfig را به FinchConfigs در فایل app.dart خود اضافه کنید. مقادیر باید از متغیرهای محیطی خوانده شوند:

FinchConfigs configs = FinchConfigs(
  mysqlConfig: FinchMysqlConfig(
    enable: true,
    host: env.get('MYSQL_HOST', 'localhost'),
    port: env.getInt('MYSQL_PORT', 3306),
    user: env.get('MYSQL_USER', 'db_user'),
    pass: env.get('MYSQL_PASS', 'db_password'),
    databaseName: env.get('MYSQL_DATABASE', 'my_db'),
    maxConnections: 10,  // size of the MySQLConnectionPool
  ),
);

نکته: FinchMysqlConfig.port از نوع int است (برخلاف FinchDBConfig.port برای MongoDB که از نوع String است).

دسترسی به Driver

پس از اجرای برنامه، می‌توانید به driver پایگاه داده از هر جایی که به نمونه app دسترسی دارد، دسترسی پیدا کنید:

var driver = app.mysqlDriver; // DatabaseDriver<MySQLConnectionPool>

// You can also check whether the connection is active
bool ok = app.mysqlDb.connected;

driver را به کلاس‌های لایه داده (data-layer) خود پاس دهید، به‌جای اینکه app.mysqlDriver را مستقیماً در controllerها فراخوانی کنید. این کار controllerها را سبک نگه می‌دارد و لایه داده شما را قابل تست می‌کند.

تعریف یک جدول (MTable)

MTable یک جدول پایگاه داده را در Dart نمایش می‌دهد. آن را یک‌بار تعریف می‌کنید و برای موارد زیر دوباره استفاده می‌کنید:

  • Migrationهاfinch migrate تعریف‌های MTable شما را می‌خواند تا دستورات CREATE TABLE و ALTER TABLE را تولید کند.
  • اعتبارسنجی (Validation)validators روی هر فیلد با AdvancedForm مشترک است.
  • ساخت کوئریtable.allSelectFields() و متدهای کمکی زیر شما را از تکرار فهرست ستون‌ها بی‌نیاز می‌کنند.

هر ستون توسط یک کلاس MField* نمایش داده می‌شود:

import 'package:finch/finch_mysql.dart';
import 'package:finch/finch_ui.dart'; // for FieldValidator

final table = MTable(
  name: 'books',
  fields: [
    MFieldInt(
      name: 'id',
      isPrimaryKey: true,
      isAutoIncrement: true,
      isNullable: false,
    ),
    MFieldVarchar(
      name: 'title',
      length: 255,
      isNullable: false,
      comment: 'Title of the book',
      validators: [
        FieldValidator.requiredField().toSimple(),
        FieldValidator.fieldLength(min: 3, max: 255).toSimple(),
      ],
    ),
    MFieldVarchar(name: 'author', length: 255, isNullable: false),
    MFieldDate(name: 'published_date', isNullable: false),
    MFieldInt(name: 'category_id', isNullable: true),
    MFieldBoolean(name: 'is_published', defaultValue: 'FALSE'),
  ],
  foreignKeys: [
    ForeignKey(
      name: 'category_id',
      refTable: 'categories',
      refColumn: 'id',
      onDelete: 'SET NULL',
    ),
  ],
);

Available Field Types

MField* طیف کاملی از انواع ستون MySQL را پوشش می‌دهد. مواردی که بیشتر از همه استفاده خواهید کرد:

کلاس نوع SQL توضیحات
MFieldInt INT پشتیبانی از primary key و auto-increment
MBigInt / MMediumInt / MSmallInt / MTinyInt BIGINT / MEDIUMINT / SMALLINT / TINYINT محدوده‌های عددی صحیح باریک‌تر/گسترده‌تر
MFieldVarchar VARCHAR(n) مقدار پیش‌فرض length برابر 255 است
MFieldChar CHAR(n) رشته با طول ثابت
MFieldText / MFieldTinyText / MFieldMediumText / MFieldLongText انواع TEXT برای رشته‌های طولانی، بر اساس محدودیت اندازه
MFieldDate DATE به‌صورت YYYY-MM-DD ذخیره می‌شود
MFieldDateTime / MFieldTimestamp DATETIME / TIMESTAMP timestamp کامل؛ MFieldTimestamp معمولاً برای created_at/updated_at استفاده می‌شود
MFieldBoolean TINYINT(1) به‌صورت 0/1 ذخیره می‌شود — نه MFieldBool
MFieldFloat (m، اختیاری d) FLOAT(m,d) اعشاری تقریبی
MFieldDecimal (m: 10، d: 2) DECIMAL(m,d) اعشاری دقیق با ممیز ثابت — به‌جای MFieldFloat برای مبالغ پولی استفاده کنید
MFieldEnum (values: [...]) ENUM(...) مجموعه ثابتی از مقادیر رشته‌ای
MFieldJson JSON ستون JSON بومی (MySQL 5.7 به بعد)
MFieldBlob family, MFieldBinary/MFieldVarBinary BLOB / BINARY داده باینری
MFieldBit, MFieldTime, MFieldYear, MFieldPoint, MFieldPolygon انواع کمتر رایج، با همان الگوی سازنده

کلاسی به نام MFieldDouble وجود ندارد — برای اعداد تقریبی از MFieldFloat و برای اعداد دقیق از MFieldDecimal استفاده کنید.

کلیدهای خارجی

نمونه‌های ForeignKey را به MTable.foreignKeys پاس دهید. هرکدام هنگام migrate شدن جدول، یک دستور ALTER TABLE ... ADD CONSTRAINT تولید می‌کند:

ForeignKey(
  name: 'category_id',       // column in this table
  refTable: 'categories',    // table it points to
  refColumn: 'id',           // column it points to (default: 'id')
  onDelete: 'CASCADE',       // 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'
  onUpdate: 'RESTRICT',
)

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

Sqler همیشه کوئری‌های پارامتری‌شده را از طریق QVar تولید می‌کند که مقادیر را escape کرده و از SQL injection جلوگیری می‌کند — شما هرگز ورودی کاربر را مستقیماً به رشته کوئری الحاق (concatenate) نمی‌کنید.

یک کوئری را با استفاده از fluent API بسازید، سپس آن را به driver.execute(query) پاس دهید:

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'),
      QSelect('b.published_date'),
    ])
    ..orderBy(QOrder('b.id', desc: true))
    ..limit(20);

  return db.execute(query);
}

درج

Sqler.insert() جدول مقصد و یک لیست از map‌های ردیف را می‌گیرد (به این ترتیب یک INSERT ... VALUES (...), (...) چندردیفی می‌تواند در یک فراخوانی ساخته شود):

Future<void> insertBook(DatabaseDriver db, Map<String, QVar> data) async {
  var query = Sqler().insert(QField(table.name), [data]);
  await db.execute(query);
}

به‌روزرسانی

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

Future<void> updateBook(DatabaseDriver db, int id, Map<String, QVar> data) async {
  var query = Sqler()..update(QField(table.name));
  data.forEach((field, value) => query.updateSet(field, value));
  query.where(WhereOne(QField('id'), QO.EQ, QVar(id)));

  await db.execute(query);
}

حذف

.delete() را با .from() ترکیب کنید. همیشه یک بند .where() اضافه کنید تا از حذف همه ردیف‌ها جلوگیری شود:

Future<void> deleteBook(DatabaseDriver db, int id) async {
  var query = Sqler()
    ..delete()
    ..from(QField(table.name))
    ..where(WhereOne(QField('id'), QO.EQ, QVar(id)));

  await db.execute(query);
}

Table Convenience Methods

هر MTable همچنین مجموعه‌ای از متدهای آماده (از طریق یک extension) دریافت می‌کند که برای موارد رایج، نیاز به نوشتن دستی فراخوانی‌های Sqler را از بین می‌برد:

await table.existsTable(driver);        // bool — آیا جدول وجود دارد؟
await table.createTable(driver);        // CREATE TABLE بر اساس تعریف MTable
await table.createForeignKeys(driver);  // ALTER TABLE ... ADD CONSTRAINT برای هر ForeignKey
await table.dropTable(driver);          // DROP TABLE IF EXISTS

await table.insert(driver, {'title': QVar('Dart in Action'), 'author': QVar('Alice')});
await table.insertMany(driver, [
  {'title': QVar('Book A'), 'author': QVar('Alice')},
  {'title': QVar('Book B'), 'author': QVar('Bob')},
]);

await table.select(driver, Sqler()..from(table.qName)..selects(table.allSelectFields()));
await table.delete(driver, Sqler()..delete()..from(table.qName)..where(WhereOne(QField('id'), QO.EQ, QVar(1))));

// Validate a form submission against this table's field validators
var formResult = await table.formValidateUI({'title': 'x', 'author': 'Alice'});

table.qName میان‌بری برای QField(table.name) است، و table.allSelectFields() یک ورودی QSelect برای هر فیلد تعریف‌شده برمی‌گرداند، بنابراین لازم نیست ستون‌ها را به‌صورت دستی فهرست کنید.

کلاس پایه Repository (MysqlTable)

برای یک الگوی repository کوچک، به‌جای نوشتن کوئری‌های خام در هر متد، کلاس abstract به نام MysqlTable را extend کنید. این کلاس از قبل deleteBy، deleteById، findById و countBy را پیاده‌سازی کرده است؛ شما فقط باید findAll/updateFilters انتزاعی (abstract) را پیاده‌سازی کنید:

class BooksRepository extends MysqlTable {
  @override
  DatabaseDriver get db => app.mysqlDriver;

  @override
  String get tableName => 'books';

  @override
  Sqler updateFilters(Sqler query, Map<String, dynamic> filter) {
    if (filter['author'] != null) {
      query.where(WhereOne(QField('author'), QO.EQ, QVar(filter['author'])));
    }
    return query;
  }

  @override
  Future<({int count, SqlDatabaseResult rows})> findAll({
    String orderBy = 'id',
    bool orderReverse = true,
    Map<String, dynamic> filters = const {},
    int? pageSize,
    int? offset,
  }) async {
    var query = Sqler()..from(qName)..selects(table.allSelectFields());
    query = updateFilters(query, filters);
    query.orderBy(QOrder(orderBy, desc: orderReverse));
    if (pageSize != null) query.limit(pageSize, offset);

    var count = await countBy(filters.isEmpty ? WhereOne(QField('id'), QO.GT, QVar(0)) : Where());
    var rows = await db.execute(query);
    return (count: count, rows: rows);
  }
}

// Usage:
var books = BooksRepository();
await books.deleteById(3);
var found = await books.findById(1);

Result Handling

db.execute() یک SqlDatabaseResult برمی‌گرداند. به‌طور خاص برای MySQL، .rows فهرستی از ResultSetRow (از mysql_client_plus) است که از .colByName(name) پشتیبانی می‌کند — توجه کنید این متد مقدار خام را مستقیماً برمی‌گرداند، و اگر نام ستون وجود نداشته باشد خطا (throw) می‌کند، بنابراین فقط برای ستون‌هایی از آن استفاده کنید که مطمئن هستید در کوئری وجود دارند:

var result = await getAllBooks(app.mysqlDriver);

for (var row in result.rows) {
  var title = row.colByName('title') ?? '';
  var id    = row.colByName('id') ?? 0;
}

جایگزین مستقل از پایگاه‌داده (database-agnostic) — و تنها گزینه برای SQLite، به SQLite مراجعه کنید — .assoc/.assocFirst است که یک Map<String, dynamic> ساده برمی‌گرداند:

for (var row in result.assoc) {
  print(row['title']);
}

var firstRow = result.assocFirst;   // Map<String, dynamic>? — اولین ردیف یا null
var numRows  = result.numRows;      // تعداد ردیف‌ها در این نتیجه
var newId    = result.insertId;     // شناسه auto-increment از آخرین INSERT
var affected = result.affectedRows; // تعداد ردیف‌های تحت‌تأثیر آخرین INSERT/UPDATE/DELETE

برای یک کوئری صفحه‌بندی از نوع شمارش کل ردیف‌ها، 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 به Database Migration مراجعه کنید.