SQLite مع sqflite: عمليات CRUD والاستعلامات
SQLite مع sqflite: عمليات CRUD والاستعلامات
حزمة sqflite هي المكون الإضافي القياسي لـ SQLite في Flutter. توفر لك قاعدة بيانات علائقية كاملة على الجهاز — دائمة ومنظمة وقادرة على الاستعلام. في هذا الدرس ستتقن كل عملية من عمليات CRUD (الإنشاء والقراءة والتحديث والحذف)، وكتابة SQL خام عند الحاجة إلى تحكم كامل، وتحويل صفوف قاعدة البيانات إلى كائنات Dart مكتوبة بأنواع محددة، وتجميع عمليات الكتابة المتعددة داخل معاملة حتى تنجح جميعها أو تُرجع جميعها معاً.
فئة النموذج
قبل كتابة أي كود لقاعدة البيانات، عرّف فئة Dart تعكس الجدول الخاص بك. يتضمن النموذج النظيف طريقة toMap() للإدراج والتحديث، ومُنشئ مصنع fromMap() لقراءة الصفوف مرة أخرى.
نموذج Task
class Task {
final int? id; // قابل للفراغ — null قبل أول إدراج
final String title;
final bool isDone;
final DateTime createdAt;
const Task({
this.id,
required this.title,
this.isDone = false,
required this.createdAt,
});
// كائن Dart ➜ صف قاعدة بيانات
Map<String, dynamic> toMap() => {
if (id != null) 'id': id,
'title': title,
'is_done': isDone ? 1 : 0, // SQLite لا تملك BOOLEAN
'created_at': createdAt.toIso8601String(),
};
// صف قاعدة بيانات ➜ كائن Dart
factory Task.fromMap(Map<String, dynamic> map) => Task(
id: map['id'] as int?,
title: map['title'] as String,
isDone: (map['is_done'] as int) == 1,
createdAt: DateTime.parse(map['created_at'] as String),
);
Task copyWith({int? id, String? title, bool? isDone}) => Task(
id: id ?? this.id,
title: title ?? this.title,
isDone: isDone ?? this.isDone,
createdAt: createdAt,
);
}
toMap() وfromMap() — لا تفترض أبداً أن sqflite ستجري الإكراه على الأنواع نيابة عنك.الإدراج (الإنشاء)
استخدم db.insert() لإضافة صف. مرر اسم الجدول والخريطة من toMap() وخوارزمية ConflictAlgorithm الاختيارية التي تتحكم فيما يحدث عند انتهاك قيد الفرادة.
إدراج صف واحد
import 'package:sqflite/sqflite.dart';
Future<int> insertTask(Database db, Task task) async {
// يُرجع معرف الصف الجديد (عدد صحيح بزيادة تلقائية)
return await db.insert(
'tasks',
task.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
// replace → استبدال الصف الموجود بنفس المفتاح الأساسي
// ignore → تجاهل المكرر بصمت
// rollback → رمي استثناء (الافتراضي)
);
}
// الاستخدام
final newId = await insertTask(db, Task(
title: 'شراء البقالة',
createdAt: DateTime.now(),
));
print('تم إدراج المهمة بالمعرف $newId');
الاستعلام (القراءة)
استخدم db.query() لاستعلام SELECT منظم، أو db.rawQuery() عند الحاجة إلى JOINs أو استعلامات فرعية أو جمل WHERE معقدة. كلاهما يُرجع List<Map<String, dynamic>> التي تحولها إلى نموذجك باستخدام fromMap().
الاستعلام عن الصفوف
// --- استعلام منظم ---
Future<List<Task>> getPendingTasks(Database db) async {
final rows = await db.query(
'tasks',
columns: ['id', 'title', 'is_done', 'created_at'],
where: 'is_done = ?',
whereArgs: [0], // استخدم دائماً محددات ?
orderBy: 'created_at DESC',
limit: 50,
);
return rows.map(Task.fromMap).toList();
}
// --- استعلام SQL خام ---
Future<List<Task>> searchTasks(Database db, String keyword) async {
final rows = await db.rawQuery(
'SELECT * FROM tasks WHERE title LIKE ? ORDER BY created_at DESC',
['%$keyword%'],
);
return rows.map(Task.fromMap).toList();
}
// --- جلب صف واحد بالمعرف ---
Future<Task?> getTaskById(Database db, int id) async {
final rows = await db.query(
'tasks',
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
if (rows.isEmpty) return null;
return Task.fromMap(rows.first);
}
'WHERE title = "$input"'). هذا يعرضك لحقن SQL. استخدم دائماً محددات ? ومرر القيم عبر whereArgs أو الوسيطة الثانية لـ rawQuery().التحديث
استخدم db.update() لتعديل الصفوف الموجودة. وفر جملة where وwhereArgs لاستهداف صفوف محددة؛ حذفها يحدث كل صف في الجدول.
تحديث صف
Future<int> markTaskDone(Database db, int id) async {
// يُرجع عدد الصفوف المتأثرة
return await db.update(
'tasks',
{'is_done': 1},
where: 'id = ?',
whereArgs: [id],
);
}
// تحديث كائن كامل باستخدام toMap() للنموذج
Future<int> updateTask(Database db, Task task) async {
return await db.update(
'tasks',
task.toMap(),
where: 'id = ?',
whereArgs: [task.id],
);
}
الحذف
استخدم db.delete() لإزالة الصفوف. مثل update()، حذف where يحذف جميع الصفوف.
حذف صف وحذفات مجمعة عبر المعاملة
// حذف صف واحد
Future<int> deleteTask(Database db, int id) async {
return await db.delete('tasks', where: 'id = ?', whereArgs: [id]);
}
// حذف جميع المهام المكتملة داخل معاملة لضمان
// أن العملية ذرية — إما حذف جميع الصفوف أو لا شيء.
Future<void> purgeCompleted(Database db) async {
await db.transaction((txn) async {
// txn كائن معاملة يتصرف مثل Database
final deleted = await txn.delete(
'tasks',
where: 'is_done = ?',
whereArgs: [1],
);
await txn.rawInsert(
'INSERT INTO audit_log (action, count, ts) VALUES (?, ?, ?)',
['purge_completed', deleted, DateTime.now().toIso8601String()],
);
});
// إذا رمى أي تعليمة استثناء، تُرجع المعاملة بالكامل.
}
المعاملات للكتابات المجمعة
المعاملة (Transaction) تلف تعليمات SQL متعددة في وحدة ذرية واحدة. إما أن تنجح كل تعليمة وتُلتزم التغييرات، أو يتسبب استثناء في إرجاع كامل. استخدم المعاملات عندما:
- إدراج صفوف مرتبطة عبر جداول متعددة (مثل طلب وعناصر البند الخاص به)
- تحديث عداد وإدراج سجل في السجل معاً
- استيراد دفعة من السجلات — إذا فشل أحدها، لا ينبغي أن يستمر أي منها
الخلاصة
مساعدو CRUD الأربعة — insert() وquery() وupdate() وdelete() — يغطون معظم أعمال قاعدة البيانات. للاستعلامات المعقدة، ارجع إلى rawQuery() وrawInsert()/rawUpdate(). عرّف فئة نموذج مع toMap() وfromMap() للحفاظ على منطق التسلسل في مكان واحد، واستخدم دائماً محددات ? لمنع حقن SQL. جمّع الكتابات المرتبطة داخل db.transaction() لضمان الذرية وتعزيز إنتاجية الإدراج المجمع.