flask_cms ترشيح البيانات باستعمال شروط منطقية في SQLAlchemy


عبدالهادي الديوري

سنُتم في هذا الدرس ما بدأناه في الدرس السابق من تعلم لكيفيّة استعمال الشروط والمعاملات المنطقيّة المتقدمة في لغة بايثون، من أجل الحصول على نتائج دقيقة من قاعدة البيانات عند استعمال مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy.

التعامل مع السلاسل النصية

قد تحتاج أحيانا إلى التّحقق من أنّ سلسلة نصيّة ما تبدأ أو تنتهي بكلمة أو حرف، أو أنّ كلمة أو حرفا معيّنا يتواجد في قائمة أو سلسلة نصيّة، لذا سنتعرّف في الفقرات التّاليّة على كيفيّة فعل ذلك في لغة بايثون.

التابع startswith

يُطبّق التّابع startswith على السّلسلة النّصيّة للتأكّد من أنّها تبدأ بمقطع معيّن، فمثلًا، سنتأكّد أنّ السّلسلة النّصيّة abdelhadi تبدأ بالمقطع abd:

>>> name = 'abdelhadi'
>>> name.startswith('abd')
True

يُمكنك تطبيق هذه الميّزة من أجل الحصول على جميع المقالات التّي يبدأ عنوانها بمقطع معيّن، فمثلا لنقل بأنّك تمتلك في مدوّنتك مقالات مُختلفة إضافة إلى مقالات تُشير إلى كل تحديث في تطبيقك لتعرض الميّزات الجديدة، ثم لنفترض زيادة حجم التطبيق بعد مدة إلى أن صارت له مُدونته المستقلة عن مُدوّنتك، سيتوجّب عليك الآن نقل المقالات المُخصّصة للتّحديثات إلى هذه المُدوّنة الجديدة، وعلى فرض أنّ تلك المقالات تبدأ بكلمة "تحديث"، فكيف يُمكنك الحصول على هذه المقالات لنقلها دون المقالات الأخرى؟

فكما ترى، تلك طريقة واحدة لاستعمال هذا التّابع، إذ يُمكنك استعمال startswith للحصول على جميع المقالات التّي تبدأ بالمقطع، ومن ثم تنقلها إلى مكان آخر بشكل آلي.

العامل in

يُستعمل العامل in للتّأكّد من أن سلسلة نصيّة أو قائمة في بايثون تحتوي على مقطع نصي معيّن. فمثلا، سنتأكّد من أن الحرف a يتواجد بكلمة abcd، وسنتأكّد في المثال الآخر من أنّ اسم المُستخدم khalid يتواجد ضمن قائمة usernames، بالإضافة إلى حالتين لعدم استيفاء الشّرط والحصول على القيمة المنطقيّة False:

>>> 'a' in 'abcd'
True
>>> 'e' in 'abcd' 
False

>>> usernames = ['abdelhadi', 'ahmed',  'ibrahim',
 'ali', 'khalid', 'mohammed']

>>> 'khalid' in usernames
True

>>> 'youssuf' in usernames
False

إليك استخدامًا للعامل in:

قلنا في الفقرة السّابقة أنك تستطيع نقل جميع المقالات التّي تبدأ بالمقطع "تحديث" إلى مدونة أخرى، لكن ماذا لو كانت مقالاتنا التّي تبدأ بهذا المقطع تُشير إلى أكثر من تطبيق واحد؟ لنقل بأنّ المدونة حاليا بها تحديثات لتطبيقين، تطبيق "كلمة" وتطبيق آخر، وكلاهما يحصلان على تحديثات ومقال لكل تحديث، هذا يعني بأنّ الحصول على المقالات التّي تبدأ بالمقطع "تحديث" ستكون مزيجا بين تحديثات التّطبيقين، ولكنّنا نرغب بنقل مقالات التطبيق "كلمة" إلى مدونة "كلمة" دون المقالات الخاصّة بالتّطبيق الآخر.

