تستعرض هذه المقالة أفضل الممارسات المُتعارف عليها لكتابة شيفرات SQL نظيفة، وكذلك تأمين الشيفرات عبر التحوّط من هجمات حقن SQL.
الشيفرات البرمجية النظيفة في SQL
هذه بعض النصائح والقواعد حول كيفية كتابة استعلامات SQL تراعي أفضل الممارسات، وذات مقروئية عالية.
تنسيق وتهجئة الكلمات المفتاحية والأسماء
أسماء الجداول والأعمدة
هناك طريقتان شائعتان لكتابة أسماء الجداول والأعمدة، وهما CamelCase
و snake_case
كما يوضح المثال التالي:
SELECT FirstName, LastName FROM Employees WHERE Salary > 500; SELECT first_name, last_name FROM employees WHERE salary > 500;
يجب أن تعطي الأسماء فكرة عمّا هو مُخزّن في الكائن المُسمّى. هناك نقاش محتدم حول ما إذا كان الأفضل أن تكون أسماء الجداول بصيغة المفرد أو الجمع، ولكنّ الشائع استخدام صيغة الجمع.
تُنقِص إضافة سابقات أو لاحقات، مثل tbl
أو col
، إلى الأسماء مقروئية الشيفرة، لذلك يُفضل تجنبها. إلا أنّها قد تكون ضرورية في بعض الأحيان لتجنّب التداخل مع الكلمات المفتاحية في SQL، وغالبًا ما تُستخدم مع الزنادات (triggers) والفهارس (والتي لا تُذكر أسماؤها في الاستعلامات عادةً).
الكلمات المفتاحية
الكلمات المفتاحية في SQL ليست حسّاسة لحالة الأحرف. ولكن تغلُب كتابتها بأحرف كبيرة.
المسافات البادئة Indenting
لا يوجد معيار مقبول ومُوحّد للمسافات البادئة. لكنّ الجميع يتفق على أنّ حشر كل شيء في سطر واحد أمر سيء مثل:
SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC;
أضعف الإيمان أن تضع كل عبارة في سطر جديد، مع تقسيم السطور الطويلة:
SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC;
في بعض الأحيان، تُوضع نفس المسافة البادئة قبل الأسطر التي تعقُب الكلمات المفتاحية في SQL:
SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC;
(يمكن القيام بذلك أيضًا عند محاذاة الكلمات المفتاحية في SQL إلى اليمين.)
هناك طريقة شائعة أخرى، وهي وضع الكلمات المفتاحية المهمّة في سطور خاصّة على النحو التالي:
SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC;
تُحسّن المحاذاة الرأسية للعبارات المتماثلة مقروئية الشيفرة:
SELECT Model, EmployeeID FROM Cars WHERE CustomerID = 42 AND Status = 'READY';
استخدام عدّة أسطر يصعّب تضمين أوامر SQL في لغات البرمجة الأخرى. إلا أنّ العديد من اللغات لديها آلية للتعامل مع السلاسل النصية متعددة الأسطر، مثل @"..."
في C# أو """..."""
في Python أو R"(...)"
في C++.
SELECT *
تعيد العبارة SELECT *
جميع الأعمدة بنفس ترتيب ظهورها في الجدول. عند استخدام SELECT *
، فقد تتغيّر البيانات المُعادة من الاستعلام كلّما تغيّر تعريف الجدول. وهذا يضعف توافقية الإصدارات المختلفة من التطبيق أو قاعدة البيانات مع بعضها بعضًا.
علاوة على ذلك، فإنّ قراءة الأعمدة غير الضرورية قد يرفع من مساحة القرص المُستخدَمة، والدخل / الخرج الشبكي (network I/O). لذا عليك دائمًا تحديد العمود (أو الأعمدة) الذي تريد استردادها صراحة:
SELECT * -- تجنّب هذا SELECT ID, FName, LName, PhoneNumber -- هذا أفضل FROM Emplopees;
(لا تنطبق هذه الاعتبارات عند إجراء استعلامات تفاعلية - interactive queries.)
بالمقابل، ليس هناك ضرر من استخدام SELECT *
في استعلام فرعي لعبارة EXISTS
، ذلك أنّ EXISTS
تتجاهل البيانات الفعلية على أيّ حال (إذ تكتفي بالتحقق من أنّه تمّ العثور على صفّ واحد على الأقل). للسبب نفسه، لا فائدة من إدراج أيّ عمود (أو أعمدة) معيّنة في عبارة EXISTS
، لذلك يُفضّل استخدام SELECT *
:
-- سرد الأقسام التي لم يُعيّن فيها أيّ موظف حديثا SELECT ID, Name FROM Departments WHERE NOT EXISTS (SELECT * FROM Employees WHERE DepartmentID = Departments.ID AND HireDate >= '2015-01-01');
عمليات الضمّ Joins
يجب دائمًا استخدام عمليات الضمّ الصريحة (Explicit joins)؛ لأنّ عمليات الضمّ الضمنية (implicit joins) تطرح العديد من المشاكل، مثلًا:
-
في عمليات الضمّ الضمنية، يكون شرط الضمّ داخل عبارة
WHERE
مخلوطًا مع شروط أخرى. وذلك يصعّب معرفة الجداول المضمومة، وكيفية ضمّها. - بسبب النقطة أعلاه، يتعاظم خطر حدوث أخطاء.
- في SQL القياسية، عمليات الضمّ الصريحة هي الطريقة الوحيدة لاستخدام الضمّ الخارجي:
SELECT d.Name, e.Fname || e.LName AS EmpName FROM Departments AS d LEFT JOIN Employees AS e ON d.ID = e.DepartmentID;
-
يتيح الضمّ الصريح استخدام عبارة
USING
كما يوضّح المثال التالي:
SELECT RecipeID, Recipes.Name, COUNT(*) AS NumberOfIngredients FROM Recipes LEFT JOIN Ingredients USING (RecipeID);
(يتطلب هذا أن يستخدم كلا الجدولين اسم العمود نفسه. تزيل USING
تلقائيًا العمود المكرّر من النتيجة، وهكذا سيُعيد الضم في الاستعلام أعلاه عمودًا RecipeID
واحدا.)
حقن SQL
حقن SQL هي تقنية يستخدمها القراصنة للوصول إلى جداول قاعدة بيانات موقع معيّن عن طريق حقن تعليمات SQL في حقل إدخال.
إذا لم يكن خادم الويب مُجهّزا للتعامل مع هجمات حقن SQL، فيمكن للمخترقين خداع قاعدة البيانات، وجعلها تنفّذ شيفرة SQL إضافية. والتي قد تمكّنهم من ترقية صلاحيات حساباتهم، أو الوصول إلى المعلومات الشخصية لحساب آخر، أو إجراء أيّ تعديلات أخرى على قاعدة البيانات.
لنفترض أنّ استدعاء معالج تسجيل الدخول إلى موقعك يبدو كما يلي:
https://somepage.com/ajax/login.ashx?username=admin&password=123
الآن في login.ashx، ستقرأ القيم التالية:
strUserName = getHttpsRequestParameterString("username"); strPassword = getHttpsRequestParameterString("password");
يمكنك استعلام قاعدة البيانات للتحقق ممّا إذا كان هناك مستخدم له كلمة المرور هذه. لذا ستنشئ استعلام SQL التالي:
txtSQL = "SELECT * FROM Users WHERE username = '" + strUserName + "' AND password = '"+ strPassword +"'";
سيعمل هذا الاستعلام بلا مشاكل إذا لم يحتو اسم المستخدم وكلمة المرور على علامات اقتباس. ولكن إن احتوى أحد المعاملات على علامات اقتباس، فإنّ شيفرة SQL المُرسلة إلى قاعدة البيانات ستبدو كما يلي:
-- strUserName = "d'Alambert"; txtSQL = "SELECT * FROM Users WHERE username = 'd'Alambert' AND password = '123'";
سينتج عن هذا خطأ في الصياغة، لأنّ علامة الاقتباس بعد d
في d'Alambert
تنتهي بشيفرة SQL.
يمكنك تصحيح هذا عن طريق تهريب (escaping) علامات الاقتباس في اسم المستخدم وكلمة المرور على النحو التالي:
strUserName = strUserName.Replace("'", "''"); strPassword = strPassword.Replace("'", "''");
هناك حلّ آخر أفضل، وهو استخدام المعاملات:
cmd.CommandText = "SELECT * FROM Users WHERE username = @username AND password = @password"; cmd.Parameters.Add("@username", strUserName); cmd.Parameters.Add("@password", strPassword);
إذا لم تستخدم المعاملات، ونسيت استبدال علامات الاقتباس ولو في قيمة واحدة، فيمكن للقرصان استخدام هذا لتنفيذ أوامر SQL في قاعدة البيانات الخاصة بك. على سبيل المثال، يمكن للقرصان أن يعيّن كلمة المرور التالية:
lol'; DROP DATABASE master; --
وبعدها ستبدو SQL كالتالي:
"SELECT * FROM Users WHERE username = 'somebody' AND password = 'lol'; DROP DATABASE master; --'";
لسوء الحظ، هذه شيفرة SQL صحيحة، وستنفّذها قاعدة البيانات DB! هذا النوع من الهجمات يسمّى حقن SQL.
هناك أشياء أخرى كثيرة يمكن أن يقوم بها القرصان، مثل سرقة عناوين البريد الإلكتروني الخاصة بالمستخدمين، أو سرقة كلمات المرور خاصتهم، أو سرقة أرقام بطاقات الائتمان، أو سرقة أيّ نوع من البيانات في قاعدة البيانات. لهذا السبب، عليك دائمًا تهريب السلاسل النصية.
لمّا كان النسيان طبيعة في الإنسان، ينصح الكثيرون باستخدام المعاملات دائمًا. لأنّ إطارات لغة البرمجة المُستخدمة تتكفّل بتهريبها نيابة عنك.
مثال على حقن بسيط
إذا تم إنشاء عبارة SQL على النحو التالي:
SQL = "SELECT * FROM Users WHERE username = '" + user + "' AND password ='" + pw + "'"; db.execute(SQL);
سيكون بمقدور القرصان سرقة بياناتك عن طريق إعطاء كلمة مرور من هذا القبيل pw' or '1'='1
؛ وهكذا تصبح عبارة SQL الناتجة على النحو التالي:
SELECT * FROM Users WHERE username = 'somebody' AND password ='pw' or '1'='1'
العبارة '1'='1'
صحيحة دائمًا، لذلك سيتم اختيار كل الصفوف. لمنع هذا، استخدم معاملات SQL على النحو التالي:
SQL = "SELECT * FROM Users WHERE username = ? AND password = ?"; db.execute(SQL, [user, pw]);
ترجمة -وبتصرّف- للفصلين من 61 إلى 62 من الكتاب SQL Notes for Professionals
اقرأ أيضًا:
- المقال السابق: تصميم الجداول ومعلومات المخطط وترتيب تنفيذ الاستعلامات في SQL
- النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.