Menu
flag Ar iconالعربيةdown icon
جرّب في Playground

ربط المعاملات في SQLite: ? و :name بأمان

كيف يعمل ربط المعاملات في SQLite — العناصر النائبة بالموضع، والمعاملات المسماة، والقواعد الصحيحة لتمرير القيم من تطبيقك بأمان.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

ربط المعاملات في sqlite: كيف تصل القيم إلى الاستعلام المُجهَّز

الاستعلام المُجهَّز (prepared statement) هو ببساطة جملة SQL فيها فراغات. وربط المعاملات هو عملية تعبئة هذه الفراغات بالقيم — بشكل آمن، قيمةً تلو الأخرى، عبر واجهة المشغّل (driver) بدلاً من لصق النصوص ببعضها.

الفكرة دائماً واحدة: تكتب جملة SQL مع وضع placeholder مكان كل قيمة، ثم تُمرّر القيم بشكل منفصل.

في الواجهة السطرية (CLI) لا يمكنك فعلياً استعراض ربط المعاملات في sqlite، لأن الشِل ليس متصلاً بكود تطبيق. لكن جملة SQL في الأعلى هي بالضبط ما يرسله تطبيقك. علامات ? ما هي إلا placeholder، والـ driver الذي تستخدمه — سواء sqlite3 في بايثون، أو better-sqlite3 في Node، أو rusqlite في Rust — هو الذي يملأ هذه الأماكن عبر استدعاء bind منفصل.

تخيّل الأمر هكذا: جملة SQL هي الوصفة، والقيم المربوطة هي المكونات. الاثنان لا يختلطان أبداً.

المعاملات الموضعية: ?

أبسط شكل للـ placeholder في sqlite هو ?. كل علامة استفهام تقابل القيمة التالية التي تربطها، حسب الترتيب.

INSERT INTO users (name, email) VALUES (?, ?);

في بايثون يكون الأمر هكذا:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ("روزا", "rosa@example.com"),
)

علامة ? الأولى تأخذ القيمة "روزا"، والثانية تأخذ "rosa@example.com". وإذا مرّرت عدد قيم أقل أو أكثر من اللازم، فسيُطلق المحرّك خطأً قبل تنفيذ الجملة أصلًا.

كذلك يمكنك ترقيم العلامات صراحةً بالشكل ?1 و?2 و?3، وهذا مفيد عندما تتكرّر القيمة نفسها أكثر من مرة:

SELECT ?1 AS التحية, ?1 AS لا_تزال_نفسها;

?1 يعيد استخدام أول قيمة مربوطة، فبدون الترقيم ستضطر إلى ربط نفس القيمة مرتين.

المعاملات المُسمّاة: :name

عندما يحتوي الاستعلام على أكثر من فتحتين أو ثلاث، يتحول الربط بالموضع إلى لعبة تخمين. هنا يأتي دور المعاملات المُسمّاة في sqlite لحل المشكلة:

INSERT INTO users (name, email)
VALUES (:name, :email);

في Python:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (:name, :email)",
    {"name": "Boris", "email": "boris@example.com"},
)

ترتيب المفاتيح داخل القاموس لا يهم — المهم هو الأسماء فقط. كذلك يقبل SQLite البادئتين @name و$name كبدائل، وكلها تعمل بنفس الطريقة. لكن تبقى :name هي الأكثر شيوعًا بفارق كبير.

تظهر فائدة ربط المعاملات بالأسماء فورًا عندما تواجه جملة UPDATE فيها خمسة أعمدة، أو استعلامًا يستخدم نفس القيمة في WHERE وRETURNING معًا.

ربط القيمة NULL

الطريقة الصحيحة لإدراج NULL هي تمرير قيمة الـ null الخاصة بلغتك عبر واجهة الربط (binding)، والمشغّل (driver) يتولى الترجمة بنفسه:

INSERT INTO users (name, email) VALUES (?, ?);
-- الربط: ("Cyrus", None)   في Python
-- الربط: ["Cyrus", null]   في Node

SELECT id, name, email FROM users;

سواء كانت لغتك تسميها None أو null أو nil، فإن المحرّك (driver) يحوّلها إلى NULL حقيقية في SQL. لا تربط النص "NULL"، لأن ذلك سيُخزَّن كنص من أربعة أحرف فقط. وكذلك لا تُدرج كلمة NULL داخل نص الاستعلام مباشرة، لأنك بهذا تُلغي فائدة الربط (binding) من الأساس.

نفس القاعدة تنطبق على الأرقام والـ blobs والتواريخ: مرِّر القيمة بصيغتها الأصلية في لغتك، ودع المحرّك يتولى عملية الربط.

إعادة استخدام نفس الاستعلام بقيم مختلفة

ربط المعاملات في sqlite يتكامل بشكل طبيعي مع الاستعلامات المُجهَّزة (prepared statements). جهِّز الاستعلام مرة واحدة، ثم اربط القيم ونفِّذ كما تشاء. المُحلِّل (parser) يقوم بعمله مرة واحدة فقط، وقاعدة البيانات تُعيد استخدام الخطة المُجمَّعة مع كل مجموعة قيم جديدة تُربط بها.

