يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون.
أمّا SQLAlchemy، فهي أداةٌ في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات، مثل SQLite و MySQL و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر رابط الكائنات بالعلاقات Object Relational Mapper -أو اختصارًا ORM- الذي يتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون.
تعدّ Flask-SQLAlchemy إضافة لفلاسك تسهّل من استخدام SQLAlchemy ضمن فلاسك مؤمنةً الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy.
تُعرَّف علاقة قاعدة البيانات متعدّد-إلى-متعدّد many-to-many على أنها علاقةٌ بين جدولين، إذ يمكن لأي سجلٍ من الجدولين الارتباط مع عدّة سجلات من الآخر، فمثلًا في تطبيق مدونة يمكن لجدول التدوينات posts أن يرتبط بعلاقة من نوع متعدّد-إلى-متعدّد مع جدول أسماء المؤلفين (كُتَّاب التدوينات)، بمعنى أن كل تدوينة قد ترتبط بعدّة أسماء مؤلفين، وأيضًا قد يرتبط كل اسم مؤلف بعدّة تدوينات، وبالتالي فإنّ العلاقة ما بين التدوينات وأسماء المؤلفين هي علاقة متعدّد-إلى-متعدّد. أيُّ تطبيقٍ آخر من تطبيقات التواصل الاجتماعي هو مثالٌ آخر، إذ أن كل منشور قد يحتوي عدّة إشارات مرجعية، وكل إشارة مرجعية قد تتضمّن عدة منشورات.
سنعمل في هذا المقال على تعديل تطبيق مبني باستخدام كل من فلاسك والإضافة SQLAlchemy فيه، وذلك بإضافة علاقة من نوع متعدّد-إلى-متعدّد إليه، إذ سننشئ علاقة ما بين التدوينات والوسوم tags، بحيث يمكن لكل تدوينة أن تتضمّن عدّة وسوم، ويمكن أن يُشار للوسم الواحد في عدّة تدوينات.
يمكنك قراءة هذا المقال مُنفصلًا، إلّا أنّه تكملة لمقالنا السابق كيفية استخدام SQLAlchemy في فلاسك، وفيه قد بنينا قاعدة بيانات مُتعدّدة الجداول بعلاقة من نواع واحد إلى مُتعدّد one-to-many ما بين التدوينات والتعليقات عليها وذلك ضمن تطبيق مدونة.
سيتضمّن تطبيقك -مع نهاية هذا المقال- ميزةً جديدة تتمثّل بإمكانية إضافة الوسوم إلى التدوينات، بحيث يُشار إلى التدوينة الواحدة في عدّة وسوم، لتعرض صفحة الوسم الواحد كافّة التدوينات المُشار إليها من خلاله.
مستلزمات العمل
قبل المتابعة في هذا المقال لا بُدّ من:
- توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app".
- الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك.
- فهم أساسيات لغة HTML.
- (مُتطلّب اختياري) في الخطوة الأولى ستنسخ تطبيق المدونة الذي ستعمل عليه في هذا المقال، ولكن يمكنك بناءه من الصفر بدلًا من نسخه باتباعك للخطوات الواردة في المقال السابق، كما يمكنك الوصول إلى الشيفرة الكاملة للتطبيق من flask-slqa-bloggy.
الخطوة 1 - إعداد تطبيق الويب
سنعمل في هذه الخطوة على إعداد تطبيق المدونة ليكون جاهزًا للتعديل عليه، كما سنستعرض نماذج قاعدة بيانات Flask-SQLAlchemy ووجهات فلاسك لضمان فهمك الجيد لبنية التطبيق.
يمكنك تجاوز هذه الخطوة في حال اتباعك للمقال السابق المذكور آنفًا في فقرة مستلزمات العمل واحتفاظك بالشيفرة والبيئة الافتراضية على حاسوبك.
سنستخدم شيفرة التطبيق المبني في المقال السابق (راجع فقرة مستلزمات العمل) لشرح آلية إضافة علاقة من النوع متعدّد-إلى-متعدّد لتطبيق ويب مبني باستخدام فلاسك مع الإضافة Flask-SQLAlchemy؛ وهذا التطبيق هو مدونة يتيح إمكانية إضافة وعرض التدوينات والتعليق عليها، إضافةً إلى إمكانية قراءة التعليقات الموجودة وحذفها.
سننسخ الآن ملف الشيفرة من المستودع ونعيد تسميته من "flask-slqa-bloggy" ليصبح "flask_app" وذلك باستخدام الأمر التالي:
user@localhost:$ git clone https://github.com/do-community/flask-slqa-bloggy flask_app
ننتقل الآن إلى الملف "flask_app" على النحو التالي:
user@localhost:$ cd flask_app
ثمّ سننشئ بيئة افتراضية جديدة:
user@localhost:$ python -m venv env
ونفعّل هذه البيئة على النحو التالي:
user@localhost:$ source env/bin/activate
ونثبّت الآن كلًا من فلاسك والإضافة Flask-SQLAlchemy باستخدام الأمر:
(env)user@localhost:$ pip install Flask Flask-SQLAlchemy
ثمّ نضبط متغيرات البيئة التالية:
(env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development
يشير متغيّر البيئة FLASK_APP
إلى التطبيق الذي نعمل على تطويره حاليًا، وهو "app.py" في حالتنا، أمّا المتغير FLASK_ENV
فيحدّد وضع التشغيل، وقد اخترناه ليكون وضع التطوير development
الذي يوفّر إمكانية تنقيح الأخطاء في التطبيق، مع الانتباه إلى عدم استخدام وضع التشغيل هذا في بيئة الإنتاج.
والآن، سنفتح صَدَفَة فلاسك لإنشاء جداول قاعدة البيانات:
(env)user@localhost:$ flask shell
ومن ثمّ سنستورد الكائن db
الخاص بقاعدة بيانات Flask-SQLAlchemy، بالإضافة إلى النموذج post
الخاص بالتدوينات، والنموذج Comment
الخاص بالتعليقات، لننشئ بعدها جداول قاعدة البيانات الموافقة للنماذج السابقة باستخدام الدالة ()db.create_all
، كما يلي:
>>> from app import db, Post, Comment >>> db.create_all() >>> exit()
ومن ثمّ سنملأ قاعدة البيانات باستخدام البرنامج من الملف "init_db.py":
(env)user@localhost:$ python init_db.py
والذي سيضيف ثلاث تدوينات وأربعة تعليقات إلى قاعدة البيانات.
نشغّل الآن خادم التطوير:
(env)user@localhost:$ flask run
الآن وبالذهاب إلى المتصفّح، يصبح من الممكن الوصول إلى التطبيق عبر الرابط:
http://127.0.0.1:5000/
فتظهر صفحة شبيهة بما يلي:
وفي حال ظهور خطأ، تأكّد من اتباعك الخطوات السابقة على النحو الصحيح.
يمكنك إيقاف تشغيل خادم التطوير بالضغط على مفتاحي "CTRL+C" في لوحة المفاتيح.
فيما يلي سنمر على نماذج قاعدة البيانات Flask-SQLAlchemy بغية فهم العلاقات الحالية ما بين الجداول فيها، فإذا كانت محتويات الملف "app.py" مألوفةً يالنسبة لك من المقال السابق، فيمكنك تجاوز هذه الخطوة.
نفتح الآن الملف "app.py":
(env)user@localhost:$ nano app.py
فتظهر محتوياته على النحو التالي:
import os from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') def __repr__(self): return f'<Post "{self.title}">' class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) def __repr__(self): return f'<Comment "{self.content[:20]}...">' @app.route('/') def index(): posts = Post.query.all() return render_template('index.html', posts=posts) @app.route('/<int:post_id>/', methods=('GET', 'POST')) def post(post_id): post = Post.query.get_or_404(post_id) if request.method == 'POST': comment = Comment(content=request.form['content'], post=post) db.session.add(comment) db.session.commit() return redirect(url_for('post', post_id=post.id)) return render_template('post.html', post=post) @app.route('/comments/') def comments(): comments = Comment.query.order_by(Comment.id.desc()).all() return render_template('comments.html', comments=comments) @app.post('/comments/<int:comment_id>/delete') def delete_comment(comment_id): comment = Comment.query.get_or_404(comment_id) post_id = comment.post.id db.session.delete(comment) db.session.commit() return redirect(url_for('post', post_id=post_id))
وفيه لدينا نموذجي قاعدة بيانات يمثلان جدولين، هما:
-
الجدول
Post
: والذي يتضمّن عمودًا لكل من معرّف التدوينة وعنوانها ومضمونها، إضافةً إلى علاقة من النوع واحد-إلى-مُتعدّد مع جدول التعليقات. -
الجدول
Comment
: والذي يتضمّن عمودًا لكل من معرّف التعليق ومحتواه، إضافةً إلى عمود لمعرّف التدوينةpost_id
للإشارة إلى التدوينة التي يتبع لها التعليق.
كما يتضمّن الملف بعد النموذجين كل من الوجهات التالية:
-
/
: الصفحة الرئيسية للتطبيق، والتي تعرض كافّة التدوينات الموجودة في قاعدة البيانات. -
/<int:post_id>/
: الصفحة الخاصّة بكل تدوينة، فمثلًا يعرض الرابط "/http://127.0.0.1:5000/2" تفاصيل التدوينة الثانية من قاعدة البيانات مع التعليقات عليها. -
/comments/
: صفحة لعرض كافّة التعليقات من قاعدة البيانات مع روابط التدوينات التي يتبع لها كل تعليق. -
comments/<int:comment_id>/delete/
: وجهة مُخصّصة لحذف التعليقات باستخدام زر أوامر باسم حذف تعليق Delete Comment.
نغلق الآن الملف app.py.
سنستخدم في الخطوة التالية علاقة من نوع مُتعدّد-إلى-مُتعدّد للربط بين الجدولين المذكورين سابقًا.
الخطوة 2 - إعداد نماذج قاعدة بيانات لإنشاء علاقة من نوع متعدد-إلى-متعدد
سنضيف في هذه الخطوة نموذج قاعدة البيانات المُمثّل لجدول الوسوم، والذي سنربطه مع جدول التدوينات الحالي باستخدام "جدول ارتباط association table"، والذي يعرّف بأنّه جدول وسيط يربط بين جدولين بعلاقة مُتعدّد-إلى-مُتعدّد.
تربط العلاقة مُتعدّد-إلى-مُتعدّد بين جدولين، بحيث يتعلّق كل عنصر من أحدهما بعدّة عناصر من الآخر، وتشير كل تدوينة في جدول الارتباط إلى وسومها، كما يشير كل وسم إلى كافّة التدوينات المُتضمّنة له.
سنضيف أيضًا في هذه الخطوة بعضًا من التدوينات والوسوم إلى قاعدة البيانات، لنتمكن لاحقًا من عرض التدوينات مع وسوم كل منها أو الوسوم مع التدوينات المُتضمّنة لكل منها.
ليكن لدينا جدول تدوينات بسيط على النحو التالي:
Posts +----+-----------------------------------+ | id | content | +----+-----------------------------------+ | 1 | A post on life and death | | 2 | A post on joy | +----+-----------------------------------+
وجدولًا للوسوم كما يلي:
Tags +----+-------+ | id | name | +----+-------+ | 1 | life | | 2 | death | | 3 | joy | +----+-------+
يمكننا مثلًا إضافة كل من الوسمين "life" و "death" إلى التدوينة "A post on life and death" عن طريق إضافة عمود جديد إلى جدول التدوينات، ليصبح بالشّكل:
Posts +----+-----------------------------------+------+ | id | content | tags | +----+-----------------------------------+------+ | 1 | A post on life and death | 1, 2 | | 2 | A post on joy | | +----+------------------------------------------+
لن تعمل هذه المنهجية لأنّ كل عمود يجب أن يتضمّن قيمة واحدة، وفي حال وجود عدّة قيم للعمود الواحد من السجل، ستصبح العمليات الأساسية مثل إضافة البيانات وتحديثها مرهقة وبطيئة، وسنستخدم بدلًا من ذلك جدولًا إضافيًا ثالثًا ليشير إلى المفاتيح الأساسية ويربط بينها وبين الجداول المرتبطة، وهو ما ندعوه بجدول الارتباط أو جدول الصلة join table، والذي سيخزّن معرّفات كل عنصر من كل جدول.
وفيما يلي مثال لجدول ارتباط يربط بين جدولي التدوينات والوسوم:
post_tag +----+---------+-------------+ | id | post_id | tag_id | +----+---------+-------------+ | 1 | 1 | 1 | | 2 | 1 | 2 | +----+---------+-------------+
ترتبط التدوينة ذات المعرّف رقم "1" (وهي "A post on life and death") مع الوسم ذي المعرّف رقم "1" (وهو "life") في السجل الأوّل من الجدول السابق؛ أمّا في السجل الثاني، فترتبط التدوينة الأولى نفسها أيضًا مع الوسم ذو المعرّف رقم "2" (وهو "death")، ما يعني أنّ التدوينة الأولى تتضمّن كلا الوسمين "life" و "death"، ويمكن ربط كل تدوينة بوسومات متعدّدة بنفس الآلية.
سنعدّل الآن الملف app.py لإضافة نموذج قاعدة بيانات جديد ليمثّل الجدول الذي سنستخدمه لتخزين الوسوم، كما سنضيف جدول ارتباط باسم "post_tag" لربط التدوينات بالوسوم.
لذا، سنفتح بدايةً الملف app.py بغية إنشاء العلاقة ما بين التدوينات والوسوم:
(env)user@localhost:$ nano app.py
سنضيف الآن جدول post_tag
ونموذج باسم Tag
بعد كائن قاعدة البيانات db
وقبل النموذج Post
، ومن ثمّ سنضيف للنموذج Post
عمودًا علاقيًا زائفًا للوسوم باسم tags
لنمكّن لاحقًا من الوصول إلى وسوم التدوينة باستخدام التعليمة post.tags
أو الوصول إلى التدوينات المُتضمّنة وسمًا ما باستخدام التعليمة tag.posts
.
# ... db = SQLAlchemy(app) post_tag = db.Table('post_tag', db.Column('post_id', db.Integer, db.ForeignKey('post.id')), db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')) ) class Tag(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50)) def __repr__(self): return f'<Tag "{self.name}">' class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') tags = db.relationship('Tag', secondary=post_tag, backref='posts') def __repr__(self): return f'<Post "{self.title}">'
احفظ الملف واغلقه.
استخدمنا في الشيفرة السابقة الدالة ()db.Table
لإنشاء جدول بعمودين، إذ أنّ الإجراء الأفضل بالنسبة لجدول الارتباط هو استخدام جدول بدلًا من نموذج قاعدة بيانات، وهنا يمتلك الجدول post_tag عمودين يمثلان مفتاحين خارجيين foreign keys أي المفتاحين المستخدمين للإشارة إلى أعمدة المفاتيح الأساسية في الجداول الأصل (المفتاح الأساسي لجدول ما هو مفتاح أجنبي عند تواجده في جدول آخر):
-
post_id
: وهو مفتاح خارجي من نوع عدد صحيح يمثّل معرّف التدوينة ويشير إلى عمود المُعرّف ID في جدول التدويناتpost
. -
tag_id
: وهو مفتاح خارجي من نوع عدد صحيح يمثّل معرّف الوسم ويشير إلى عمود المُعرّف ID في جدول الوسومtag
.
ستُنشئ هذه المفاتيح العلاقات ما بين الجداول.
وبعد الانتهاء من الجدول post_tag
، أنشأنا النموذج Tag
الممثّل للجدول الذي سنخزّن ضمنه الوسوم، ويحتوي هذا الجدول على عمودين:
-
id
: معرّف الوسم. -
name
: اسم الوسم.
استخدمنا اسم الوسم ضمن التابع الخاص ()__repr__
بغية إعطاء كل كائن وسم تمثيلًا محرفيًا واضحًا لأسباب تنقيحية، كما أضفنا متغير صنف للوسوم باسم tags
ضمن النموذج Post
، وذلك باستخدام التابع ()db.relationship
ممررين إليه اسم نموذج الوسوم، وهو Tag
في حالتنا.
مرّرنا جدول الارتباط post_tag
إلى المعامل الثاني secondary
من التابع بغية إنشاء علاقة من نوع مُتعدّد-إلى-مُتعدّد ما بين التدوينات والوسوم. استخدمنا المعامل backref
لإضافة مرجعٍ عكسي back reference يلعب ما يشبه دور العمود في النموذج Tag
، وبذلك يصبح من الممكن الوصول تدوينات الوسم المُحدّد عبر التعليمة tag.posts
ووسوم تدوينة ما باستخدام التعليمة post.tags
، وسنعرض مثالًا يوضّح هذه الفكرة لاحقًا.
أمّا الآن فسنحرّر برنامج بايثون "init_db.py" لتعديل قاعدة البيانات بإضافة كل من جدول الارتباط post_tag
وجدول الوسوم المبني استنادًا إلى نموذج الوسوم Tag
، على النحو التالي:
(env)user@localhost:$ nano init_db.py
ونعدّل الملف ليصبح كما يلي:
from app import db, Post, Comment, Tag db.drop_all() db.create_all() post1 = Post(title='Post The First', content='Content for the first post') post2 = Post(title='Post The Second', content='Content for the Second post') post3 = Post(title='Post The Third', content='Content for the third post') comment1 = Comment(content='Comment for the first post', post=post1) comment2 = Comment(content='Comment for the second post', post=post2) comment3 = Comment(content='Another comment for the second post', post_id=2) comment4 = Comment(content='Another comment for the first post', post_id=1) tag1 = Tag(name='animals') tag2 = Tag(name='tech') tag3 = Tag(name='cooking') tag4 = Tag(name='writing') post1.tags.append(tag1) # Tag the first post with 'animals' post1.tags.append(tag4) # Tag the first post with 'writing' post3.tags.append(tag3) # Tag the third post with 'cooking' post3.tags.append(tag2) # Tag the third post with 'tech' post3.tags.append(tag4) # Tag the third post with 'writing' db.session.add_all([post1, post2, post3]) db.session.add_all([comment1, comment2, comment3, comment4]) db.session.add_all([tag1, tag2, tag3, tag4]) db.session.commit()
احفظ الملف واغلقه.
استوردنا في الشيفرة السابقة النموذج Tag
، كما حذفنا كل محتويات قاعدة البيانات باستخدام الدالة ()db.drop_all
بغية إضافة كل من جدول الوسوم والجدول post_tag
بأمان، متجنبين بذلك وقوع أي من المشاكل الشائعة التي قد تحدث لدى إضافة جداول جديدة إلى قاعدة البيانات. أنشأنا بعد ذلك جميع الجداول مُجدّدًا باستخدام الدالة ()db.create_all
.
يمكننا -بعد تصريح الشيفرة من المقال السابق عن التدوينات والتعليقات- استخدام نموذج الوسوم Tag
لإنشاء أربعة وسوم، ومن ثم إضافة الوسوم إلى التدوينات باستخدام السمة tags
، التي أُضيفت عبر السطر البرمجي:
tags = db.relationship('Tag', secondary=post_tag, backref='posts')
من الملف app.py. كما خصّصنا الوسوم للتدوينات باستخدام تابع الإسناد ()append
الشبيه بقوائم بايثون.
أُضيفت بعد ذلك الوسوم المُنشأة إلى جلسة قاعدة البيانات باستخدام الدالة ()db.session.add_all
.
ملاحظة: لا تعيد الدالة ()db.create_all
إنشاء جدول موجود أصلًا ولا تحدثّه، فعلى سبيل المثال، لو عدّلنا أحد النماذج بإضافة عمود جديد إليه، ثمّ استخدمنا الدالة ()db.create_all
فلن يُطبَّق هذا التغيير على الجدول في حال كان هذا الجدول موجود أصلًا. ويكون الحل بحذف كافّة الجداول الموجودة في قاعدة البيانات باستخدام الدالة ()db.drop_all
ومن ثمّ إعادة إنشائها باستخدام الدلة ()db.create_all
كما هو موضّح في الملف "init_db.py".
تضمن هذه الآلية تطبيق التغييرات المُنفّذة على النماذج إلّا أنّها ستتسبب بحذف كافّة البيانات الموجودة في الجداول. من الممكن تهجير ملف تخطيط قاعدة البيانات Schema migration بغية تحديث قاعدة البيانات مع الحفاظ على البيانات الموجودة ضمنها، وهذا التهجير سيسمح بتعديل الجداول مع الحفاظ على البيانات فيها، إذ يمكننا استخدام الإضافة Flask-Migrate
لإجراء ترحيل لملف تخطيط قاعدة بيانات SQLAlchemy عن طريق واجهة سطر أوامر فلاسك.
سنشغّل الآن البرنامج init_db.py لتطبيق التغييرات على قاعدة البيانات:
(env)user@localhost:$ python init_db.py
يُفترض في هذه المرحلة أن يُنفّذ البرنامج بنجاح دون ظهور أي خرج، أمّا في حال ظهور رسالة خطأ، فتأكّد من كونك قد أجريت التغييرات الصحيحة على الملف init_db.py.
ولإلقاء نظرة على التدوينات والوسوم الموجودة حاليًا في قاعدة البيانات، سنفتح صَدَفَة فلاسك:
(env)user@localhost:$ flask shell
ثمّ سننفّذ الشيفرة التالية التي ستمر على كافّة التدوينات والوسوم:
from app import Post posts = Post.query.all() for post in posts: print(post.title) print(post.tags) print('---')
استوردنا في الشيفرة السابقة نموذج التدوينات Post
من الملف app.py، واستعلمنا عن جدول التدوينات جالبين منه كافّة التدوينات الموجودة في قاعدة البيانات، ومررنا بعدها على كل تدوينة لنعرض عنوانها وقائمة الوسوم المرتبطة بها، وسيظهر الخرج كما يلي:
Post The First [<Tag "animals">, <Tag "writing">] --- Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] --- Post The Second [] ---
يمكنك الوصول إلى أسماء الوسوم باستخدام الأمر tag.name
كما هو مبين في المثال التالي، والذي يمكنك تشغيله باستخدام صَدَفَة فلاسك:
from app import Post posts = Post.query.all() for post in posts: print('TITLE: ', post.title) print('-') print('TAGS:') for tag in post.tags: print('> ', tag.name) print('-'*30)
وفي هذه الحالة وبالإضافة إلى طباعة عنوان كل تدوينة، نمرّ على وسوم كل منها ونطبع أسماءها، لنحصل على خرجٍ على النحو التالي:
TITLE: Post The First - TAGS: > animals > writing ------------------------------ TITLE: Post The Third - TAGS: > cooking > tech > writing ------------------------------ TITLE: Post The Second - TAGS: ------------------------------
نلاحظ مما سبق أنّ الوسوم المُضافة إلى التدوينات في البرنامج init_db.py متموضعة بمكانها الصحيح بالنسبة للتدوينات.
وللاطلاع على مثال لتوضيح كيفية الوصول إلى التدوينات التابعة لكل وسم باستخدام التعليمة tag.posts
، سنشغّل الشيفرة التالية في صّدّفّة فلاسك:
from app import Tag writing_tag = Tag.query.filter_by(name='writing').first() for post in writing_tag.posts: print(post.title) print('-'*6) print(post.content) print('-') print([tag.name for tag in post.tags]) print('-'*20)
استوردنا في الشيفرة السابقة النموذج Tag
، ثمّ استخدمنا تابع الترشيح ()filter_by
على سمة الاستعلام query
ممررين إليه معامل الاسم name
ليحصل على الوسم writing
، ونحصل على أوّل نتيجة باستخدام التابع ()first
، ثم خزّنا كائن الوسم الناتج ضمن متغير باسم writing_tag
. للمزيد حول تابع الترشيح filter_by
ننصحك بقراءة الخطوة 4 من المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك.
مررنا بعد ذلك على جميع التدوينات المُتضمّنة للوسم writing
، والتي وصلنا إليها باستخدام التعليمة writing_tag.posts
، لنطبع عنوان كل تدوينة ومحتواها وقائمة بأسماء الوسوم الخاصّة بها والتي أنشأناها باستخدام طريقة استيعاب القائمة list comprehension المعتمدة على وسوم التدوينة، التي يمكننا الوصول إليها باستخدام الأمر post.tags
.
ويكون الخرج على النحو التالي:
Post The Third ------ Content for the third post - ['cooking', 'tech', 'writing'] -------------------- Post The First ------ Content for the first post - ['animals', 'writing'] --------------------
نرى في الخرج السابق التدوينتان المُتضمنتان للوسم writing
مع قائمة بايثون بأسماء كافّة وسوم كل من التدوينتين.
ومع نهاية هذه الخطوة أصبح من الممكن الوصول إلى التدوينات ووسوم كل منها أو إلى التدوينات المُتضمّنة لوسم معيّن، إذ أضفنا نموذج قاعدة بيانات ليمثّل جدول الوسوم، وربطنا ما بين جدولي التدوينات والوسوم باستخدام جدول ارتباط، كما أدخلنا عددًا من الوسوم إلى قاعدة البيانات واستخدمناها للإشارة إلى التدوينات، وأصبح من الممكن الوصول إلى التدوينات ووسوم كل منها أو إلى التدوينات المُتضمّنة لوسم ما.
سنستخدم في الخطوة التالية صَدَفَة فلاسك لإضافة تدوينات ووسوم جديدة والربط فيما بينها، كما سنتعرّف على كيفية حذف وسوم من تدوينة ما.
الخطوة 3 - إدارة البيانات في العلاقة متعدد-إلى-متعدد
سنستخدم في هذه الخطوة صَدَفَة فلاسك لإضافة تدوينات ووسوم جديدة إلى قاعدة البيانات والربط فيما بينها، إذ سنتمكّن من الوصول إلى كل تدوينة مع وسومها، وسنتعرّف على كيفية فك ارتباط عنصرين في العلاقة مُتعدّد-إلى-مُتعدّد.
بدايةً وفي حال كون صَدَفَة فلاسك غير مفتوحة أصلًا، نفتحها مع التأكّد من كون البيئة البرمجية مُفعّلة، كما يلي:
(env)user@localhost:$ flask shell
سنضيف بعد ذلك بضعة تدويناتٍ ووسوم:
from app import db, Post, Tag life_death_post = Post(title='A post on life and death', content='life and death') joy_post = Post(title='A post on joy', content='joy') life_tag = Tag(name='life') death_tag = Tag(name='death') joy_tag = Tag(name='joy') life_death_post.tags.append(life_tag) life_death_post.tags.append(death_tag) joy_post.tags.append(joy_tag) db.session.add_all([life_death_post, joy_post, life_tag, death_tag, joy_tag]) db.session.commit()
وهذا ما سيُنشئ بالنتيجة تدوينتين وثلاثة وسوم. إذ أشرنا إلى التدوينات باستخدام الوسوم الموافقة، كما استخدمنا التابع ()add_all
لإضافة العناصر المُنشأة حديثًا إلى جلسة قاعدة البيانات. ونهايةً أكدنا التغييرات وطبّقناها على قاعدة البيانات باستخدام التابع ()commit
.
فيما يلي سنستخدم صَدَفة فلاسك للحصول على كافّة التدوينات ووسومها كما فعلنا في الخطوة السابقة، على النحو التالي:
posts = Post.query.all() for post in posts: print(post.title) print(post.tags) print('---')
فنحصل على خرجٍ شبيه بما يلي:
Post The First [<Tag "animals">, <Tag "writing">] --- Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] --- Post The Second [] --- A post on life and death [<Tag "life">, <Tag "death">] --- A post on joy [<Tag "joy">] ---
وبذلك نجد أنّ التدوينات قد أُضيفت مع وسوم كل منها.
الآن وبغية توضيح كيفية فك ارتباط عنصرين ضمن علاقة قاعدة البيانات من النوع مُتعدّد-إلى-مُتعدّد، سنفرض على سبيل المثال أنّ التدوينة الثالثة Post The Third
لم تعد معنية بالطبخ، وبالتالي يجب أن نحذف الوسم cooking
منها.
لتحقيق ذلك، سنجلب بدايةً التدوينة مع الوسم المراد حذفه، كما يلي:
>>> from app import db, Post, Tag >>> post = Post.query.filter_by(title='Post The Third').first() >>> tag = Tag.query.filter_by(name='cooking').first() >>> print(post.title) >>> print(post.tags) >>> print(tag.posts)
جلبنا في الشيفرة السابقة التدوينة ذات العنوان Post The Third
باستخدام التابع ()filter_by
، كما جلبنا الوسم cooking
، لنطبع كل من عنوان التدوينة ووسومها وكافّة التدوينات المُتضمّنة للوسم cooking
.
يعيد التابع ()filter_by
كائن استعلام، وباستخدام التابع ()all
نحصل على نتائج هذا الاستعلام ضمن قائمة، ولكن وبما أنّنا نتوقّع الحصول على نتيجة واحدة من الاستعلام في هذه الحالة، فاستخدمنا التابع ()first
للحصول فقط على النتيجة الأولى، وللمزيد حول كل من التابعين ()first
و ()all
، ننصحك بقراءة الخطوة 4 من المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك.
ويكون الخرج في هذه الحالة على النحو التالي:
Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] [<Post "Post The Third">]
وفيه نرى عنوان التدوينة ووسومها، مع قائمة بكافّة التدوينات المُتضمّنة للوسم cooking
.
الآن وبغية حذف الوسم cooking
من التدوينة، سنستخدم التابع ()remove
كما يلي:
>>> post.tags.remove(tag) >>> db.session.commit() >>> print(post.tags) >>> print(tag.posts)
استخدمنا في الشيفرة السابقة التابع ()remove
لفصل الوسم cooking
عن التدوينة، ثمّ استخدمنا التابع ()db.session.commit
لتطبيق التغييرات على قاعدة البيانات، فنحصل على خرجٍ يؤكّد حذف الوسم من التدوينة، على النحو التالي:
[<Tag "tech">, <Tag "writing">] []
وبذلك نجد أنّ الوسم cooking
قد حُذف فعلًا من القائمة post.tags
، كما أنّ التدوينة لم تعد موجودةً ضمن قائمة التدوينات tag.posts
المُتضمّنة لهذا الوسم.
نُغلق الآن صَدَفَة فلاسك:
>>> exit()
ومع نهاية هذه الخطوة نكون قد أضفنا تدويناتٍ ووسوم جديدة، كما أضفنا الوسوم إلى التدوينات وحذفناها منها.
سنعمل في الخطوة التالية على عرض الوسوم الخاصّة بكل تدوينة في صفحة تطبيق المدونة الرئيسية.
الخطوة 4 - عرض الوسوم أسفل كل تدوينة
سنحرّر في هذه الخطوة قالب الصفحة الرئيسية لعرض الوسوم الخاصّة بكل تدوينة أسفلها. لذا، اطلع بدايةً على البنية الحالية للصفحة الرئيسية للتطبيق.
أعلم فلاسك بالتطبيق المطلوب تشغيله وهو في حالتنا الملف app.py باستخدام متغير البيئة FLASK_APP
وذلك بعد التأكد من أن البيئة البرمجية مُفعّلة، واضبط متغير البيئة FLASK_ENV
على وضع التطوير development
، على النحو التالي:
(env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development
شغّل البرنامج الآن:
(env)user@localhost:$ flask run
وأثناء كون خادم التطوير قيد التشغيل، انتقل إلى الرابط التالي باستخدام المُتصفح:
http://127.0.0.1:5000/
فستظهر لك صفحة شبيهة بالشّكل التالي:
نترك خادم التطوير قيد التشغيل ونفتح نافذة سطر أوامر جديدة.
نريد في هذه المرحلة عرض كافّة وسوم كل تدوينة ضمن صفحتين من صفحات التطبيق: الأولى في الصفحة الرئيسية أسفل كل تدوينة، والثانية في الصفحة المُخصّصة لكل تدوينة لتظهر الوسوم أسفل محتواها، إذ سنستخدم في الحالتين نفس الشيفرة لعرض الوسوم، لذا ولتجنّب التكرار سنستخدم ماكرو جينجا jinja macro، الذي يعمل مثل أي دالة في بايثون ويتضمّن شيفرة HTML ديناميكية قابلة للاستخدام في أي مكان يُستدعى فيه الماكرو؛ وفي حال التعديل عليه، سيُعدّل تلقائيًا في كافّة الأماكن المُستدعى فيها ما يجعل الشيفرة قابلة لإعادة الاستخدام.
لذا، سننشئ ملفًا جديدًا باسم "macros.html" ضمن مجلد القوالب templates، على النحو التالي:
(env)user@localhost:$ nano templates/macros.html
ونكتب ضمنه الشيفرة التالية:
{% macro display_tags(post) %} <div class="tags"> <p> <h4>Tags:</h4> {% for tag in post.tags %} <a href="#" style="text-decoration: none; color: #dd5b5b"> {{ tag.name }} </a> | {% endfor %} </p> </div> {% endmacro %}
نحفظ الملف ونغلقه.
استخدمنا في الشيفرة السابقة الكلمة المفتاحية macro
للتصريح عن ماكرو باسم ()display_tags
ذو معامل باسم post
، إذ استخدمنا الوسم <div>
ليعرض عنوانًا من المستوى الرابع <h4>
، كما استخدمنا حلقة for
تكرارية بغية المرور على كافّة وسوم كائن التدوينة المُمرّرة مثل وسيط إلى الماكرو المُستدعى، كما هو الحال لدى تمرير أي وسيط لدالة بايثون اعتيادية مُراد استدعاؤها.
حصلنا على وسوم كل تدوينة باستخدام التعليمة post.tags
، لنعرض اسم الوسم ضمن وسم رابط <a>
، إذ سنعدّل لاحقًا قيمة السمة href
لوسم الرابط لنجعله يشير إلى صفحة مُخصّصة للوسم سننشئها لاحقًا لتعرض كافّة التدوينات المُتضمّنة للوسم المُحدّد. نهايةً، استخدمنا الكلمة المفتاحية endmacro
للدلالة على انتهاء الماكرو.
الآن، وبغية عرض كافّة الوسوم التابعة للتدوينة أسفلها في الصفحة الرئيسية للتطبيق، سنفتح ملف القالب index.html:
(env)user@localhost:$ nano templates/index.html
وسنستورد بدايةً الماكرو ()display_tags
من الملف "macros.html"، لذا سنضيف الاستيرادات التالية إلى بداية الملف قبل السطر {% extends 'base.html' %}
، كما يلي:
{% from 'macros.html' import display_tags %} {% extends 'base.html' %}
سنعدّل بعد ذلك الحلقة التكرارية for post in posts
لتتضمّن استدعاءً للماكرو ()display_tags
على النحو التالي:
{% for post in posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="{{ url_for('post', post_id=post.id)}}"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> </div> {% endfor %}
نحفظ الملف ونغلقه.
استدعينا في الشيفرة السابقة الشيفرة الجامعة ()display_tags
ممررين إليها الكائن post
، وهذا ما سيعرض بالنتيجة أسماء الوسوم أسفل كل تدوينة.
الآن، بتحديث الصفحة الرئيسية للتطبيق، نرى الوسوم أسفل كل تدوينة كما في الشّكل:
أمّا الآن فسنعمل على إضافة الوسوم أسفل محتوى التدوينة في الصفحة المُخصّصة لها. لذا، نفتح ملف القالب post.html:
(env)user@localhost:$ nano templates/post.html
ونستورد في بدايته الماكرو display_tags
بالشّكل:
{% from 'macros.html' import display_tags %} {% extends 'base.html' %}
ونستدعي بعد ذلك الماكرو ()display_tags
ممررين إليه الكائن post
أسفل محتوى التدوينة وقبل وسم الفاصل الأفقي <hr>
على النحو التالي:
<div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title">{{ post.title }}</p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> <h3>Comments</h3>
نحفظ الملف ونغلقه.
الآن وبالانتقال إلى صفحة التدوينة (الثانية مثلًا) باستخدام الرابط:
http://127.0.0.1:5000/2
نرى الوسوم معروضة أسفل محتوى التدوينة بنفس طريقة عرضها في الصفحة الرئيسية للتطبيق.
سنعمل في الخطوة التالية على إضافة وجهة جديدة إلى تطبيق فلاسك هذا لتعرض كافّة التدوينات المُتضمّنة لوسم معيّن، كما سنجعل رابط الوسم الذي أضفناه في هذه الخطوة فعّالًا.
الخطوة 5 - عرض الوسوم وتدويناتها
سنضيف في هذه الخطوة وجهةً وقالبًا إلى التطبيق لعرض الوسوم الموجودة في قاعدة البيانات مع التدوينات المُتضمّنة لكل منها.
لذا، سنضيف بدايةً وجهةً لعرض التدوينات المُتضمّنة لكل وسم، فعلى سبيل المثال، ستعرض الوجهة /tags/tag_name/
صفحةً تحتوي كافّة التدوينات المُتضمّنة للوسم المُحدّد في الجزء المُخصّص لاسم الوسم المطلوب tag_name
من الوجهة.
نقتح الآن الملف app.py لتحريره:
(env)user@localhost:$ nano app.py
ونضيف الوجهة التالية إلى نهايته:
# ... @app.route('/tags/<tag_name>/') def tag(tag_name): tag = Tag.query.filter_by(name=tag_name).first_or_404() return render_template('tag.html', tag=tag)
نحفظ الملف ونغلقه.
استخدمنا في الشيفرة السابقة متغير الرابط tag_name
المسؤول عن تحديد كل من الوسم والتدوينات المُتضمّنة لهذا الوسم والتي ستُعرض ضمن صفحة الوسم، إذ يُمرّر اسم الوسم إلى الدالة ()tag
العاملة في فلاسك باستخدام المعامل tag_name
المُستخدم أيضًا ضمن تابع الترشيح ()filter_by
للاستعلام عن الوسم المطلوب.
استخدمنا التابع ()first_or_404
للحصول على كائن الوسم، فإمّا أن يُخزّن في حال وجوده ضمن متغير باسم tag
، وإلّا سنحصل على رسالة الخطأ "404 Not Found" في حال عدم وجود وسم في قاعدة البيانات باسم مطابق للاسم المطلوب. ثمّ صيّرنا ملف قالب باسم "tag.html" مُمررين إليه الكائن tag
.
سنفتح الآن القالب الجديد "templates/tag.html" بغية تحريره:
(env)user@localhost:$ nano templates/tag.html
ونكتب ضمنه الشيفرة التالية:
{% from 'macros.html' import display_tags %} {% extends 'base.html' %} {% block content %} <span class="title"> <h1>{% block title %} Posts Tagged with "{{ tag.name }}" {% endblock %}</h1> </span> <div class="content"> {% for post in tag.posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="{{ url_for('post', post_id=post.id)}}"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> </div> {% endfor %} </div> {% endblock %}
نحفظ الملف ونغلقه.
استوردنا في الشيفرة السابقة الماكرو ()display_tags
من الملف "macros.html"، واستخدمنا القالب الرئيسي بالاعتماد على تعليمة extend
. كما عيّنا ترويسةً ضمن كتلة المحتوى لتعمل مثل عنوان مُتضمنةً اسم الوسم، مرّرنا بعد ذلك على كافّة التدوينات المُتضمّنة للوسم المطلوب والتي قد وصلنا إليها باستخدام التعليمة tag.posts
، لنعرض معرّف التدوينة وعنوانها ومحتواها، ومن ثمّ استدعينا الماكرو ()display_tags
لعرض كافّة الوسوم التابعة للتدوينة.
الآن ومع كون خادم التطوير قيد التشغيل، ننتقل إلى الرابط التالي باستخدام المُتصفّح:
http://127.0.0.1:5000/tags/writing/
وهي الصفحة المُخصّصة للوسم writing
، وكما هو موضّح في الصورة أدناه فإنّ كافّة الوسوم المُتضمّنة للوسم writing
معروضةٌ في هذه الصفحة.
أمّا الآن فسنعدّل الماكرو ()display_tags
لجعل روابط الوسوم فعّالة، لذا سنفتح الملف "macros.html":
(env)user@localhost:$ nano templates/macros.html
ونعدّل قيمة السمة href
كما يلي:
{% macro display_tags(post) %} <div class="tags"> <p> <h4>Tags:</h4> {% for tag in post.tags %} <a href="{{ url_for('tag', tag_name=tag.name) }}" style="text-decoration: none; color: #dd5b5b"> {{ tag.name }} </a> | {% endfor %} </p> </div> {% endmacro %}
نحفظ الملف ونغلقه.
الآن وبتحديث الصفحات التي استخدمنا فيها الماكرو ()display_tags
، نلاحظ أنّ روابط الوسوم أصبحت فعّالة:
http://127.0.0.1:5000/ http://127.0.0.1:5000/2/ http://127.0.0.1:5000/tags/writing/
وبذلك نجد أنّ استخدام الماكرو المُقدّمة من محرّك القوالب جينجا سمح بإعادة استخدام الشيفرة وبالتالي تقليل التكرار، ناهيك عن حقيقة كون تعديل ملف الماكرو الرئيسي كفيل بتطبيق التغييرات على كافّة القوالب المُستخدِمة له.
ومع نهاية هذه الخطوة أصبح لدينا صفحة خاصّة بكل وسم يتمكّن من خلالها المُستخدمون من رؤية كافّة التدوينات المُتضمّنة لوسم معيّن، إذ يمكن الوصول إلى صفحة الوسم هذه من خلال النقر على الوسوم أسفل كل تدوينة كونها تتضمّن روابط إلى الصفحة الخاصّة بالوسم.
الخاتمة
وضحنا في هذا المقال كيفية إدارة العلاقات من نوع مُتعدّد-إلى-مُتعدّد باستخدام الإضافة Flask-SQLAlchemy وذلك بإضافة ميزة الوسوم إلى تطبيق المدونة السابق، إذ تعرفنا على كيفية ربط جدولين باستخدام جدول الارتباط (والذي يُدعى أيضًا جدول الصلة)، وكيفية ربط المدخلات ببعضها وإضافتها إلى قاعدة البيانات والوصول إلى البيانات وفك ارتباطها عن أحد المُدخلات.
ترجمة -وبتصرف- للمقال How To Use Many-to-Many Database Relationships with Flask-SQLAlchemy لصاحبه Abdelhadi Dyouri.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.