الاتصال بقاعدة بيانات SQLite ما هو إلا فتح ملف
SQLite بدون خادم. لا توجد عملية تستمع على منفذ، ولا مضيف تتصل به، ولا بيانات اعتماد تتفاوض عليها. "الاتصال" هنا يعني ببساطة أن المُشغِّل (driver) يفتح ملفًا على القرص ويبدأ بقراءة صفحاته والكتابة فيه. هذه هي الصورة الذهنية كاملةً.
كل لغة برمجة لها مُشغِّل يغلّف مكتبة SQLite المكتوبة بلغة C. الأشكال تختلف، لكن المكوّنات الأساسية واحدة: مسار لملف قاعدة البيانات، استدعاء للفتح، مَقبِض (handle) تنفّذ عليه الجمل، ثم استدعاء للإغلاق عند الانتهاء.
-- من الناحية المفاهيمية، كل مُشغِّل يقوم بما يلي:
-- 1. فتح أو إنشاء الملف في المسار المحدد.
-- 2. الحصول على مَقبَض (handle).
-- 3. تنفيذ SQL عبر العبارات المُعدَّة مسبقًا (prepared statements).
-- 4. إغلاق المَقبَض.
في بقية هذه الصفحة هتلاقي شكل ده فعليًا في الكود، مع شوية إعدادات يستحسن تضبطها قبل أول استعلام.
بايثون: مكتبة sqlite3 المدمجة
بايثون بييجي ومعاه sqlite3 جاهزة — مفيش داعي لأي تثبيت. الشكل الأساسي للاستخدام:
-- Python
import sqlite3
conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes (body) VALUES (?)", ("الملاحظة الأولى",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
بعض الأمور التي يجدر بك معرفتها:
sqlite3.connect("app.db")ينشئ الملف تلقائيًا إذا لم يكن موجودًا. مرّر":memory:"للحصول على قاعدة بيانات تعيش في الذاكرة فقط.sqlite3.connect("file:app.db?mode=ro", uri=True)يفتح قاعدة البيانات للقراءة فقط عبر صيغة URI.- علامة
?داخل جملة SQL هي عنصر نائب (placeholder) — استخدم دائمًا ربط المعاملات (parameter binding) ولا تلجأ أبدًا إلى دمج النصوص. سنتعمق في هذا الموضوع في الفصل القادم. - استدعاء
conn.commit()ضروري، إلا إذا استخدمت مدير السياق (with conn:) الذي يقوم بالـ commit تلقائيًا.
أما إذا كان تطبيقك يعمل لفترات طويلة، فمن الأفضل ضبط مهلة الانشغال (busy timeout) حتى ينتظر الكتّاب المتزامنون بدلًا من أن يصطدموا بخطأ:
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- الانتظار حتى 5 ثوانٍ
conn.execute("PRAGMA journal_mode = WAL") -- تزامن أفضل
Node.js مع better-sqlite3
في عالم Node فيه أكثر من مكتبة للتعامل مع SQLite، لكن better-sqlite3 هي الخيار الأول لمعظم الفرق. المكتبة تعمل بشكل متزامن (synchronous)، وقد يبدو هذا غريباً في بيئة Node، إلا أنه فعلياً أسرع مع SQLite لأن الاستعلامات ترجع نتائجها خلال ميكروثوانٍ معدودة.
-- Node.js
const Database = require("better-sqlite3");
const db = new Database("app.db");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
const insert = db.prepare("INSERT INTO notes (body) VALUES (?)");
insert.run("الملاحظة الأولى");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
تُرجع لك db.prepare(...) كائن statement قابل لإعادة الاستخدام. تستخدم .run() لعمليات الكتابة، و.all() لجلب كل الصفوف، و.get() لجلب صف واحد فقط. نفس النمط المتّبع في معظم محرّكات SQL.
اضبط الـ pragmas عند بدء التشغيل:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- معطّل افتراضيًا، ومطلوب في معظم الحالات تقريبًا
foreign_keys = ON خيار يستحق التنويه: SQLite لا يفرض المفاتيح الأجنبية إلا إذا طلبت ذلك صراحةً، ولكل اتصال على حدة. إذا نسيت تفعيله، فإن جمل REFERENCES لديك مجرد زينة لا أكثر.
استخدام SQLite في Go عبر database/sql
حزمة database/sql القياسية في Go مستقلة عن المُشغِّل (driver-agnostic)، أي أنها تعمل مع أي مُشغِّل متوافق. أما بالنسبة لـ SQLite في Go، فالخياران الشائعان هما modernc.org/sqlite (مكتوب بلغة Go خالصة بدون CGO) و github.com/mattn/go-sqlite3 (يعتمد على CGO).
-- Go
import (
"database/sql"
_ "modernc.org/sqlite"
)
db, err := sql.Open("sqlite", "app.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil { panic(err) }
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
_, err = db.Exec("INSERT INTO notes (body) VALUES (?)", "الملاحظة الأولى")
rows, _ := db.Query("SELECT id, body FROM notes")
defer rows.Close()
for rows.Next() {
var id int; var body string
rows.Scan(&id, &body)
fmt.Println(id, body)
}
سلسلة الاستعلام التي تأتي بعد اسم الملف هي الطريقة التي يُمرّر بها هذا الدرايفر الـ pragmas عند فتح الاتصال — والصيغة تختلف من درايفر لآخر، لذا راجع توثيق الدرايفر الذي تستخدمه.
الدالة sql.Open لا تفتح اتصالاً فعلياً؛ أول استعلام تنفّذه هو الذي يفتح الاتصال. المتغير db عبارة عن connection pool، ومع SQLite يُفضَّل عادةً أن يكون هذا الـ pool صغيراً، بل قد يصل الأمر إلى ضبط db.SetMaxOpenConns(1) في التطبيقات التي تكثُر فيها عمليات الكتابة.
الاتصال بـ SQLite من Java عبر JDBC
الدرايفر org.xerial:sqlite-jdbc هو الخيار القياسي في عالم Java. وروابط JDBC تأخذ الشكل التالي: jdbc:sqlite:<path>:
-- Java
import java.sql.*;
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db")) {
try (Statement st = conn.createStatement()) {
st.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
st.execute("PRAGMA journal_mode = WAL");
st.execute("PRAGMA busy_timeout = 5000");
}
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO notes (body) VALUES (?)")) {
ps.setString(1, "الملاحظة الأولى");
ps.executeUpdate();
}
try (PreparedStatement ps = conn.prepareStatement("SELECT id, body FROM notes");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getInt(1) + " " + rs.getString(2));
}
}
قاعدة البيانات في الذاكرة: jdbc:sqlite::memory:. للقراءة فقط: أضف ?open_mode=1 أو استخدم كائن SQLiteConfig.
الاتصال بـ sqlite من PHP عبر PDO
سلسلة الاتصال (DSN) الخاصة بـ SQLite في PDO تأخذ الشكل sqlite:<path>:
-- PHP
$db = new PDO("sqlite:app.db");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
$db->exec("PRAGMA journal_mode = WAL");
$db->exec("PRAGMA busy_timeout = 5000");
$stmt = $db->prepare("INSERT INTO notes (body) VALUES (?)");
$stmt->execute(["الملاحظة الأولى"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: لقاعدة بيانات تعمل في الذاكرة. واحرص دائمًا على ضبط ATTR_ERRMODE على وضع الاستثناءات — فالأخطاء الصامتة كابوس عند تتبّع المشاكل.
سلاسل الاتصال ومسارات الملفات
ستلاحظ في معظم المشغّلات (drivers) أن "sqlite connection string" تأتي بصيغتين:
- مسار عادي:
app.dbأو./data/app.dbأو/var/lib/myapp/app.db. المسارات النسبية تُحسب انطلاقًا من مجلد العمل الحالي للعملية — وهذا غالبًا ليس ما تريده في بيئة الإنتاج. استخدم المسارات المطلقة. - صيغة URI:
file:app.db?mode=rwc&cache=shared. تتيح لك تمرير خيارات مثلmode=ro(قراءة فقط)، وmode=rwc(قراءة وكتابة وإنشاء، وهو الوضع الافتراضي)، وcache=shared، وnolock=1.
قيم خاصة ستصادفها:
:memory:— قاعدة بيانات خاصة داخل الذاكرة. كل اتصال يحصل على نسخته المستقلّة.file::memory:?cache=shared— قاعدة بيانات في الذاكرة يمكن أن تتشاركها عدة اتصالات داخل نفس العملية.""(سلسلة فارغة) — قاعدة بيانات مؤقتة وخاصة على القرص، تُحذف بمجرد إغلاقها.
في sqlite jdbc java يُسبَق الـ URI بالبادئة jdbc:sqlite:، أما sqlite pdo php فيستخدم sqlite:. وفي مشغّلات sqlite في golang ومكتبة sqlite3 في بايثون، يمكن تمرير المسار أو الـ URI مباشرة.
ماذا عن تجمّعات الاتصالات (Connection Pools)؟
SQLite قاعدة بيانات تسمح بكاتب واحد فقط. في أي لحظة، يكون هناك اتصال واحد فقط يحوز قفل الكتابة، والبقية ينتظرون دورهم. ولذلك فإن إنشاء تجمّع كبير من الكاتبين لن يجعل الكتابة أسرع — كل ما سيفعله هو زيادة عدد المتنافسين على القفل ذاته.
ومع ذلك، يبقى التجمّع الصغير مفيدًا في حالات مثل:
- القراءات المتوازية في وضع WAL، حيث لا يُعيق القرّاء بعضهم البعض ولا يُعيقون الكاتب.
- تجنّب انسداد بداية الطابور الذي يحدث عندما يُعطّل استعلام بطيء واحد التطبيق بأكمله.
إعدادات افتراضية معقولة لتطبيق ويب:
- تفعيل وضع WAL عبر
PRAGMA journal_mode WAL. - ضبط
busy_timeoutعلى بضع ثوانٍ، حتى ينتظر التطبيق بهدوء عند التزاحم بدل أن يرمي خطأ "database is locked". - حجم تجمّع يتكوّن من كاتب واحد + N من القرّاء، أو الاكتفاء باتصال واحد مشترك إذا كان الحِمل خفيفًا.
- تفعيل المفاتيح الأجنبية (foreign keys) على كل اتصال دون استثناء.
-- طبّق هذه على كل اتصال جديد:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- آمن مع WAL؛ أسرع من FULL
synchronous = NORMAL هو الإعداد المتعارف عليه مع وضع WAL — يضمن المتانة عند تعطّل التطبيق، مع تساهل بسيط في حالة تعطّل النظام نفسه، وأسرع بشكل ملحوظ من الإعداد الافتراضي FULL.
إغلاق الاتصالات (ولماذا يهم؟)
كل مكتبة (driver) عندها دالة إغلاق: conn.close() أو db.Close() أو db.close(). إهمال الإغلاق يؤدي إلى تسريب واصفات الملفات (file descriptors)، وقد يترك ملف WAL يتضخّم بلا توقف.
في الخدمات التي تعمل لفترات طويلة، النمط الشائع هو اتصال واحد (أو pool) يعيش طوال عمر العملية، بدلاً من فتح وإغلاق الاتصال مع كل طلب. فتح اتصال SQLite عملية رخيصة، لكن إعادة تطبيق الـ pragmas في كل مرة هو هدر للموارد، وسهل أن تنساه.
-- بايثون — اتصال واحد لكل عملية، يُعاد استخدامه عبر الطلبات
DB = sqlite3.connect("app.db", check_same_thread=False)
DB.execute("PRAGMA journal_mode = WAL")
DB.execute("PRAGMA busy_timeout = 5000")
DB.execute("PRAGMA foreign_keys = ON")
في بايثون تحديدًا، تحتاج إلى تمرير check_same_thread=False إذا كنت ستستخدم الاتصال من خيوط متعددة — ويُفضّل أن تستعين بقفل (lock) أو بـ pool لتنظيم الاستدعاءات بشكل متسلسل.
قائمة تحقق قبل الإطلاق إلى الإنتاج
قبل أن توجّه حركة المرور الحقيقية إلى قاعدة بيانات SQLite، راجع النقاط التالية:
- استخدم مسارًا مطلقًا لملف قاعدة البيانات.
- فعّل وضع WAL عبر
PRAGMA journal_mode = WAL. - اضبط
busy_timeoutبقيمة بين 2 و10 ثوانٍ. - فعّل المفاتيح الأجنبية (foreign keys) في كل اتصال جديد.
- اعتمد على prepared statements مع ربط المعاملات (parameter binding) — ولا تلجأ أبدًا إلى دمج النصوص مباشرة في الاستعلام.
- تأكّد من أن المجلد الذي يحوي ملف قاعدة البيانات قابل للكتابة من قِبل العملية، لأن SQLite في وضع WAL ينشئ ملفّين إضافيّين هما
-walو-shmبجوار الملف الأساسي. - فكّر في النسخ الاحتياطي قبل أن تحتاجه فعلًا — سنتطرق لاحقًا إلى
VACUUM INTOوأمر.backup.
الخطوة التالية: ترحيل المخططات (Migrations)
الاتصال بقاعدة البيانات هو الجزء السهل. الجزء الأصعب هو تطوير مخطط قاعدة البيانات (schema) مع مرور الوقت دون التعديل اليدوي على قواعد بيانات الإنتاج. هنا تأتي أهمية الـ Migrations: فهي الطريقة التي تحوّل بها ALTER TABLE إلى عملية متكررة وقابلة للتتبّع عبر نظام إدارة الإصدارات — وهذا ما سنتناوله في الصفحة التالية.
الأسئلة الشائعة
كيف أتصل بقاعدة بيانات SQLite من داخل الكود؟
كل ما تحتاجه هو توجيه الـ driver إلى مسار الملف. في Python تكتب sqlite3.connect('app.db')، وفي Node تستخدم new Database('app.db') مع مكتبة better-sqlite3، وفي Go تكتب sql.Open("sqlite", "app.db"). لا يوجد سيرفر في SQLite أصلاً، فعملية "الاتصال" ما هي إلا فتح ملف، وإذا لم يكن موجوداً فإن SQLite ينشئه تلقائياً.
ما الشكل الذي تأخذه سلسلة الاتصال في SQLite؟
معظم التعريفات تقبل إما مساراً عادياً للملف مثل ./data/app.db، أو صيغة URI مثل file:app.db?mode=rwc&cache=shared. صيغة الـ URI تتيح لك ضبط خيارات مثل وضع القراءة فقط، أو الذاكرة المؤقتة المشتركة، أو قواعد بيانات :memory:. أما JDBC فيستخدم jdbc:sqlite:app.db، و PDO يستخدم sqlite:app.db.
هل أحتاج إلى connection pool مع SQLite؟
غالباً لا، ليس بنفس الطريقة المعتادة مع Postgres أو MySQL. SQLite يُسلسل عمليات الكتابة على مستوى قاعدة البيانات نفسها، فوجود تجمّع للكُتّاب لن يزيد السرعة. تجمّع صغير قد يفيد للقراءات المتزامنة، خاصة في وضع WAL. كثير من التطبيقات تعمل بسلاسة باتصال واحد مشترك مع PRAGMA journal_mode=WAL و busy_timeout معقول.
كيف أتخلص من خطأ 'database is locked'؟
اضبط مهلة انتظار حتى ينتظر الـ driver بدلاً من أن يفشل فوراً عبر PRAGMA busy_timeout = 5000 (بالميلي ثانية). فعّل وضع WAL باستخدام PRAGMA journal_mode=WAL كي لا يُعطّل الكُتّاب القُرّاء. اجعل المعاملات قصيرة قدر الإمكان، ولا تُبقِ معاملة كتابة مفتوحة أثناء تنفيذ عمل بطيء خارج قاعدة البيانات.