محمد الميداوي

تستعرض هذه المقالة أفضل الممارسات المُتعارف عليها لكتابة شيفرات 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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن