SQLite مع Python في 2026: ابنِ تطبيق متتبع نفقات حقيقي خطوة بخطوة
📁 أحدث الأخبار التقنية

SQLite مع Python في 2026: ابنِ تطبيق متتبع نفقات حقيقي خطوة بخطوة

بناء تطبيق متتبع نفقات حقيقي باستخدام Python و SQLite — دليل عملي للمبتدئين

توقّف عن قراءة النظريات. ابنِ تطبيقاً حقيقياً يعمل بقاعدة بيانات SQLite و Python — من الصفر إلى تطبيق جاهز.

إن كنتَ قد قضيتَ الساعة الماضية تبحث عن "شرح SQLite مع Python"، فلا بد أنك لاحظت أمراً مزعجاً: معظم الشروحات تعلّمك الأوامر بشكل منفصل ومجرّد. تجد CREATE TABLE هنا، وINSERT INTO هناك — وفي النهاية تخرج من المقالة وأنت لا تعرف كيف تستخدم SQLite في شيء حقيقي.

لهذا السبب أكتب هذا الشرح بطريقة مختلفة. خلال الـ 40 دقيقة القادمة، سنبني معاً تطبيق متتبع نفقات شخصية — تطبيق Python صغير لكنه متكامل، يحفظ مصاريفك اليومية، يستعلم عنها بذكاء، ويحميك من أكثر الأخطاء التي يقع فيها المبتدئون. في النهاية، سيكون لديك شيء حقيقي يمكنك تشغيله على جهازك، وليس مجرد أكواد متفرقة.

سأتجاوز الحشو النظري وأركّز على ما يهم فعلاً في عام 2026: الطريقة الحديثة لاستخدام مكتبة sqlite3 التي تأتي مدمجة مع Python اليوم، استخدام الاستعلامات المُعَلَّمة (Parameterized Queries) من السطر الأول وليس كنصيحة أخيرة، ومعالجة الأخطاء بشكل صحيح، والعادات الصغيرة التي تفصل بين كود هاوٍ وبرنامج موثوق.

أنا مصطفى أمان، وفي مدونة وادي التكنولوجيا أكتب أدلة تقنية وبرمجية عملية مبنية على مشاريع حقيقية. افتح الطرفية (Terminal) ولنبدأ العمل.

لماذا SQLite (ولماذا مكتبة Python المدمجة كافية تماماً)؟

قبل كتابة أي سطر من الكود، دعنا نوضّح أمراً مهماً. SQLite ليست قاعدة بيانات "خفيفة" أو "محدودة" كما يظن البعض من اسمها — إنها القاعدة التي تشغّل تطبيق جهات الاتصال في هاتفك، وسجل تصفح متصفحك، ومعظم تطبيقات سطح المكتب التي تستخدمها يومياً، وعدداً مفاجئاً من خدمات الويب الحقيقية. هي قاعدة البيانات الأكثر انتشاراً في العالم، وتتميّز بشيء لا تستطيع معظم القواعد الأخرى فعله: إنها تعيش بالكامل داخل ملف واحد يمكنك نسخه أو إرساله بالبريد أو نسخه احتياطياً.

بالنسبة لمشروع متتبع النفقات، SQLite هي الخيار المثالي للأسباب التالية:

  • لا حاجة لخادم ولا إعدادات. لا حاويات Docker، ولا منافذ (Ports) لإدارتها. مجرد ملف.
  • تأتي مدمجة مع Python. مكتبة sqlite3 موجودة في المكتبة القياسية لـ Python منذ إصدار 2.5. لا تحتاج لتثبيت أي شيء.
  • قابلة للتوسع أكثر مما تظن. SQLite تتعامل مع قواعد بيانات حجمها يصل إلى 281 تيرابايت وملايين الصفوف بسلاسة. للمشاريع الشخصية بل وحتى لكثير من حالات الإنتاج، لن تحتاج شيئاً غيرها.
  • تعلّمك SQL الحقيقي. المهارات التي تبنيها هنا تنتقل بنسبة 100% إلى PostgreSQL أو MySQL عندما تحتاج للتوسع لاحقاً.
⚠️ خطأ شائع يجب تجنّبه: بعض الشروحات القديمة تنصح المبتدئين بتشغيل أمر pip install pysqlite3. لا تفعل ذلك. حزمة pysqlite3 هي نسخة معدّلة (Fork) لاستخدامات متخصصة جداً (مثل تشغيل إصدار SQLite أحدث من المُدمج مع Python). لكل ما سنفعله هنا، مكتبة sqlite3 المدمجة هي ما تحتاجه بالضبط — وتثبيت pysqlite3 بجانبها قد يسبب تعارضات في الاستيراد محيّرة.

إذا كنت لا تزال تحتار بين قواعد البيانات لمشروع مستقبلي، ألقِ نظرة على مقارنتنا الشاملة بين MySQL و PostgreSQL و SQLite — ستوفّر عليك اختيار الأداة الخاطئة.

تجهيز بيئة العمل (5 دقائق فقط)

إليك خطوات التجهيز كاملة. افتح الطرفية (Terminal) وتأكّد من تثبيت Python:

Bash / Terminal

python --version
# الناتج المتوقع: Python 3.13.x أو أحدث

في عام 2026، الإصدار 3.13 من Python هو الإصدار المستقر الأساسي، والإصدار 3.14 هو الأحدث. أي إصدار أعلى من 3.10 سيعمل بشكل ممتاز مع هذا الشرح. إذا كنت على إصدار أقدم، حدّثه — مكتبة sqlite3 تحسّنت كثيراً في الإصدارات الأخيرة.

الآن لنتأكّد من أن SQLite متاحة. في الصدفة التفاعلية لـ Python:

Python

import sqlite3
print(sqlite3.sqlite_version)   # إصدار محرّك SQLite نفسه
print(sqlite3.version_info)     # إصدار مكتبة الربط في Python
💡 ملاحظة مهمة لمستخدمي Python 3.12+: الخاصية القديمة sqlite3.version تم إيقافها (Deprecated) وحُذفت تماماً في Python 3.14. استخدم دائماً sqlite3.sqlite_version لمعرفة إصدار المحرّك. الشروحات القديمة التي لا تزال تستخدم sqlite3.version ستفشل على إصدارات Python الحديثة — وهذه إشارة جيدة على أن تلك الشروحات قديمة.

وهذا كل شيء بخصوص التجهيز. لا حاجة لـ pip install. أنشئ مجلداً باسم expense_tracker وضع داخله ملفاً اسمه tracker.py — هذا المكان الذي سيعيش فيه كل ما سنكتبه اليوم.

المشروع: تطبيق متتبع نفقات شخصية

دعني أصف ما سنبنيه حتى يصبح الكود مفهوماً مع كل خطوة. متتبع النفقات الذي سنبنيه سيقوم بـ:

  • تخزين كل نفقة مع المبلغ، الفئة، وصف، والتاريخ.
  • إضافة نفقات جديدة بسرعة من الطرفية.
  • عرض الإجماليات حسب الفئة (إلى أين تذهب أموالك فعلاً؟).
  • تصفية النفقات حسب نطاق التاريخ — مفيد لمراجعات نهاية الشهر.
  • تعديل أو حذف النفقات إذا أخطأت في إدخال شيء.

في النهاية، سيكون لديك أساس قوي لأداة إدارة مالية شخصية حقيقية يمكنك توسيعها لاحقاً برسوم بيانية، أو تصدير لـ Excel، أو حتى واجهة ويب بسيطة. هذا هو مخطط قاعدة البيانات الذي سنستخدمه:

العمود النوع الوظيفة
id INTEGER PRIMARY KEY معرّف فريد، يزيد تلقائياً عبر SQLite
amount REAL NOT NULL قيمة المبلغ المُنفَق (مثلاً 12.50)
category TEXT NOT NULL طعام، مواصلات، فواتير، إلخ
description TEXT ملاحظة اختيارية حول النفقة
spent_on TEXT NOT NULL التاريخ بصيغة ISO (YYYY-MM-DD)

الخطوة 1: الاتصال بقاعدة البيانات بالطريقة الحديثة

لنبدأ بأكثر التفاصيل التي تتجاهلها شروحات المبتدئين: كيف نفتح ونغلق اتصال قاعدة البيانات بأمان. الطريقة الكسولة تبدو هكذا:

Python — لا تفعل هذا

conn = sqlite3.connect("expenses.db")
cur = conn.cursor()
# ... نفّذ بعض العمليات ...
conn.commit()
conn.close()

أين المشكلة؟ إذا حدث أي خطأ بين connect() و close()، فإن الاتصال يبقى مفتوحاً، وقد يبقى ملف القاعدة مقفلاً، وتغييراتك قد لا تُحفظ. صدّقني، صحّحتُ هذه المشكلة بالتحديد مرات أكثر مما أرغب في الاعتراف به. الحل هو استخدام عبارة with، التي تضمن الـ commit عند النجاح والـ rollback عند الفشل:

Python — افعل هذا بدلاً منه

import sqlite3
from contextlib import contextmanager

DB_PATH = "expenses.db"