INSERT INTO users (name, email) VALUES (?, ?);
-- ربط ("Ada",   "ada@example.com")    -> تنفيذ
-- ربط ("Boris", "boris@example.com")  -> تنفيذ
-- ربط ("Cyrus", NULL)                 -> تنفيذ

SELECT id, name, email FROM users ORDER BY id;

معظم برامج التشغيل تغلّف هذا داخل executemany في بايثون أو حلقة .run() في Node. في كلتا الحالتين، الذي توفّره فعلياً هو تكلفة التحليل (parsing) — وهي بسيطة لكل عبارة بمفردها، لكنها تصبح ملموسة عندما تُدرج آلاف الصفوف.

لا تخلط بين الأنماط في عبارة واحدة

من الناحية التقنية، يسمح SQLite باستخدام العناصر النائبة الموضعية (?) والمسمّاة (:name) داخل نفس العبارة. تجنّب ذلك.

-- قانوني لكنه فخ خطير:
INSERT INTO users (name, email) VALUES (?, :email);

على القارئ أن يتتبّع ذهنياً واجهتَي ربط في آن واحد، كما أنّ معظم برامج التشغيل (drivers) لا تدعم الصيغة المختلطة بشكل نظيف. اختر أسلوباً واحداً لكل عبارة: استخدم ? لقيمة أو قيمتين، و:name لما سوى ذلك.

فخّ شائع: ربط المعاملات ليس تنسيقاً للنصوص

جوهر فكرة ربط المعاملات في sqlite هو أنّ القيم لا تمرّ عبر مُحلِّل SQL إطلاقاً. قارن بين هذين السطرين في بايثون:

# خطأ — تنسيق السلاسل النصية:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# صحيح — ربط المعاملات:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

السطر الأول يبني جملة SQL عن طريق دمج النصوص. لو كانت قيمة name هي "'; DROP TABLE users; --"، فإن قاعدة البيانات ستحلّل الجملة المحقونة وتنفّذها بكل بساطة. أما السطر الثاني فيُرسل جملة SQL والقيمة عبر قناتين منفصلتين — القيمة تُربط كنص فقط، بصرف النظر عن الأحرف التي تحتويها. لهذا السبب يُلحّ كل دليل تعليمي على ربط المعاملات في sqlite: المسألة ليست مسألة ذوق في الكتابة، بل تتعلق بما يراه المحلّل اللغوي للـ SQL فعلياً.

سنتعمّق في موضوع حقن SQL (SQL injection) في الصفحة التالية.

مأزق آخر: لا يمكنك ربط المعرّفات

تعمل العناصر النائبة (placeholders) مع القيم فقط — النصوص، الأرقام، البيانات الثنائية (blobs)، وقيم NULL. لكنها لا تعمل مع أسماء الجداول أو أسماء الأعمدة أو الكلمات المفتاحية في SQL:

-- هذا لا يقوم بما تريده:
SELECT * FROM ? WHERE id = ?;
-- علامة ? الأولى تُربط كسلسلة نصية حرفية، وليس كاسم جدول.

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

مثال عملي متكامل

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

في الكود الحقيقي، تُكتب جمل INSERT وSELECT كلها باستخدام placeholders. لكن سطر الأوامر CLI لا يملك تطبيقاً يربط منه القيم، فتقوم القيم الحرفية مقام ما يُنتجه الربط فعلياً.

التالي: الحماية من حقن SQL

ربط المعاملات هو الآلية المستخدمة. أما لماذا يوقف هذا الأسلوب هجمات SQL injection، والحالات القليلة التي لا يكفي فيها الربط وحده، فهذا موضوع الصفحة التالية.

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

ما المقصود بربط المعاملات في SQLite؟

ربط المعاملات يعني تمرير القيم إلى الجملة المُحضَّرة (prepared statement) بشكل منفصل عن نص الـ SQL نفسه. تكتب عنصرًا نائبًا مثل ? أو :name داخل الجملة، ثم تمرّر القيمة الفعلية عبر واجهة الربط في الـ driver. ميزة هذه الطريقة أن SQLite يتعامل مع القيم المربوطة كبيانات بحتة، ولا يحللها أبدًا كجزء من الـ SQL.

ما الفرق بين ? و :name في SQLite؟

علامة ? عنصر نائب بالموضع، أي أن القيم تُربط حسب ترتيب ظهورها في الجملة. أما :name (وكذلك @name و$name) فهي عناصر نائبة مسماة، تربط القيمة بالاسم بدلًا من الموضع. المعاملات المسماة أوضح في القراءة وأسهل في إعادة الترتيب، خاصة عندما يكون لديك أكثر من قيمتين أو ثلاث.

كيف أربط قيمة NULL في SQLite؟

مرّر قيمة null/None/nil الخاصة بلغتك عبر واجهة الربط مباشرة، وسيقوم الـ driver بترجمتها تلقائيًا إلى NULL في الـ SQL. لا تكتب أبدًا النص 'NULL' كسلسلة نصية، ولا تُدخل كلمة NULL داخل نص الاستعلام بطريقة يدوية. الفكرة كلها من الربط هي إبعاد القيم تمامًا عن محلل الـ SQL.

هل يمكن خلط المعاملات بالموضع والمعاملات المسماة في نفس الجملة؟

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

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن