MySQL
فینچ از پکیج mysql_client_plus برای MySQL استفاده میکند. اتصال بهصورت خودکار توسط FinchApp در زمان راهاندازی و بر اساس تنظیمات FinchMysqlConfig شما برقرار میشود، و از طریق app.mysqlDriver بهصورت DatabaseDriver<MySQLConnectionPool> در دسترس قرار میگیرد.
یکپارچهسازی SQL فینچ (که بین MySQL و SQLite مشترک است) سه لایه دارد که با هم کار میکنند و همگی از پکیج sqler میآیند:
MTable/MField*— کلاسهای Dart که schema پایگاه داده شما را توصیف میکنند. این کلاسها برای migrationها SQL مربوط بهCREATE TABLEرا تولید میکنند و همچنین بهعنوان اعتبارسنج فرم (form validator) برای هر فیلد عمل میکنند.Sqler— یک query builder روان (fluent) که SQL پارامتریشده را بهصورت امن میسازد.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 مراجعه کنید.