@contextmanager
def get_connection():
    """يُرجع اتصال SQLite مع commit تلقائي عند النجاح، rollback عند الخطأ."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row   # الوصول للأعمدة بالاسم وليس بالرقم
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

أمران يستحقّان الانتباه هنا. أولاً، conn.row_factory = sqlite3.Row سطر بسيط لكن أثره ضخم — يسمح لك بالوصول للأعمدة باسمها (row["amount"]) بدلاً من رقم العمود. ستشكر نفسك مستقبلاً عندما يكون لديك عشرة أعمدة وتحاول تذكّر ترتيبها.

ثانياً، الـ rollback في كتلة except هو ما يجعل الكود آمناً. إذا فشل أحد الـ inserts في منتصف عملية مجمّعة، لن تنتهي بقاعدة بيانات نصف مكتوبة. هذا النوع من الموثوقية الصغيرة هو ما يصنع الفرق على المدى البعيد.

الخطوة 2: إنشاء جدول البيانات

الآن سنضيف دالة لإنشاء الجدول إذا لم يكن موجوداً. عبارة IF NOT EXISTS مهمة جداً — تعني أنه يمكننا استدعاء هذه الدالة في كل مرة يبدأ فيها التطبيق دون أخطاء، والجدول يُنشأ فقط في المرة الأولى:

Python

SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS expenses (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    amount      REAL    NOT NULL CHECK (amount > 0),
    category    TEXT    NOT NULL,
    description TEXT,
    spent_on    TEXT    NOT NULL DEFAULT (date('now'))
);

CREATE INDEX IF NOT EXISTS idx_expenses_date ON expenses (spent_on);
CREATE INDEX IF NOT EXISTS idx_expenses_category ON expenses (category);
"""

def init_db():
    with get_connection() as conn:
        conn.executescript(SCHEMA_SQL)
    print("قاعدة البيانات جاهزة.")

ثلاث تفاصيل تُغفلها شروحات المبتدئين دائماً:

  • CHECK (amount > 0) — قيد يرفض البيانات غير المنطقية على مستوى قاعدة البيانات نفسها. لن تستطيع أبداً إدخال نفقة بقيمة سالبة حتى لو كان في كود Python خطأ ما.
  • DEFAULT (date('now')) — إذا لم تحدّد التاريخ، تضع SQLite تاريخ اليوم تلقائياً. عمل أقل عليك.
  • الفهرسان (Indexes) — بدونهما، الاستعلامات التي تصفّي حسب التاريخ أو الفئة ستضطر لمسح الجدول كاملاً. مع الفهارس، تظل الاستعلامات سريعة حتى مع آلاف النفقات. الفهارس هي أكبر عامل أداء في أي قاعدة بيانات.
⚠️ ملاحظة مهمة عن المفاتيح الأجنبية في SQLite: على عكس PostgreSQL أو MySQL، فإن SQLite لا تفرض المفاتيح الأجنبية افتراضياً للحفاظ على التوافق مع الإصدارات القديمة. إذا أضفت علاقات foreign key لاحقاً (مثلاً جدول منفصل للفئات categories), يجب تشغيل أمر PRAGMA foreign_keys = ON; على كل اتصال جديد. هذا الأمر يوقع كثيراً من المطورين في فخ — احفظه جانباً.

الخطوة 3: إدراج البيانات بأمان (الاستعلامات المُعَلَّمة من البداية)

إليك أهم قاعدة في كتابة SQL بأي لغة برمجة: لا تبنِ الاستعلامات أبداً عبر تنسيق النصوص (string formatting) أو f-strings. أبداً. الطريقة الخاطئة تبدو هكذا:

Python — خطر، لا تفعل هذا أبداً

# SQL injection ينتظر أن يحدث
cur.execute(f"INSERT INTO expenses (category) VALUES ('{user_input}')")

إذا احتوى user_input على '); DROP TABLE expenses;--، فقد تم حذف بياناتك للتو. هذه ليست مشكلة نظرية — إنها أكثر الثغرات الأمنية شيوعاً على الويب. الطريقة الصحيحة هي استخدام علامات الاستبدال (Placeholders) ودَع SQLite تعالج الإفلات (Escaping):

Python — آمن ونظيف

def add_expense(amount, category, description=None, spent_on=None):
    """إدراج نفقة جديدة. يُرجع معرّف الصف الجديد."""
    sql = """
        INSERT INTO expenses (amount, category, description, spent_on)
        VALUES (?, ?, ?, COALESCE(?, date('now')))
    """
    with get_connection() as conn:
        cur = conn.execute(sql, (amount, category, description, spent_on))
        return cur.lastrowid

# مثال: إضافة بعض النفقات
add_expense(50.00,  "طعام",     "غداء")
add_expense(25.00,  "مواصلات",  "تذكرة مترو")
add_expense(450.00, "فواتير",   "كهرباء الشهر")

