اذهب إلى المحتوى

استخدام SQLAlchemy في فلاسك لإنشاء مدونة مع علاقات many-to-many


محمد الخضور

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

أمّا SQLAlchemy، فهي أداةٌ في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات، مثل SQLite و MySQL و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر رابط الكائنات بالعلاقات Object Relational Mapper -أو اختصارًا ORM- الذي يتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون.

تعدّ Flask-SQLAlchemy إضافة لفلاسك تسهّل من استخدام SQLAlchemy ضمن فلاسك مؤمنةً الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy.

تُعرَّف علاقة قاعدة البيانات متعدّد-إلى-متعدّد many-to-many على أنها علاقةٌ بين جدولين، إذ يمكن لأي سجلٍ من الجدولين الارتباط مع عدّة سجلات من الآخر، فمثلًا في تطبيق مدونة يمكن لجدول التدوينات posts أن يرتبط بعلاقة من نوع متعدّد-إلى-متعدّد مع جدول أسماء المؤلفين (كُتَّاب التدوينات)، بمعنى أن كل تدوينة قد ترتبط بعدّة أسماء مؤلفين، وأيضًا قد يرتبط كل اسم مؤلف بعدّة تدوينات، وبالتالي فإنّ العلاقة ما بين التدوينات وأسماء المؤلفين هي علاقة متعدّد-إلى-متعدّد. أيُّ تطبيقٍ آخر من تطبيقات التواصل الاجتماعي هو مثالٌ آخر، إذ أن كل منشور قد يحتوي عدّة إشارات مرجعية، وكل إشارة مرجعية قد تتضمّن عدة منشورات.

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

يمكنك قراءة هذا المقال مُنفصلًا، إلّا أنّه تكملة لمقالنا السابق كيفية استخدام SQLAlchemy في فلاسك، وفيه قد بنينا قاعدة بيانات مُتعدّدة الجداول بعلاقة من نواع واحد إلى مُتعدّد one-to-many ما بين التدوينات والتعليقات عليها وذلك ضمن تطبيق مدونة.

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

مستلزمات العمل

قبل المتابعة في هذا المقال لا بُدّ من:

الخطوة 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/

فتظهر صفحة شبيهة بما يلي:

1st.png

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

يمكنك إيقاف تشغيل خادم التطوير بالضغط على مفتاحي "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/

فستظهر لك صفحة شبيهة بالشّكل التالي:

2nd.png

نترك خادم التطوير قيد التشغيل ونفتح نافذة سطر أوامر جديدة.

نريد في هذه المرحلة عرض كافّة وسوم كل تدوينة ضمن صفحتين من صفحات التطبيق: الأولى في الصفحة الرئيسية أسفل كل تدوينة، والثانية في الصفحة المُخصّصة لكل تدوينة لتظهر الوسوم أسفل محتواها، إذ سنستخدم في الحالتين نفس الشيفرة لعرض الوسوم، لذا ولتجنّب التكرار سنستخدم ماكرو جينجا 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، وهذا ما سيعرض بالنتيجة أسماء الوسوم أسفل كل تدوينة.

الآن، بتحديث الصفحة الرئيسية للتطبيق، نرى الوسوم أسفل كل تدوينة كما في الشّكل:

3rd.png

أمّا الآن فسنعمل على إضافة الوسوم أسفل محتوى التدوينة في الصفحة المُخصّصة لها. لذا، نفتح ملف القالب 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 معروضةٌ في هذه الصفحة.

4th.png

أمّا الآن فسنعدّل الماكرو ()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.

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...