Drift ORM: قاعدة بيانات آمنة من حيث الأنواع مع توليد الكود
Drift ORM: قاعدة بيانات آمنة من حيث الأنواع مع توليد الكود
Drift (المعروف سابقاً بـ Moor) هو مكتبة تخزين تفاعلية وآمنة من حيث الأنواع لـ Flutter وDart. بدلاً من كتابة SQL خام وتعيين الصفوف يدوياً إلى كائنات Dart، تتيح لك Drift تعريف مخطط قاعدة البيانات كفئات Dart عادية. يُنتج مولّد الكود بعد ذلك طبقة قاعدة بيانات مكتوبة بالكامل، وكائنات DAOs، ومساعدات الاستعلام — مما يُزيل فئة كاملة من الأخطاء الزمنية الناجمة عن الأخطاء الإملائية أو عدم تطابق الأنواع في سلاسل SQL.
Map<String, dynamic> يدوياً، وتكتشف الأخطاء فقط في وقت التشغيل. تكشف Drift تلك الأخطاء في وقت الترجمة: اسم عمود خاطئ أو نوع إرجاع خاطئ يُعدّ خطأ في البناء، وليس تعطلاً في الإنتاج.إعداد Drift
أضف ما يلي إلى ملف pubspec.yaml:
اعتماديات pubspec.yaml
dependencies:
drift: ^2.14.0
drift_flutter: ^0.2.0 # يوفر فاتح قاعدة البيانات الخاص بـ Flutter
sqlite3_flutter_libs: ^0.5.0
dev_dependencies:
drift_dev: ^2.14.0
build_runner: ^2.4.0
شغّل flutter pub get بعد تعديل الملف.
تعريف الجداول كفئات Dart
كل جدول هو فئة Dart تمتد من Table. كل getter تُعلنه يصبح عموداً في مخطط SQL المُولَّد. تستنتج Drift نوع SQL من نوع Dart الذي تستخدمه:
lib/database/tables.dart — تعريف جدول Tasks
import 'package:drift/drift.dart';
// كل فئة = جدول SQL واحد
class Tasks extends Table {
// INTEGER PRIMARY KEY AUTOINCREMENT
IntColumn get id => integer().autoIncrement()();
// TEXT NOT NULL
TextColumn get title => text().withLength(min: 1, max: 200)();
// TEXT (قابل للقيمة الفارغة)
TextColumn get description => text().nullable()();
// INTEGER (0 أو 1، مخزّن كقيمة منطقية)
BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
// INTEGER يمثل طابعاً زمنياً Unix
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 100)();
// مفتاح خارجي للمهام (تتحقق Drift على مستوى Dart)
IntColumn get taskId => integer().references(Tasks, #id)();
}
إنشاء فئة قاعدة البيانات وتشغيل build_runner
تجمع الجداول في فئة واحدة مُعلَّقة بـ @DriftDatabase. توجيه part يُخبر Dart بتوقع ملف مُولَّد بجانب ملفك:
lib/database/app_database.dart — فئة قاعدة البيانات
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'tables.dart';
part 'app_database.g.dart'; // مُولَّد بواسطة build_runner
@DriftDatabase(tables: [Tasks, Categories])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
// إصدار المخطط — زِد عند إضافة/تغيير الجداول
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'app_db');
}
}
ولّد الكود الجاهز بتشغيل:
الطرفية — توليد طبقة قاعدة البيانات
flutter pub run build_runner build --delete-conflicting-outputs
يُنشئ هذا app_database.g.dart يحتوي على الـ mixin الخاص بـ _$AppDatabase، وفئات الصف المكتوبة (Task، Category)، وفئات الرفيق للإدراج/التحديث (TasksCompanion، CategoriesCompanion).
build_runner watch بدلاً من build. يُعيد توليد الملف تلقائياً في كل مرة تحفظ فيها تغييراً على تعريفات الجداول، لذا لديك دائماً ملف مُولَّد محدّث دون تشغيل الأمر يدوياً.CRUD الآمن من حيث الأنواع مع DAOs
DAO (كائن الوصول إلى البيانات) يجمع الاستعلامات ذات الصلة في فئة واحدة ويبقي منطق قاعدة البيانات بعيداً عن الودجات. عَلِّقه بـ @DriftAccessor:
lib/database/task_dao.dart — DAO مع CRUD كامل
import 'package:drift/drift.dart';
import 'app_database.dart';
import 'tables.dart';
part 'task_dao.g.dart';
@DriftAccessor(tables: [Tasks])
class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
TaskDao(super.db);
// SELECT * FROM tasks ORDER BY created_at DESC
Stream<List<Task>> watchAllTasks() =>
(select(tasks)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])).watch();
// SELECT * FROM tasks WHERE is_completed = 0
Future<List<Task>> getIncompleteTasks() =>
(select(tasks)..where((t) => t.isCompleted.equals(false))).get();
// INSERT — يستخدم companion لتوفير الأعمدة التي تعرفها فقط بأمان
Future<int> insertTask(TasksCompanion entry) => into(tasks).insert(entry);
// UPDATE — فقط الأعمدة الموجودة في companion تتغير
Future<bool> updateTask(TasksCompanion entry) => update(tasks).replace(entry);
// DELETE بالمفتاح الأساسي
Future<int> deleteTask(int id) =>
(delete(tasks)..where((t) => t.id.equals(id))).go();
}
في الودجت تستهلك الـ DAO بشكل تفاعلي باستخدام StreamBuilder:
الودجت — مراقبة المهام باستخدام StreamBuilder
class TaskListPage extends StatelessWidget {
final TaskDao dao;
const TaskListPage({super.key, required this.dao});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Task>>(
stream: dao.watchAllTasks(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final tasks = snapshot.data!;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
subtitle: Text(task.description ?? ''),
trailing: Checkbox(
value: task.isCompleted,
onChanged: (_) => dao.updateTask(
TasksCompanion(
id: Value(task.id),
isCompleted: Value(!task.isCompleted),
),
),
),
onLongPress: () => dao.deleteTask(task.id),
);
},
);
},
);
}
}
build_runner بعد تعديل جدول أو DAO. إذا كان الملف المُولَّد قديماً، ستظهر أخطاء ترجمة مُربِكة بشأن أعضاء مفقودين. عند الشك، احذف ملفات .g.dart وشغّل build_runner build --delete-conflicting-outputs من البداية.التهجيرات — التعامل مع تغييرات المخطط
عند إضافة عمود أو جدول، زِد schemaVersion وتجاوز migration في فئة قاعدة البيانات:
إضافة عمود priority في الإصدار 2
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
if (from < 2) {
// إضافة العمود الجديد؛ الصفوف الموجودة تأخذ القيمة الافتراضية 0
await migrator.addColumn(tasks, tasks.priority);
}
},
);
ملخص
تُحوّل Drift الوصول إلى قاعدة بيانات SQLite في Flutter من SQL خام عرضة للأخطاء إلى تجربة يتم التحقق منها بالكامل في وقت الترجمة. تُعرّف الجداول كفئات Dart، وتشغّل build_runner مرة واحدة لتوليد قاعدة البيانات وكود الـ DAO الجاهز، ثم تُجري عمليات CRUD باستخدام أساليب مكتوبة بشدة وكائنات companion. تتيح التدفقات التفاعلية (عبر watch()) لواجهة المستخدم إعادة البناء تلقائياً عند تغيير البيانات الأساسية — مما يجعل Drift الخيار الأكثر قوةً لمتطلبات البيانات المحلية المعقدة.