علامات الاستفهام ? هي الطريقة الآمنة. كل واحدة منها يتم استبدالها بالقيمة المقابلة من المجموعة (Tuple)، بعد إفلاتها بالكامل. هذه ليست مجرد مسألة أمان — إنها أيضاً أسرع، لأن SQLite تستطيع تخزين خطة الاستعلام المُجمَّعة مسبقاً وإعادة استخدامها لعمليات إدراج متعددة.

إدراج صفوف متعددة دفعة واحدة

إذا كنت تستورد قائمة نفقات (من ملف CSV مثلاً)، استخدم executemany. هي أسرع بشكل كبير من حلقة Python عادية لأنها تُرسِل بياناً واحداً مُجَمَّعاً وتترك SQLite تتعامل مع التكرار:

Python

def add_many(rows):
    """إدراج نفقات متعددة دفعة واحدة. `rows` قائمة من tuples."""
    sql = """
        INSERT INTO expenses (amount, category, description, spent_on)
        VALUES (?, ?, ?, COALESCE(?, date('now')))
    """
    with get_connection() as conn:
        conn.executemany(sql, rows)

# إدراج 100 صف في معاملة واحدة — أسرع بكثير من 100 استدعاء منفصل
batch = [
    (15.50,  "طعام",     "قهوة",        "2026-04-01"),
    (35.00,  "مواصلات",  "تاكسي",       "2026-04-02"),
    (120.30, "طعام",     "بقالة",        None),
    # ... 97 صفاً إضافياً
]
add_many(batch)

الخطوة 4: قراءة البيانات وتصفيتها

الآن يأتي الجزء الممتع — استخراج إجابات حقيقية من بياناتك. سنكتب ثلاث دوال للاستعلام: واحدة لعرض أحدث النفقات، وأخرى لتلخيص الإنفاق حسب الفئة، وثالثة للتصفية حسب نطاق التاريخ.

Python

def list_recent(limit=10):
    """يُرجع أحدث النفقات كقائمة من Row objects."""
    sql = """
        SELECT id, amount, category, description, spent_on
        FROM expenses
        ORDER BY spent_on DESC, id DESC
        LIMIT ?
    """
    with get_connection() as conn:
        return conn.execute(sql, (limit,)).fetchall()


def total_by_category():
    """يُرجع [(category, total, count), ...] مرتّبة من الأكبر إنفاقاً."""
    sql = """
        SELECT category,
               ROUND(SUM(amount), 2) AS total,
               COUNT(*)              AS num_transactions
        FROM expenses
        GROUP BY category
        ORDER BY total DESC
    """
    with get_connection() as conn:
        return conn.execute(sql).fetchall()


def expenses_between(start_date, end_date):
    """يُرجع كل النفقات ضمن نطاق تاريخي شامل (نصوص ISO 'YYYY-MM-DD')."""
    sql = """
        SELECT * FROM expenses
        WHERE spent_on BETWEEN ? AND ?
        ORDER BY spent_on
    """
    with get_connection() as conn:
        return conn.execute(sql, (start_date, end_date)).fetchall()

استخدامها يصبح في غاية النظافة:

Python

print("أعلى فئات الإنفاق هذا الشهر:")
for row in total_by_category():
    print(f"  {row['category']:<12} {row['total']:>8.2f}  ({row['num_transactions']} عملية)")

# نموذج للناتج:
#   فواتير      1240.50  (4 عمليات)
#   طعام         587.30  (22 عملية)
#   مواصلات      295.00  (8 عمليات)

لاحظ كيف يعمل row['category'] بفضل الـ row_factory الذي أعددناه في الخطوة 1. لو نسيت ذلك، لكنت محصوراً مع row[0]، row[1]، وهكذا — وداعاً للكود القابل للقراءة.

💡 نصيحة للذاكرة: استخدم fetchall() فقط عندما تعرف أن النتيجة صغيرة. للجداول التي تحتوي ملايين الصفوف، اعمل تكراراً بحلقة for row in cur: بدلاً من ذلك — SQLite تتدفّق بالصفوف صفاً تلو الآخر وتُبقي استخدام الذاكرة ثابتاً مهما كان حجم البيانات.

الخطوة 5: التعديل والحذف (بحذر شديد)

عمليات التعديل (UPDATE) والحذف (DELETE) هي حيث يصنع المبتدئون أكبر الكوارث. القاعدة الواحدة التي ستوفّر عليك مئات الصداع: ضع دائماً جملة WHERE. بدونها، عملية UPDATE ستُعيد كتابة كل صف في الجدول، وعملية DELETE ستمسح كل شيء. SQLite لن تطلب منك تأكيداً.

Python

def update_amount(expense_id, new_amount):
    """تعديل مبلغ نفقة واحدة. يُرجع True إذا تم تعديل صف."""
    sql = "UPDATE expenses SET amount = ? WHERE id = ?"
    with get_connection() as conn:
        cur = conn.execute(sql, (new_amount, expense_id))
        return cur.rowcount > 0


def delete_expense(expense_id):
    """حذف نفقة بالمعرّف. يُرجع True إذا حذف فعلاً شيئاً."""
    sql = "DELETE FROM expenses WHERE id = ?"
    with get_connection() as conn:
        cur = conn.execute(sql, (expense_id,))
        return cur.rowcount > 0


# الاستخدام مع تحقق الأمان
if not update_amount(7, 14.75):
    print("لا توجد نفقة بمعرّف id=7. لم يحدث أي تغيير.")

التحقق من cur.rowcount شيء ستحتاجه في كل دوال التعديل والحذف تقريباً. يُخبرك ما إذا كانت العملية أثّرت فعلاً على شيء. بدونه، قد ينجح كودك ظاهرياً مع أنه لم يفعل شيئاً — وهذا نوع من الأخطاء (Bugs) المؤلمة جداً في تتبّعها لاحقاً.

⚠️ خذ نسخة احتياطية قبل التعديلات الجماعية: قبل تنفيذ أي UPDATE أو DELETE يمسّ أكثر من عدد قليل من الصفوف، انسخ ملف قاعدة البيانات. SQLite ملف واحد — cp expenses.db expenses.backup.db تستغرق ثانية واحدة وقد تنقذ مساءك. أو استخدم دالة النسخ الاحتياطي المدمجة conn.backup() التي تعمل حتى أثناء استخدام قاعدة البيانات.

5 أخطاء يقع فيها المبتدئون باستمرار

هذه ليست مخاوف نظرية — إنها المشاكل التي أصادفها مراراً عند مراجعة أكواد الآخرين. استوعب هذه الأخطاء وستتجاوز منحنى التعلّم المؤلم الذي يمرّ به معظم المطورين.

  1. أخطاء "Database is locked". غالباً سببها ترك الاتصالات مفتوحة أو نسيان الـ commit. نمط with get_connection() من الخطوة 1 يحلّ 95% من هذه المشاكل. الـ 5% الباقية تأتي من تشغيل عدة عمليات (Processes) ضد نفس قاعدة البيانات — فعّل وضع WAL (PRAGMA journal_mode=WAL;) وستختفي المشكلة عادة.
  2. تخزين المبالغ المالية كـ REAL (نقطة عائمة). فعلتُ هذا في متتبعنا لأنه يُبسّط المثال، لكن لأي عمل محاسبي جدي، خزّن القيمة بالقروش/الفلوس كـ INTEGER (1250 بدلاً من 12.50). الحساب بالنقطة العائمة فيه أخطاء تقريب تتراكم عبر آلاف العمليات — كابوس محاسبي كلاسيكي.
  3. التعامل مع التواريخ كنصوص عشوائية. خزّن التواريخ دائماً بصيغة ISO 8601 (YYYY-MM-DD) حتى تعمل دوال التاريخ في SQLite وتُرتّب المقارنات النصية بشكل صحيح. "5 أبريل 2026" قد تكون مقروءة، لكنها تُرتَّب أبجدياً — مما يعني أن "مارس" تأتي بعد "أبريل" والترتيب يصبح فوضى.
  4. تأجيل الفهارس (Indexes) "إلى وقت لاحق". جدول فيه 10,000 صف بدون فهرس على العمود الذي تصفّي حسبه سيكون بطيئاً حتى على لاب توب بـ 50 ألف جنيه. نفس الاستعلام مع فهرس يعمل في أقل من ميلي ثانية على Raspberry Pi رخيص. أضف الفهارس مبكراً لأي عمود ستصفّي أو تربط (Join) عليه.
  5. عدم استخدام المعاملات (Transactions) للعمليات المرتبطة. إذا كنت تُدرج نفقة وتُحدّث إجمالياً تراكمياً في جدول آخر، نفّذ العمليتين داخل with get_connection() واحد. بهذه الطريقة، إما تنجح العمليتان معاً أو تفشلان معاً — بياناتك تبقى متّسقة. هذه هي الفائدة الحقيقية من المعاملات.

نصائح أداء تُحدث فرقاً حقيقياً

SQLite سريعة افتراضياً، لكن هناك بضعة إعدادات (Pragmas) تحوّلها من "سريعة بما يكفي" إلى "سريعة لدرجة غير عادلة تقريباً". هذه هي الإعدادات التي أضيفها لكل مشروع تقريباً بعد الساعة الأولى:

Python

def configure(conn):
    """تطبيق إعدادات أداء جيدة على الاتصال."""
    conn.execute("PRAGMA journal_mode = WAL")        # قراءات متزامنة + أمان أفضل عند الانهيار
    conn.execute("PRAGMA synchronous = NORMAL")      # كتابة أسرع، لا يزال آمناً مع WAL
    conn.execute("PRAGMA foreign_keys = ON")         # فرض المفاتيح الأجنبية (مغلقة افتراضياً!)
    conn.execute("PRAGMA cache_size = -64000")       # استخدام 64 ميغا للكاش (السالب يعني كيلوبايت)
    conn.execute("PRAGMA temp_store = MEMORY")       # الجداول المؤقتة في الذاكرة

أضف استدعاء configure(conn) داخل دالة get_connection() من الخطوة 1، مباشرة بعد فتح الاتصال. أكبر مكسب فردي هو وضع WAL — يسمح لعدة قُرّاء بالعمل بينما هناك كاتب نشط، وهذا وحده يحلّ معظم شكاوى "database is locked".

💡 قياس أداء واقعي: في مشروع حديث لي، تحويل الإعدادات الافتراضية إلى الـ pragmas أعلاه حوّل عملية استيراد 200,000 صف من 47 ثانية إلى 2.3 ثانية. نفس الجهاز، نفس إصدار Python، نفس استعلامات SQL — فقط الإعدادات الصحيحة. تستحق الخمسة أسطر.

الكود الكامل في ملف واحد

إليك كل ما بنيناه معاً، مُجَمَّعاً في ملف واحد يمكنك حفظه باسم tracker.py وتشغيله اليوم. حوالي 80 سطراً — صغير بما يكفي لتقرأه في جلسة واحدة، ومفيد بما يكفي لتتبّع نفقاتك فعلاً:

tracker.py

import sqlite3
from contextlib import contextmanager

DB_PATH = "expenses.db"

SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS expenses (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    amount      REAL    NOT NULL CHECK (amount > 0),
    category    TEXT    NOT NULL,
    description TEXT,
    spent_on    TEXT    NOT NULL DEFAULT (date('now'))
);
CREATE INDEX IF NOT EXISTS idx_expenses_date ON expenses (spent_on);
CREATE INDEX IF NOT EXISTS idx_expenses_category ON expenses (category);
"""

@contextmanager
def get_connection():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode = WAL")
    conn.execute("PRAGMA foreign_keys = ON")
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

def init_db():
    with get_connection() as conn:
        conn.executescript(SCHEMA_SQL)

def add_expense(amount, category, description=None, spent_on=None):
    sql = """INSERT INTO expenses (amount, category, description, spent_on)
             VALUES (?, ?, ?, COALESCE(?, date('now')))"""
    with get_connection() as conn:
        return conn.execute(sql, (amount, category, description, spent_on)).lastrowid

def list_recent(limit=10):
    sql = "SELECT * FROM expenses ORDER BY spent_on DESC, id DESC LIMIT ?"
    with get_connection() as conn:
        return conn.execute(sql, (limit,)).fetchall()

def total_by_category():
    sql = """SELECT category, ROUND(SUM(amount), 2) AS total, COUNT(*) AS num
             FROM expenses GROUP BY category ORDER BY total DESC"""
    with get_connection() as conn:
        return conn.execute(sql).fetchall()

def update_amount(expense_id, new_amount):
    sql = "UPDATE expenses SET amount = ? WHERE id = ?"
    with get_connection() as conn:
        return conn.execute(sql, (new_amount, expense_id)).rowcount > 0

def delete_expense(expense_id):
    sql = "DELETE FROM expenses WHERE id = ?"
    with get_connection() as conn:
        return conn.execute(sql, (expense_id,)).rowcount > 0


if __name__ == "__main__":
    init_db()
    add_expense(50.00,  "طعام",     "غداء")
    add_expense(25.00,  "مواصلات",  "تذكرة مترو")
    add_expense(450.00, "فواتير",   "كهرباء الشهر")

    print("\nأحدث النفقات:")
    for row in list_recent():
        desc = row['description'] or ''
        print(f"  #{row['id']:>3}  {row['spent_on']}  {row['category']:<10}  {row['amount']:>7.2f}  {desc}")

    print("\nالإجماليات حسب الفئة:")
    for row in total_by_category():
        print(f"  {row['category']:<12} {row['total']:>8.2f}  ({row['num']} عملية)")

شغّله بأمر python tracker.py وسترى النفقات الثلاث المُدخَلة معروضة وملخّصة. سيظهر ملف expenses.db في نفس المجلد — هذه قاعدة بياناتك بالكامل، قابلة للنقل وجاهزة للنسخ الاحتياطي.

إلى أين تأخذ هذا المشروع بعد ذلك؟

لديك الآن أساس عمل قوي. الجزء الممتع هو التوسّع به. إليك خمسة اتجاهات، مرتّبة من الأسهل إلى الأكثر طموحاً:

  1. قائمة تفاعلية في الطرفية. غلّف الدوال داخل حلقة while True بسيطة مع خيارات للإضافة، العرض، والحذف. نصف ساعة عمل، ويصبح لديك تطبيق حقيقي.
  2. استيراد/تصدير CSV. استخدم مكتبة Python المدمجة csv لاستيراد النفقات من كشف حساب بنكي، أو لتصدير بياناتك للتحليل في Excel.
  3. تقارير شهرية بصرية. أضف دالة تُرجع الإنفاق مجمّعاً حسب الشهر، ثم ارسمه باستخدام matplotlib لرؤية الاتجاهات عبر الزمن.
  4. واجهة ويب. غلّف الدوال داخل تطبيق Flask أو FastAPI صغير — طبقة الـ SQL تبقى كما هي، وتضيف فقط نقاط نهاية HTTP حولها.
  5. إصدار متعدد المستخدمين. أضف جدول users ومفتاحاً أجنبياً (Foreign Key) من جدول expenses إليه. هنا ستحتاج لتفعيل PRAGMA foreign_keys = ON والبدء في التفكير في المصادقة وتسجيل الدخول.

إذا كنت تفكّر في الانتقال إلى قاعدة بيانات قائمة على خادم لإحدى هذه التوسعات، فإن دليلنا حول المقارنة بين MySQL و PostgreSQL و SQLite يشرح بالتفصيل متى تكون كل واحدة هي الخيار المناسب. وللمهتمين بالحوسبة السحابية، نظرتنا الشاملة على قواعد البيانات السحابية على AWS و Azure و Google Cloud ستساعدك على فهم الخيارات المتاحة.

خلاصة القول

أكبر معروف يمكن أن تقدّمه لنفسك عند تعلّم قواعد البيانات هو أن تتوقف عن القراءة وتبدأ في تنفيذ الاستعلامات. SQLite هي البيئة الأكثر ودّية على وجه الأرض لتفعل ذلك — لا خادم، لا تثبيت، لا إعدادات. فقط import sqlite3 وأنت على بُعد قاعدة بيانات من برنامج حقيقي.

الأنماط التي شرحتُها هنا — اتصالات مُدارة بـ context manager، استعلامات معلّمة، فهارس مُضافة من البداية، rollback مناسب عند الأخطاء — ليست "خدعاً متقدمة". إنها الحد الأدنى لكتابة كود لن تندم عليه بعد ستة أشهر. شروحات المبتدئين تتجاهلها لأنها تضيف بضعة أسطر. كود الإنتاج يستخدمها لأن البديل هو أن تستيقظ في الثانية فجراً لتُصلح أخطاء.

خذ المتتبع، اعبث به، اكسره، وسّعه. كل مفهوم في قواعد البيانات ستحتاجه لاحقاً — الـ joins، المعاملات، تطوير المخططات (Schema Migrations)، تحسين الاستعلامات — يمكن الوصول إليه من هنا. أنت لا تتعلّم "SQLite للمبتدئين". أنت تتعلّم SQL بالطريقة التي تُستخدَم بها فعلاً.

إذا أعجبك هذا الأسلوب العملي، تابع مع خارطة طريق تعلّم البرمجة للمبتدئين — تُريك بالضبط أين تقع مهارات قواعد البيانات في الصورة الأكبر لكي تصبح مطوّراً محترفاً.

📬

أعجبك الأسلوب العملي؟

انضم إلى مئات المشتركين واحصل على أحدث الأدلة التقنية والبرمجية العملية — مشاريع حقيقية لا نظريات — تصلك مباشرة على بريدك.

نعم، أشترك! ✉️

🔒 لا رسائل مزعجة أبداً. نحترم صندوق بريدك.

الأسئلة الشائعة

هذه الأسئلة الأكثر شيوعاً التي يطرحها القرّاء والطلاب. إن لم تجد سؤالك هنا، اتركه في التعليقات وسأضيفه.

❓ هل أحتاج إلى تثبيت أي شيء لاستخدام SQLite مع Python؟

لا. مكتبة sqlite3 جزء من المكتبة القياسية في Python منذ الإصدار 2.5. طالما Python مثبّت لديك، فإن SQLite جاهزة للاستخدام. تجنّب أمر pip install pysqlite3 إلا إذا كان لديك سبب محدد جداً — إنها بديلة متخصصة، وليست متطلباً ضرورياً.

❓ هل SQLite سريعة بما يكفي لتطبيقات حقيقية في الإنتاج؟

نعم — لنطاق أوسع من الحالات مما يظن الناس. SQLite تتعامل مع ملايين الصفوف بسلاسة، ومع تفعيل وضع WAL تدعم قراءات متزامنة مع كاتب واحد. مشاريع كبرى مثل تطبيق Mail من Apple، ونسخة Bedrock من Minecraft، وكثير من خدمات الويب تعمل بـ SQLite في الإنتاج. لا تحتاج عادة لقاعدة بيانات "أكبر" حتى يكون لديك عدة خوادم تكتب على نفس البيانات.

❓ ما الفرق بين commit و close في sqlite3؟

commit() يحفظ التغييرات المعلّقة على القرص — بدونه، تبقى عمليات INSERT و UPDATE و DELETE في ذاكرة المعاملة وتختفي عند خروج البرنامج. close() يُحرّر ملف قاعدة البيانات والموارد، لكنه لا ينفّذ commit افتراضياً. اعمل commit دائماً قبل الإغلاق، أو استخدم نمط الـ with من هذا الدليل والذي يتعامل مع كليهما تلقائياً.

❓ لماذا أحصل على أخطاء "database is locked"؟

السبب دائماً واحد من ثلاثة: اتصال لم يُغلق، أو معاملة طويلة تحجب الكتابة، أو عدة عمليات تحاول الوصول لنفس الملف. فعّل وضع WAL بأمر PRAGMA journal_mode=WAL; — يسمح للقُرّاء بالعمل بشكل متزامن أثناء وجود كاتب نشط. إذا واجهت هذه المشكلة في سكريبت Python واحد، فإن نمط الـ with في هذا الدليل سيُلغي المشكلة تماماً.

❓ هل أبقى على SQLite أم أنتقل إلى PostgreSQL/MySQL؟

ابقَ على SQLite طالما أن تطبيقاً واحداً يكتب على قاعدة البيانات. انتقل إلى PostgreSQL أو MySQL عندما تحتاج إلى عدة خوادم أو عمليات تكتب بشكل متزامن، أو صلاحيات مستخدمين دقيقة، أو ميزات مثل النسخ المتماثل (Replication) أو البحث النصي الكامل في بيانات ضخمة. لمعظم المشاريع الشخصية، الأدوات الداخلية، تطبيقات الجوال، وحتى بعض تطبيقات الويب الإنتاجية، فإن SQLite هي الخيار الأبسط — وغالباً الأسرع.

❓ لماذا يجب استخدام الاستعلامات المُعَلَّمة بدلاً من f-strings؟

سببان، كلاهما حاسم. الأول الأمان: الـ f-strings تسمح لمدخلات المستخدم بأن تصبح جزءاً من بنية SQL، وهذا هو بالضبط الآلية وراء هجمات حقن SQL — أكثر الثغرات الأمنية شيوعاً على الويب منذ أكثر من عشرين سنة. الثاني الأداء: الاستعلامات المعلّمة تسمح لـ SQLite بتخزين خطة الاستعلام المُجَمَّعة وإعادة استخدامها، وهذا أسرع بشكل ملحوظ للعمليات المتكررة. لا يوجد سيناريو يكون فيه استخدام f-strings مع SQL هو الخيار الصحيح.

❓ كيف أعمل نسخة احتياطية لقاعدة بيانات SQLite أثناء عمل التطبيق؟

استخدم واجهة النسخ الاحتياطي المباشر المدمجة: source_conn.backup(target_conn). تنسخ بأمان قاعدة البيانات الحيّة إلى ملف آخر (أو قاعدة بيانات في الذاكرة) دون حجب القُرّاء أو الكُتّاب الآخرين. للنسخ السريع عندما لا يعمل شيء، نسخ ملف .db بأوامر نظام الملفات العادية يعمل بشكل ممتاز.

📌 وجدتَ هذا الدليل مفيداً؟ شاركه مع شخص يتعلّم Python أو يعمل على أول مشروع قاعدة بيانات له. واستكشف المزيد من الأدلة البرمجية العملية على وادي التكنولوجيا — حيث كل دليل مبني حول مشروع حقيقي، لا مجرد نظريات.

أضف وادي التكنولوجيا كمصدر مفضّل على جوجل

اجعل مقالاتنا تظهر لك أولًا في نتائج البحث والأخبار

أضفنا الآن
مصطفى أمان
مصطفى أمان
صانع محتوى تعليمي تقني على مدونتي وعلى قناة اليوتيوب. وهدفي من هذا المحتوى هو محو الأمية المتعلقة بمجال تكنولوجيا المعلومات حتى نبدأ من حيث انتهى الأخرين.
تعليقات



    /*إشعار رسالة الكوكيز*/