نستطيع هنا مزج كل من startswith وin وand للوصول إلى مُرادنا، بحيث نتأكّد من أنّ عناوين المقالات التّي سنحصل عليها تبدأ بكلمة "تحديث" وفي نفس الوقت تحتوي على اسم التّطبيق "كلمة" وإليك مثالا بسيطا:

>>> kalima_update = 'تحديث في تطبيق كلمة'
>>> app_update = 'تحديث في تطبيق آخر'
>>> kalima_update.startswith('تحديث') and 'كلمة' in  kalima_update
True

>>> app_update.startswith('تحديث') and 'كلمة' in  app_update
False

>>> app_update.startswith('تحديث')
True

>>> 'كلمة' in  kalima_update
True

>>> 'كلمة' in  app_update
False

كما تُلاحظ، استخدمنا كلًا من startswith مع and وin لنستطيع ترشيح عنوان المقال الذي يحمل تحديثا للتطبيق "كلمة" من العنوان الآخر.

هذا مجرّد مثال على كيفيّة الاستفادة من مختلف الأدوات التّي توفّرها لنا لغة بايثون وكيفيّة مزجها لحل مشاكل تحدث على الواقع، وسنرى فيما بعد كيفيّة استخدام هذه الجمل التّي تُرجع حاليا القيمتين True وFalse فقط لترشيح بياناتنا في قاعدة البيانات باستعمال SQLAlchemy.

التابع endswith

يُطبّق التّابع endswith على السّلسلة النّصيّة للتأكّد من أنّ سلسلة نصيّة تنتهي بمقطع معيّن، على سبيل المثال، يُمكن أن نتأكّد من أنّ بريدا إلكترونيّا ينتهي بـexample.com كما يلي:

>>> 'abdelhadi@example.com'.endswith('example.com')
True

>>> 'abdelhadi@gmail.com'.endswith('example.com')
False

لاحظ أن البريد الإلكتروني الأول ينتهي بـexample.com لذا تحقّق الشّرط وأرجع مُفسّر بايثون القيمة True، أمّا الثّاني فينتهي بـgmail.com وليس example.com لذا أُرجِعّت القيمة المنطقيّة False.

إن تساءلت عن الفائدة من هذا الأمر على الواقع، فإليك السيناريو التّالي: لنقل مثلا بأنّ خدمة example.com ستلغي خدمات بريدها الإلكتروني وسيتوجّب عليك تنبيه جميع مُستخدميك لتنصحهم بالانتقال إلى خدمة بريد إلكتروني أخرى، يُمكنك مثلا أن تستعمل هذه الطّريقة للحصول على جميع المُستخدمين الذين سجّلوا ببريد إلكتروني من هذه الخدمة، ومن ثم تستطيع تنبيههم وحدهم دون المُستخدمين الآخرين. لا شك أن هناك أمثلة كثيرة أخرى على الواقع، وستجد تحديّات ومشاكل يُمكنك حلّها بمثل هذه الأدوات التّي توفّرها لغة بايثون.

ترشيح البيانات التي نحصل عليها من قاعدة البيانات باستعمال شروط منطقية عبر مكتبة SQLAlchemy

في السّابق، كنّا نستعمل التّابع filter_by للحصول على سجل أو عدّة سجلّات عند تَطابُقِ قيمة عمود معيّن مع القيمة المُمرّرة، فمثلا لو أردنا الحصول على مُستخدم يُطابق اسمه السّلسلة النّصيّة khalid يُمكن أن نستعمل التّابع filter_by مع تمرير السّلسلة النّصيّة إلى المعامل name والذي يمثّل اسم العمود في هذه الحالة:

>>> from project.models import User
>>> User.query.filter_by(name = 'khalid').first()
<username: khalid | email: khalid@example.com >

في الشّيفرة أعلاه، كل ما نفعله هو طلب سجل من قاعدة البيانات في كل مكان تُساوي فيه قيمة العمود name السّلسلة النّصيّة "khalid”. أي كأنّنا نقول: أعطني السّجل الذي يُرجع طَلبُه القيمة المنطقيّة True عند تنفيذ الجملة التّاليّة:

User.name == 'khalid'

وبالمثل يُمكن أن تستخدم filter_by مع بقيّة الأعمدة.

يُعد التّابع fliter_by جيدًا في حالات كثيرة كالوصول إلى مستخدم عن طريق اسمه أو بريده الإلكتروني، أو حتى مقال عبر عنوانه، لكن ماذا لو أردنا إجراء عمليّات أكثر تقدّما، مثل الحصول على السّجلات التّي تبدأ أو تنتهي بمقطع نصي معيّن أو تلك التّي تحتوي بداخلها على مقطع ما، أو الوصول إلى جميع السّجلات التّي تحقّق مجموعة من الشّروط أو بعضها؟

التابع filter

في SQLAlchemy يُمكنك استعمال التّابع filter للحصول على نتائج تستوفي شروطا معيّنة تُمرّر إلى هذا التّابع كما يلي:

>>> Table.query.filter(Table.column == value)

لدينا ثلاثة متغيّرات يُمكنك استبدالها هنا لتتحصّل على نتيجتك المرغوبة: Table هو اسم الصّنف المرتبط بالجدول الذي ترغب بالحصول على البيانات منه، وcolumn يُمثّل اسم العمود، أمّا value فتمثّل القيمة.

وكما تلاحظ، فهذا التّعبير شبيه جدّا بالتّابع filter_by لأنّ الشّرط هو ما إذا كانت قيمة العمود تُساوي كذا. أمّا للوصول إلى نتائج متنوّعة فتستطيع استبدال العامل == لكتابة شروط منطقيّة أخرى، وإليك بعض الأمثلة:

Table.query.filter(Table.column == value)
Table.query.filter(Table.column < value)
Table.query.filter(Table.column > value)
Table.query.filter(Table.column.endswith(value))
Table.query.filter(Table.column.startswith(value))

يُمكنك أن تعوض المتغيّر value بقيمة عمود آخر من الجدول:

Table.query.filter(Table.column == Table.column2)
Table.query.filter(Table.column.startswith( Table.column2))

بعد ترشيح السّجلات، يُمكنك بعد ذلك تطبيق كل من all أو first على النّتيجة. لاحظ في هذا المثال كيف أنّ كلّا من filter و filter_by يرجعان نفس النّتيجة:

>>> User.query.filter_by(name = 'khalid').first()
<username: khalid | email: khalid@example.com >

>>> User.query.filter(User.name == 'khalid').first()
<username: khalid | email: khalid@example.com >

وكذلك يتّبع get نفس المنهج لكنّه لا يحتاج إلى التّابع first لأنّ المعروف أنّ المفتاح الأولي لا يُمكن أن يحمل من طرف أكثر من سجل واحد:

>>> User.query.filter_by(id = 3).first()
<username: dyouri | email: dyouri@example.com >

>>> User.query.get(3)
<username: dyouri | email: dyouri@example.com >

>>> User.query.filter(User.id == 3).first()
<username: dyouri | email: dyouri@example.com >

ترشيح السجلات عن طريق in

قد تُفكّر في تطبيق العامل in للحصول على سجلات تتواجد إحدى قيم أعمدتها في قائمة ما، لكنّ SQLAlchemy لن يقبل العامل in الذي توفّره لنا لغة بايثون، وعليه لن تحصل على أيّة نتيجة.

يُمكنك تنفيذ التّابع in_ في SQLAlchemy مع تمرير قائمة من القيم كما يلي:

>>> User.query.filter(User.name.in_(['abdelhadi', 'user4', 'user5'])).all()



[<username: abdelhadi | email: abdelhadi@example.com >,
 <username: user4 | email: user4@example.com >,
 <username: user5 | email: user5@example.com >]

لاحظ كيف طبّقنا التّابع in_ كقيمة ممرّرة للتّابع filter:

Table.column.in_(list)

أمّا كل من startswith و endswith فلا تتغيّر ويُمكنك استخدامها كما وضّحت سابقا:

>>> User.query.filter(User.name.startswith('abd')).first()
<username: abdelhadi | email: abdelhadi@example.com >

>>> User.query.filter(User.name.endswith('lid')).first()
<username: khalid | email: khalid@example.com >

تعلّمنا الآن كيفيّة الحصول على سجلّات تبدأ أو تنتهي أحد قيم أعمدتها بمقطع معيّن، لكن ماذا لو أردنا الحصول على السّجلات التي تحتوي على مقطع معيّن في أي مكان من النّص؟

توفّر لنا مكتبة SQLAlchemy طريقة للحصول على السّجلات التّي تحتوي على مقطع معيّن بغض النّظر عن مكان تواجد هذا المقطع (سواء كان في بداية السّلسلة النّصيّة أو وسطها أو نهايتها)، وذلك باستعمال contains:

>>> User.query.filter(User.name.contains('d')).all()

[<username: khalid | email: khalid@example.com >,
<username: dyouri | email: dyouri@example.com >,
<username: abdelhadi | email: abdelhadi@example.com >]

كما تلاحظ فقد حصلنا على جميع المُستخدمين الذين تحتوي أسماءهم على الحرف d سواء أكان في بداية الاسم أو في آخره أو وسطه.

ترشيح السجلات بأكثر من شرط واحد

قد تود عند استعمال filter أن تحصل على نتائج تُحقّق أكثر من شرط واحد، وعلى سجل أو سجلّات تستوفي العديد من الشّروط (مثل الحصول على كل مُستخدم انضمّ قبل شهر واحد وأضاف أكثر من 10 مقالات).

وغالبًا ستستعمل and التّي توفّرها لغة بايثون، لكن كما الحال مع in فمكتبة SQLAlchemy توفّر بديلا لها، والبديل هو الدّالة and_ في SQLAlchemy والتّي نصل إليها عن طريق الكائن db. إليك مثالا لطريقة القيام بالأمر:

>>> user = User.query.filter(db.and_(condition1, condition2))

مع استبدال condition1 بالشّرط الأول و condition2 بالشّرط الثّاني.

وبما أنّ and_ شبيهة بجملة and في لغة بايثون فالمتوقّع أنّ حالة تحقّق الشّرطين معا هي الحالة الوحيدة التي يُمكن أن تُرجع نتيجة من الجدول:

>>> True and True
True
>>> False and False
False
>>> False and True
False
>>> True and False
False

لنفترض أنّنا نُريد الحصول على أول مُستخدم يبدأ اسمه بكلمة abd وفي نفس الوقت يكون رقم مُعرّفه أكبر من الرّقم 1:

>>> from project import db
>>> user = User.query.filter(
... db.and_(User.name.startswith('abd'),
...  User.id > 1)).first()

>>> user
<username: abdelhadi | email: abdelhadi@example.com >

كما تُلاحظ، حصلنا على المُستخدم عبدالهادي لأنّه يستوفي الشّرطين، فرقم المعرفّ الخاص بالمُستخدم هو 2 أي أنّه أكبر من 1، ثمّ إنّ اسم المُستخدم يبدأ بالمقطع abd بكل تأكيد. يُمكنك كذلك استخدام filter أكثر من مرّة لتطبيق العديد من الشّروط:

>>> user = User.query.filter(
... User.name.startswith('abd')).filter(
...  User.id > 1).first()
>>> user
<username: abdelhadi | email: abdelhadi@example.com >
>>> user.id
2

لاحظ هنا بأنّ النّتيجة هي نفسها، أي أنّ كلّا من db.and_ و استعمال filter أكثر من مرّة له نفس المفعول وشخصيّا أرى أنّ استعمال db.and_ أفضل وأسهل.

يُمكنك كذلك استخدام الدّالّة or_ للحصول على شرط يستوفي إمّا هذا الشّرط أو ذاك، وهو شبيه بجملة or في بايثون، وإليك تذكيرا بسيطا:

>>> True or True
True

>>> False or False
False

>>> False or True
True

>>> True or False
True

يعني لو أردنا الحصول على مُستخدم ينتهي اسمه بالحرف d أو رقم مُعرّفه يُساوي 10 لكتبنا استعلامنا كما يلي:

>>> user = User.query.filter(
  db.or_(User.name.endswith('d'),
  User.id == 10)).first()


>>> user
<username: khalid | email: khalid@example.com >
>>> user.id
1

لاحظ بأنّ الشّرط الثّاني خاطئ، ومع ذلك حصلنا على المُستخدم خالد لأنّ الشّرط الأول صحيح، لو استخدمت and_ هنا لكانت النّتيجة عبارة عن القيمة None أو بمعنى آخر لا نتيجة لهذا الاستعلام وهذا لأنّ الشّرط الثّاني خاطئ.

مزج and و or معًا

كما رأينا سابقا، يُمكنك وضع شروط معقّدة بمزج and و or أكثر من مرّة داخل بعضها البعض كما يلي:

>>> (True and True) or (False and True)
True

الشّرط الأول في الشيفرة أعلاه ينتِج القيمة True، والثّاني يُنتج القيمة False، يعني كأنّنا نقول:

True or False

ومن ثم فالخرج هو القيمة المنطقيّة True. يُمكنك أن تأخذ هذا المبدأ إلى أبعد الحدود لتكتب شروطًا أكثر تعقيدًا:

>>> ((True and True) or (False and True)) and ((True and True) and (False and True))
False

>>> (True and True) or (False and True)
True

>>> (True and True) and (False and True)
False

أعتقد بأنّني بالغت في مزج كل هذه الشّروط، لكنّني أشعر بأنّ فهم طريقة العمل هذه مُهمّة، خاصّة إن كنت تخطّط لقراءة شيفرة مشروع مفتوح المصدر.

قد لا تحتاج أبدا إلى مزج كل من or و and لإنشاء شروط معقّدة في SQLAlchemy، لكنّي رأيت أن أمنحك مثالا لكيفيّة القيام بالأمر، ولا بأس إن لم تفهم المثال الآن إذ أنّه يختلف بعض الشّيء عمّا تطرّقنا إليه سابقا، وستستطيع أن تفهمه ببعض من المُمارسة.

إليك مثالا على كيفيّة مزج and و or في SQLAlchemy:

db.and_(
    db.or_(
        condition1, condition2
       ),
    db.and_(
        condition3, condition4
       )
)

لاحظ بأنّك تستطيع وضع الشّروط في مُتغيّرات لتعمل الشّيفرة السّابقة، مثلا:

condition1 = User.name == 'khalid'
condition2 = User.name == 'username'
condition3 = User.id < 10
condition4 = User.email.endswith('example.com')

هذا كأنّنا نقول:

(condition1 or condition2) and (condition3 and condition4)

إن لاحظت بيانات المُستخدم khalid فستدرك بأنّ هذه الشّروط تتحقّق فيه. وفي الشّيفرة التّاليّة، أضع الشّروط في مُتغيّر باسم condition لأمرّرها فيما بعد إلى التّابع filter.

condition = db.and_(
    db.or_(
        User.name == 'khalid',  # True
        User.name == 'username'  # False
       ),
    db.and_(
        User.id < 10,  # True
        User.email.endswith('example.com')  # True
      )
)

لنجرّب الآن تمرير هذا الشّرط إلى مكتبة SQLAlchemy وللنَنظر هل هناك من مُستخدم يحقّق الشّروط المذكورة:

>>> User.query.filter(condition).all()
[<username: khalid | email: khalid@example.com >]

جيد! كل شيء سار كما هو متوقّع، فالمُستخدم خالد هو الوحيد الذي يُحقّق جميع الشّروط.

خاتمة

تعرّفنا في هذا الدّرس على كيفيّة استعمال التابع filter في مكتبة SQLAlchemy لترشيح السّجلات عند الحصول عليها من قاعدة البيانات باستعمال شروط منطقيّة متقدّمة. وسنتعرّف في الدرس القادم على كيفيّة استغلال ما تعلّمناه من حيل للتّعامل مع قواعد البيانات بطريقة أفضل.





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


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



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

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

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


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

تسجيل الدخول

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


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