19. استفاده از ORM SQLALCHEMY
بخش قبلی نشان داد که در برخی موارد میتوان کدی نوشت که مستقل از اسکریپت SGBD باشد، با استفاده از معماری زیر:

در این فصل، ما از ORM (نقشهبردار شیء-رابطهای) [sqlalchemy] برای دسترسی به SGBD به روشی یکپارچه، صرفنظر از SGBD مورد استفاده، استفاده خواهیم کرد. یک ORM دو چیز را ممکن میسازد:
- این امکان را برای یک اسکریپت فراهم میکند که بدون صدور دستورات SQL با SGBD تعامل داشته باشد؛
- ویژگیهای خاص هر SGBD را از اسکریپت پنهان میکند؛
معماری اکنون به شرح زیر است:
اسکریپت اکنون توسط ORM از کانکتورها جدا شده است. این اسکریپت با استفاده از کلاسها و متدها با ORM ارتباط برقرار میکند. این اسکریپت هیچ کد SQL را اجرا نمیکند. این ORM است که این کار را از طریق کانکتورهایی که به آن متصل هستند انجام میدهد. این کانکتورها جزئیات خاص خود را از اسکریپت پنهان میکنند. در نتیجه، کد اسکریپت تحت تأثیر تغییر کانکتور (و در نتیجه تغییر SGBD) قرار نمیگیرد؛
ساختار دایرکتوری برای اسکریپتهای مورد نظر به شرح زیر خواهد بود:

19.1. نصب ORM و [sqlalchemy]
ORM [sqlalchemy] به صورت یک بسته پایتون عرضه میشود که باید در یک ترمینال پایتون نصب شود:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\sqlalchemy>pip install sqlalchemy
Collecting sqlalchemy
Downloading SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl (1.2 MB)
|| 1.2 MB 3.3 MB/s
Installing collected packages: sqlalchemy
Successfully installed sqlalchemy-1.3.18
19.2. اسکریپتها ۰۱: مبانی

- در [1]، اسکریپتهایی که باید بررسی شوند. این اسکریپتها از کلاسهای [2]: BaseEntity، MyException، Person و Utils استفاده خواهند کرد؛
19.2.1. پیکربندی
فایل [config] برنامه را به شرح زیر پیکربندی میکند:
def configure():
# root_dir
# مسیر مطلق که به مسیرهای نسبی در پیکربندی ارجاع میدهد
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# مسیرهای مطلق وابستگیها
absolute_dependencies = [
#BaseEntity, MyException, Person, Tools
f"{root_dir}/classes/02/entities",
]
# مسیر سیستم را تنظیم کنید
from myutils import set_syspath
set_syspath(absolute_dependencies)
# پیکربندی کلاس
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
#ذخیره پیکربندی
return {}
توضیحات
- خط ۸: پوشهای که شامل کلاسهای [BaseEntity, MyException, Personne, Utils] است به مسیر پایتون اضافه میشود؛
- خطوط ۱۲–۱۳: مسیر پایتون برنامه تنظیم میشود؛
- خطوط ۱۶–۱۷: ممکن است به یاد داشته باشید که کلاس |BaseEntity| دارای یک ویژگی کلاس به نام [excluded_keys] است. این ویژگی یک لیست است که در آن ویژگیهای کلاسی را قرار میدهیم که نمیخواهیم در دیکشنری کلاس (تابع asdict) ظاهر شوند. در اینجا، ویژگی [_sa_instance_state] را از وضعیت کلاس [Personne] حذف میکنیم. به زودی خواهیم دید چرا؛
19.2.2. اسکریپت [démo]
اسکریپت [démo] یک کاربرد اولیه از ORM و [sqlalchemy] را نشان میدهد:
#پیکربندی برنامه را بازیابی میکند
import config
config = config.configure()
#وارد میکند
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper
from Personne import Personne
# فراداده
metadata = MetaData()
# جدول
personnes_table = Table("personnes", metadata,
Column('id', Integer, primary_key=True),
Column('prenom', String(30), nullable=False),
Column("nom", String(30), nullable=False),
Column("age", Integer, nullable=False),
UniqueConstraint('nom', 'prenom', name='uix_1')
)
#کلاس Person قبل از نگاشت
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")
# نقشهبرداری
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'نام: personnes_table.c.prenom,
'نام خانوادگی: personnes_table.c.nom,
'سن: personnes_table.c.age
})
# شخص ۱ تغییر نکرده است
print(f"personne1={personne1.__dict__}")
#کلاس Person اصلاح شده است – غنیسازی شده است
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")
توضیحات
- خطوط ۱–۴: برنامه پیکربندی میشود؛
- خطوط ۶–۱۰: ماژولهای مورد نیاز اسکریپت وارد میشوند؛
- خط ۱۳: [MetaData] یک کلاس از [sqlalchemy] است؛
- خطوط ۱۵–۲۲: [Table] یک کلاس از [sqlalchemy] است. این کلاس برای توصیف یک جدول پایگاه داده استفاده میشود. در اینجا، جدول [personnes] را از پایگاه داده MySQL [dbpersonnes]، که در فصل |MySQL| به آن پرداخته شد، توصیف خواهیم کرد؛
- خط ۱۶: پارامتر اول، [personnes]، نام جدول توصیفشده است؛
- خط ۱۶: پارامتر دوم [metadata]، نمونه [MetaData] است که در خط ۱۳ ایجاد شده است؛
- خطوط 17–22: هر یک از پارامترهای زیر یک ستون در جدول را با استفاده از نحوی که مختص [sqlalchemy] است اما مشابه نحوی SQL میباشد، توصیف میکند؛
- هر ستون با استفاده از یک نمونه از کلاس [Column] از [sqlalchemy] توصیف میشود؛
- پارامتر اول نام ستون است؛
- پارامتر دوم نوع آن است؛
- پارامترهای زیر، پارامترهای نامگذاریشده هستند:
- خط 17: [primary_key=True] برای نشان دادن اینکه ستون [id] کلید اصلی جدول [personnes] است؛
- خط ۱۸: [nullable=False] برای نشان دادن اینکه یک ستون باید هنگام درج یک سطر در جدول، مقدار داشته باشد؛
- خط ۲۱: در نهایت، کلاس [UniqueConstraint] برای تعریف یک محدودیت یکتایی استفاده میشود. در اینجا مشخص شده است که ستونها (نام خانوادگی، نام) باید در داخل جدول یکتا باشند. ویژگی با نام [name] برای اختصاص نام به این محدودیت استفاده میشود. در اینجا باید بین دو حالت تمایز قائل شد:
- ما در حال توصیف یک جدول موجود هستیم. در این حالت، باید نام قید را در ویژگیهای جدول (phpMyAdmin یا pgAdmin) جستجو کنیم؛
- شما در حال توصیف جدولی هستید که قصد دارید آن را ایجاد کنید. در این صورت، میتوانید هر نامی را که میخواهید وارد کنید؛
- سطور 23–25: ما یک شخص [personne1] ایجاد میکنیم و واژهنامه او را [__dict__] نمایش میدهیم. در اینجا خواهیم داشت:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- خطوط ۲۷–۳۳: ما یک نگاشت انجام میدهیم، یعنی یک تطابق بین کلاس [Personne] و جدول [personnes] برقرار میکنیم. این در واقع یک تطابق [propriétés de la classe colonnes de la table] است. تابع [mapper] در اینجا سه پارامتر میگیرد:
- خط ۲۸: پارامتر اول نام کلاسی است که نگاشت برای آن انجام میشود؛
- خط ۲۸: پارامتر دوم جدولی است که با آن مرتبط خواهد شد. این شیء [Table] است که در خط ۱۶ ایجاد شده است؛
- خط ۲۸: پارامتر سوم اینجا پارامتری به نام [properties] است. این یک دیکشنری است که کلیدهای آن ویژگیهای کلاس نگاشتشده و مقادیر آن ستونهای جدول نگاشتشده هستند. برای اشاره به ستون X از جدول [personnes_table]، مینویسیم [personnes_table.c.X];
- خطوط ۳۵–۳۶: شخص [personne1] پس از اتمام نگاشت دوباره نمایش داده میشود. میتوان دید که آنها تغییر نکردهاند:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
- خطوط ۳۷–۳۹: یک شخص جدید، [personne2]، ایجاد و نمایش داده میشود. سپس خروجی زیر نمایش داده میشود:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
میتوانیم ببینیم که فرهنگ لغت [__dict__] به طور قابل توجهی تغییر کرده است:
- (ادامه)
- یک ویژگی جدید، [_sa_instance_state]، ظاهر میشود. میبینیم که این یک شیء از کلاس ORM [sqlalchemy] است؛
- پیشوند سایر ویژگیها حذف شده است، که قبلاً نشان میداد به کدام کلاس تعلق دارند؛
بنابراین میتوان نتیجه گرفت که عملیات نگاشت در خطوط ۲۷ تا ۳۳ کلاس [Personne] را تغییر داده است.
وقتی میخواهیم وضعیت یک شیء [Personne] را نمایش دهیم، معمولاً ویژگی [_sa_instance_state] را نمیخواهیم. در واقع، این ویژگی تنها برای عملکردهای داخلی [sqlalchemy] وجود دارد و عموماً برای ما اهمیتی ندارد. به همین دلیل است که ما در اسکریپت [config] موارد زیر را نوشتهایم:
#پیکربندی کلاس
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
19.2.3. اسکریپت [main]
اسکریپت [main] با تعامل با [sqlalchemy]، جدول [personnes] را در پایگاه داده MySQL [dbpersonnes] دستکاری خواهد کرد. برای درک مطالب بعدی، مهم است که معماری مورد استفاده در اینجا را در نظر داشته باشید:

اگر [Database1] پایگاه داده [dbpersonnes] باشد، میتوانیم ببینیم که ارتباط بین اسکریپت و این پایگاه داده از طریق دو موجودیت برقرار میشود:
- اتصالدهنده پایتون به SGBD MySQL;
- SGBD و MySQL؛
اسکریپت [main] با ORM ارتباط برقرار میکند که سپس با کانکتور پایتون ارتباط برقرار خواهد کرد. اسکریپت ORM با استفاده از ابزارهای توصیفشده در بخشهای |MySQL| و |PostgreSQL|، بهویژه با صدور دستورات SQL، با این کانکتور ارتباط برقرار میکند. اسکریپت [main] از دستورات SQL استفاده نخواهد کرد. این اسکریپت به رابط برنامهنویسی کاربردی (API) API از ORM متکی خواهد بود که از کلاسها و رابطها تشکیل شده است.
اسکریپت [main] به شرح زیر است:
# برنامه پیکربندی شده است
import config
config = config.configure()
# وارداتها
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.exc import IntegrityError, InterfaceError
from sqlalchemy.orm import mapper, sessionmaker
from Personne import Personne
#رشته اتصال پایگاه داده MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# مِتادیتا
metadata = MetaData()
# جدول
personnes_table = Table("personnes", metadata,
Column('id', Integer, primary_key=True),
Column('prenom', String(30), nullable=False),
Column("nom", String(30), nullable=False),
Column("age", Integer, nullable=False),
UniqueConstraint('nom', 'prenom', name='uix_1')
)
# نقشهبرداری
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'نام: personnes_table.c.prenom,
'نام خانوادگی: personnes_table.c.nom,
'سن: personnes_table.c.age
})
#کارخانهٔ جلسه
Session = sessionmaker()
Session.configure(bind=engine)
session = None
try:
# یک جلسه
session = Session()
# حذف جدول [personnes]
session.execute("drop table if exists personnes")
# بازآفرینی جدول از روی نگاشت
metadata.create_all(engine)
# یک درج
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
#session.commit()
# یک پرسوجو
personnes = session.query(Personne).all()
#نمایش
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
# دو درج دیگر، که دومی بهدلیل تکراری بودن (نام، نام خانوادگی) ناموفق است
session.add(Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 10}))
session.add(Personne().fromdict({"id": 69, "prénom": "x1", "nom": "y1", "âge": 10}))
# یک پرسوجو
personnes = session.query(Personne).all()
# نمایش
print("Liste des personnes ---------")
for personne in personnes:
print(personne)
#اعتبارسنجی جلسه
session.commit()
except (InterfaceError, IntegrityError) as erreur:
# نمایش
print(f"L'erreur suivante s'est produite : {erreur}")
#لغو آخرین جلسه
if session:
print("rollback...")
session.rollback()
finally:
#منابع جلسه آزاد میشوند
if session:
session.close()
توضیحات
- خطوط ۱–۴: برنامه پیکربندی میشود؛
- خطوط ۷–۹: یک سری کامل از کلاسها و رابطها از کتابخانه [sqlalchemy] وارد میشوند؛
- خط ۱۱: کلاس [Personne] وارد میشود؛
- خط 14: رشته اتصال پایگاه داده. این مشخص میکند:
- پروندهٔ SGBD استفادهشده (mysql);
- اتصالدهنده پایتون مورد استفاده (mysql.connector بدون نقطه ویرگول);
- کاربر واردشده (admpersonnes);
- رمز عبور آنها (nobody);
- ماشینی که SGBD روی آن قرار دارد (localhost = ماشینی که اسکریپت روی آن در حال اجرا است)؛
- نام پایگاه داده (dbpersonnes);
با این اطلاعات، [sqlalchemy] میتواند به پایگاه داده متصل شود. لطفاً توجه داشته باشید که کانکتور پایتون مورد استفاده باید از قبل نصب شده باشد. [sqlalchemy] این کار را انجام نمیدهد.
- خطوط ۱۹–۲۶: توصیف جدول [personnes]؛
- خطوط ۲۸–۳۴: نگاشت بین کلاس [Personne] و جدول [personnes];
- خطوط ۳۶–۳۸: بیشتر عملیات [sqlalchemy] در یک جلسه انجام میشوند. مفهوم جلسه [sqlalchemy] مشابه مفهوم تراکنش SQL است. جلسهها با استفاده از کلاس [Session] ایجاد میشوند که توسط تابع [sessionmaker] در خط ۳۷ بازگردانده میشود؛
- خط ۳۸: کلاس [Session] از طریق رشته اتصال در خط ۱۴ به پایگاه داده [dbpersonnes] مرتبط میشود؛
- خط ۴۳: یک جلسه ایجاد میشود. همانطور که گفته شد، یک جلسه را میتوان به یک تراکنش تشبیه کرد؛
- خطوط ۴۵–۴۶: متد [Session.execute] برای اجرای یک دستور SQL استفاده میشود. این یک رویه رایج نیست، زیرا گفته شده است که ORM امکان اجتناب از زبان SQL را فراهم میکند؛
- خطوط ۴۸–۴۹: متد [metadata.create_all] با استفاده از نمونه [MetaData] از خط ۱۷، همه جدولها را ایجاد میکند. ما تنها یکی داریم: جدول [personnes] که در خطوط ۲۰–۲۶ تعریف شده است. [sqlalchemy] از اطلاعات این خطوط برای ایجاد جدول استفاده خواهد کرد. این یکی از مزایای اصلی ORM است: این کار جزئیات خاص فایلهای SGBD را پنهان میکند. در واقع، ترتیب SQL و [create] میتواند به دلیل نوع دادههای اختصاصیافته به ستونها، از یک فایل SGBD به فایل دیگر به طور قابل توجهی متفاوت باشد. هیچ استانداردسازیای برای انواع دادهها صورت نگرفته است. در نتیجه، ترتیب از یک نمونه به نمونه دیگر متفاوت است. در اینجا، به لطف این رویکرد:
- میتوانیم جدول مورد نظر خود را بهطور منحصربهفرد توصیف کنیم؛
- [sqlalchemy] موفق میشود [create] مناسب را برای SGBD که در مقابل خود دارد، تولید کند؛
- خط ۵۲: ما یک شیء [Personne] را به جلسه اضافه میکنیم. این کار به طور خودکار آن را به پایگاه داده اضافه نمیکند. در واقع، یک ORM از قوانین خاص خود برای همگامسازی با پایگاه داده پیروی میکند. این شیء همیشه در تلاش است تا تعداد پرسوجوهایی را که انجام میدهد بهینهسازی کند. بیایید یک مثال بزنیم. اسکریپت دو نفر (person1، person2) را به جلسه اضافه میکند و سپس یک پرسوجو انجام میدهد: میخواهد همه افراد موجود در جدول را ببیند. [sqlalchemy] ممکن است به شرح زیر عمل کند:
- افزودن [personne1] را میتوان در حافظه انجام داد. فعلاً نیازی به وارد کردن آن به پایگاه داده نیست؛
- همین امر در مورد [personne2] نیز صدق میکند؛
- در ادامه، پرسوجویی از نوع [select] میآید. سپس باید تمام سطرهای جدول [personnes] بازیابی شوند. سپس [sqlalchemy]، [personne1, personne2] را در پایگاه داده وارد کرده و پرسوجو را اجرا میکند؛
بنابراین [sqlalchemy] بهینهسازیهایی را انجام میدهد که برای توسعهدهنده شفاف هستند.
- خط ۵۶: برای اجرای یک پرسوجوی از نوع [select] (میخواهم ببینم …)، از متد [Session.query] استفاده میشود. پارامتر متد [query]، کلاسی است که به جدول مورد پرسوجو نگاشت شده است. این متد نوع [Query] را برمیگرداند. متد [Query.all] تمام اشیاء [Personne] را از جلسه بازیابی میکند. این متد تمام سطرها را از جدول [personnes] دریافت میکند، که هر کدام به شکل یک شیء [Personne] هستند. برای این کار، [sqlalchemy] از نگاشتی که بین کلاس [Personne] و جدول [personnes] برقرار شده است، استفاده میکند. نتیجه خط 56، فهرستی از اشیاء [Personne] است؛
- خطوط ۵۸–۶۱: عناصر لیست [personnes] نمایش داده میشوند. از آنجا که کلاس [Personne] از کلاس [BaseEntity] ارث میبرد، متد [Personne.__str__] که در خط ۶۱ بهطور ضمنی استفاده شده، در واقع متد [BaseEntity.__str__] است که رشته jSON از شی فراخواننده را برمیگرداند. این رشته، رشته jSON از فرهنگ لغت [Personne.asdict] است (به |BaseEntity| مراجعه کنید). ما بیان کردیم که پس از نگاشت، ویژگی [_sa_instance_state] در هر شیء [Personne] یافت میشود. با این حال، مقدار این ویژگی از نوع [BaseEntity] نیست. بنابراین باید از دیکشنری کلاس [Personne] حذف شود، در غیر این صورت نمایش از کار میافتد (crash میکند). این کاری است که در اسکریپت [config] انجام شده است؛
- خطوط ۶۳–۶۵: ما دو فرد دیگر را اضافه میکنیم که نام و نام خانوادگی یکسانی دارند. با این حال، یک محدودیت یکتایی بر روی اتحاد این دو ستون وجود دارد. بنابراین باید خطایی رخ دهد. این چیزی است که ما در حال تلاش برای تأیید آن هستیم؛
- خطوط ۶۷–۶۸: ما دوباره لیست تمام افراد در پایگاه داده را درخواست میکنیم؛
- خطوط ۷۰–۷۳: و آنها را نمایش میدهیم؛
- خطوط ۷۵–۷۶: جلسه commit میشود. همانطور که از نام آن پیداست، تراکنش زیربنایی commit خواهد شد؛
- در زمان اجرا خواهیم دید که خطوط ۶۷ تا ۷۶ به دلیل استثنایی که توسط خط ۶۵ پرتاب میشود، اجرا نخواهند شد. سپس برای رسیدگی به این استثنا به خطوط ۷۸ تا ۸۴ میرویم؛
- خط ۷۸: استثنای [InterfaceError] زمانی رخ میدهد که [sqlalchemy] نتواند به پایگاه داده [dbpersonnes] متصل شود. استثنا [IntegrityError] در خط 65 رخ میدهد؛
- خط ۸۰: خطا نمایش داده میشود؛
- خطوط ۸۲–۸۴: اگر جلسه وجود داشته باشد، لغو میشود. این معادل بازگشت تراکنش زیربنایی است؛
- خطوط ۸۵–۸۸: در همه موارد، چه خطایی رخ دهد و چه ندهد، جلسه برای آزادسازی منابع بسته میشود؛
نتایج اجرای برنامه به شرح زیر است:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/01/main.py
Liste des personnes ---------
{"nom": "y", "prénom": "x", "id": 67, "âge": 10}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y1-x1' for key 'uix_1'
[SQL: INSERT INTO personnes (id, prenom, nom, age) VALUES (%(id)s, %(prenom)s, %(nom)s, %(age)s)]
[parameters: ({'id': 68, 'prenom': 'x1', 'nom': 'y1', 'age': 10}, {'id': 69, 'prenom': 'x1', 'nom': 'y1', 'age': 10})]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Process finished with exit code 0
- خطوط ۲–۳: فهرست افراد پس از اولین درج؛
- خط ۵: استثنای [IntegrityError] که هنگام افزودن دو نفر با نام و نام خانوادگی یکسان رخ داد؛
- خطوط ۶–۷: توجه کنید به شغل SQL که با شکست مواجه شد. این یک فرمان پیکربندیشده INSERT است: [sqlalchemy] دو نفر را با استفاده از یک INSERT وارد کرد. در اینجا میتوانیم ببینیم که این سیستم سعی کرده است سفارشهای صادرشده SQL را بهینهسازی کند؛
اکنون با استفاده از phpMyAdmin به محتویات جدول [personnes] نگاه کنیم:

از [6] میتوان دید که جدول خالی است. حتی اولین نفری که اسکریپت به جلسه اضافه کرده بود نیز حضور ندارد. این به این دلیل است که جلسه در داخل یک تراکنش در حال انجام بود و آن تراکنش در بند [except] از اسکریپت [main] برگشت داده شد.
اکنون بیایید تغییر زیر را در [main] اعمال کنیم:
# یک درج
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
پس از افزودن یک شخص در خط ۲، خط ۳ را از حالت توضیحی خارج میکنیم. عملیات [session.commit] تراکنش زیربنایی را اعتبارسنجی کرده و یک تراکنش جدید آغاز خواهد شد. پس از اجرا، محتویات جدول [personnes] به شرح زیر است:

میتوانیم در [6] ببینیم که اولین درج حفظ شده است. این به این دلیل است که این عملیات در تراکنش ۱ انجام شده و خطای بعدی در تراکنش ۲ رخ داده است.
19.3. اسکریپتهای 02: نگاشتها برای [sqlalchemy]

اسکریپتهای 02 نسخهای از اسکریپتهای 01 هستند. هدف ما پیکربندی تا حد امکان در [config.py] است. اکنون محیط [sqlalchemy] برنامه را در آنجا پیکربندی میکنیم:
def configure():
# مسیر مطلق که به مسیرهای نسبی در پیکربندی ارجاع میدهد
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# مسیرهای مطلق وابستگیها
absolute_dependencies = [
# BaseEntity, MyException, Person, Utils
f"{root_dir}/classes/02/entities",
]
# مسیر سیستم را تنظیم کنید
from myutils import set_syspath
set_syspath(absolute_dependencies)
# وارداتها
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper, sessionmaker
# پیوند به یک پایگاه داده MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
# دادههای متا
metadata = MetaData()
# جدول
personnes_table = Table("personnes", metadata,
Column('id', Integer, primary_key=True),
Column('prenom', String(30), nullable=False),
Column("nom", String(30), nullable=False),
Column("age", Integer, nullable=False),
UniqueConstraint('nom', 'prenom', name='uix_1')
)
# نقشهبرداری
from Personne import Personne
mapper(Personne, personnes_table, properties={
'id': personnes_table.c.id,
'نام: personnes_table.c.prenom,
'نام خانوادگی: personnes_table.c.nom,
'سن: personnes_table.c.age
})
#کارخانه جلسه
Session = sessionmaker()
Session.configure(bind=engine)
# ما این اطلاعات را در پیکربندی قرار میدهیم
config = {}
config["Session"] = Session
config["metadata"] = metadata
config["engine"] = engine
config["personnes_table"] = personnes_table
#کلاس پیکربندی
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# ما پیکربندی را بارگذاری میکنیم
return config
توضیحات
- خطوط ۲–۱۲: پیکربندی مسیر پایتون؛
- خطوط 14–45: پیکربندی محیط [sqlalchemy]؛
- خطوط ۴۷–۵۲: محیط [sqlalchemy] به دیکشنری پیکربندی اضافه میشود؛
- خطوط ۵۴–۵۶: پیکربندی کلاس [Personne]؛
با این پیکربندی، اسکریپت [main] به شکل زیر درمیآید:
# ما برنامه را پیکربندی میکنیم
import config
config = config.configure()
#مسیر سیستم پیکربندی شده است – ما واردسازیها را انجام میدهیم
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError
from Personne import Personne
session = None
try:
#یک جلسه
session = config["Session"]()
# حذف جدول [personnes]
session.execute("drop table if exists personnes")
# بازسازی جدول از روی نگاشت
config["metadata"].create_all(config["engine"])
#دو درج
session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
session.add(personne)
#اعتبارسنجی دو درج
session.commit()
#یک پرسوجو
personnes = session.query(Personne).all()
#نمایش
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
# دو درج دیگر، که دومی ناموفق است
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
# یک پرسوجو
personnes = session.query(Personne).all()
#نمایش
print("Liste des personnes-----------")
for personne in personnes:
print(personne)
#اعتبارسنجی جلسه
session.commit()
except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
# نمایش
print(f"L'erreur suivante s'est produite : {erreur}")
#لغو جلسهٔ آخر
if session:
print("rollback...")
session.rollback()
finally:
#نمایش
print("Travail terminé...")
#منابع جلسه آزاد میشوند
if session:
session.close()
نتایج اجرای برنامه به شرح زیر است:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/02/main.py
Liste des personnes-----------
{"âge": 10, "nom": "y", "prénom": "x", "id": 1}
{"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
[SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
[parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Travail terminé...
Process finished with exit code 0
در phpMyAdmin، جدول [personnes] به شکل زیر درآمده است:

اکنون، بیایید به جدول [personnes] که توسط [sqlalchemy] ایجاد شده است، نگاهی بیندازیم:

- در [6]، انواع مورد استفاده برای ستونهای مختلف؛
- در [7]، میتوانیم ببینیم که ستون [id] دارای ویژگی [AUTO_INCREMENT] است. این بدان معناست که هنگام درج یک سطر در جدول، اگر آن سطر برای ستون [id] هیچ مقداری نداشته باشد، این مقدار توسط MySQL به صورت صعودی تولید میشود: 1، 2، 3، … این ویژگی به این معناست که هنگام درج یک سطر در جدول، نیازی به نگرانی در مورد مقدار کلید اصلی نداریم: ما اجازه میدهیم MySQL آن را تولید کند؛
- در [8]، میتوانیم ببینیم که ستون [id] کلید اصلی است؛
- در [9]، محدودیت یکتایی را بر روی فیلدهای [nom, prenom] میبینیم؛
19.4. اسکریپتها 03: دستکاری اشیاء در جلسه [sqlalchemy]

فایل پیکربندی [config] همانند مثال قبلی است. در اسکریپت [main]، عملیات استاندارد [INSERT, UPDATE, DELETE, SELECT] بر روی جدول [personnes] با استفاده از متدهای [sqlalchemy] انجام میشود:
# راهاندازی برنامه
import config
config = config.configure()
# واردات
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne
# محتویات جدول [personnes] را نمایش میدهد
def affiche_table(session: Session):
print("----------------")
# یک پرسوجو
personnes = session.query(Personne).all()
# نمایش
affiche_personnes(personnes)
#فهرستی از افراد را نمایش میدهد
def affiche_personnes(personnes: list):
print("----------------")
#نمایش
for personne in personnes:
print(personne)
#اصلی ---------------------------
session = None
try:
#یک جلسه
session = config["Session"]()
#حذف جدول [personnes]
# checkfirst=True: ابتدا بررسی میکند که جدول وجود دارد
config["personnes_table"].drop(config["engine"], checkfirst=True)
# بازسازی جدول از روی نگاشت
config["metadata"].create_all(config["engine"])
# وارد میکند
session.add(Personne().fromdict({"prénom": "Pierre", "nom": "Nicazou", "âge": 35}))
session.add(Personne().fromdict({"prénom": "Géraldine", "nom": "Colou", "âge": 26}))
session.add(Personne().fromdict({"prénom": "Paulette", "nom": "Girondé", "âge": 56}))
# محتویات جلسه را نمایش میدهد
affiche_table(session)
# فهرست افراد به ترتیب الفبا بر اساس نام خانوادگی؛ در صورتی که نام خانوادگی یکسان باشد، به ترتیب الفبا بر اساس نام
personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())
#نمایش
affiche_personnes(personnes)
# فهرست افرادی که سنشان در بازه [20,40] قرار دارد، به ترتیب نزولی سن
#سپس، برای همسنها، به ترتیب الفبایی نام خانوادگی و در صورت یکسان بودن نام خانوادگی، به ترتیب الفبایی نام کوچک
personnes = session.query(Personne). \
filter(Personne.âge >= 20, Personne.âge <= 40). \
order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())
# نمایش
affiche_personnes(personnes)
# افزودن خانم برونو
bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
session.add(bruneau)
# تغییر در سن او
bruneau.âge = 47
# فهرست افرادی با نام خانوادگی برونو
personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()
# نمایش
affiche_personnes([personne])
# حذف خانم برونو
session.delete(personne)
# فهرست افرادی با نام خانوادگی برونو
personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")
# نمایش
affiche_personnes(personnes)
#اعتبارسنجی جلسه
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
# نمایش
print(f"L'erreur suivante s'est produite : {erreur}")
#لغو آخرین جلسه
if session:
session.rollback()
finally:
# نمایش
print("Travail terminé...")
# رها کردن منابع جلسه
if session:
session.close()
نظرات
- خطوط ۲۰–۲۵: تابع [affiche_personnes] موارد را در یک لیست از افراد نمایش میدهد؛
- خطوط ۱۲–۱۸: تابع [affiche_table] محتویات جدول [personnes] را نمایش میدهد؛
- خطوط ۳۴–۳۶: جدول [personnes] حذف میشود. برخلاف نسخههای قبلی، از دستور SQL استفاده نمیشود؛ در عوض، از متدی در [sqlalchemy] استفاده میشود:
- config["personnes_table"] شیء [Table] است که جدول [personnes] را توصیف میکند؛
- config["engine"] رشته اتصال برای پایگاه داده [dbpersonnes] است؛
- پارامتر با نام [checkfirst=True] مشخص میکند که عملیات تنها در صورتی انجام شود که جدول [personnes] وجود داشته باشد؛
- خطوط ۳۸–۳۹: جدول [personnes] دوباره ایجاد میشود؛
- خطوط ۴۱–۴۴: سه نفر به جلسه اضافه میشوند. توجه داشته باشید که آنها لزوماً بلافاصله در جدول [personnes] درج نمیشوند. این موضوع به استراتژی بهینهسازیشده عملکرد [sqlalchemy] بستگی دارد؛
- خطوط ۴۶–۴۷: محتویات جدول [personnes] نمایش داده میشود. اگر سه نفر هنوز وارد نشده بودند، اکنون در نتیجه این درخواست وارد میشوند؛
- خطوط ۴۹–۵۰: مثالی از استفاده از روش [order_by]، که امکان ارائه نتایج یک پرسوجو در ترتیبی خاص را فراهم میکند. سینتکس [order_by(critère1, critère2)] ابتدا نتایج را بر اساس معیار [critère1] نمایش میدهد، و در مواردی که سطرها مقدار یکسانی برای [critère1] داشته باشند، سپس بر اساس معیار [critère2] مرتب میشوند. چندین معیار را میتوان به صورت زیر مشخص کرد:
- خطوط ۵۵–۵۹: مفهوم فیلترسازی را با استفاده از روش [filter] معرفی میکند. نشانه [filter(critère1, critère2)] یک رابطه منطقی ET (AND) بین معیارهای مورد استفاده برقرار میکند؛
- خطوط ۶۴–۶۷: یک کاربر جدید وارد میشود؛
- خطوط 70–71: یک مثال دیگر از پرسوجوی فیلترشده. تابع [func.lower(param)] مقدار [param] را به حروف کوچک تبدیل میکند. توابع دیگری نیز در دسترس هستند، مانند [func.xx]. در عبارت خط 71:
- [session.query.filter] یک لیست از اشیاء [Personne] را بازمیگرداند؛
- [session.query.filter.first] اولین عنصر این لیست را بازمیگرداند؛
- خط ۷۷: یک عنصر از جلسه حذف میشود؛
- خط ۸۶: جلسه اعتبارسنجی میشود؛
نتایج اجرای کد به شرح زیر است:
- خطوط ۴–۶: محتویات جلسه؛
- خطوط ۸–۱۰: محتویات جلسه به ترتیب نزولی نام؛
- خطوط ۱۲–۱۳: محتوای جلسه برای افرادی که سنشان در بازه [20, 40] قرار دارد؛
- خط ۱۵: فردی به نام «برونو»؛
در phpMyAdmin، محتویات جدول [personnes] در پایان اجرا به شرح زیر است:

19.5. اسکریپتها 04: استفاده از پایگاه داده [PostgreSQL]

پوشه [04] کپی پوشه [03] است. ما فقط یک چیز را تغییر میدهیم: رشته اتصال در فایل [config]:
# پیوند به یک پایگاه داده PostgreSQL
engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")
این رشته اتصال اکنون به پایگاه داده [dbpersonnes] از یک SGBD [PostgreSQL] اشاره میکند. به استفاده از اتصالدهنده [psycopg2] توجه کنید. این باید نصب شده باشد.
اجرای اسکریپت [main] نتایج زیر را تولید میکند:
با استفاده از ابزار [pgAdmin] (به بخش |pgAdmin| مراجعه کنید)، جدول [personnes] در وضعیت زیر قرار دارد:

جدول [personnes] با استفاده از کد زیر تولید شده است: SQL:

- در [4-5]، میتوانیم ببینیم که ستون [id] کلید اصلی است. همچنین میتوان دید که دارای مقدار پیشفرض [mot clé DEFAULT] است، که به این معنی است که اگر یک سطر بدون کلید اصلی وارد شود، یک کلید توسط SGBD ایجاد خواهد شد. این یک رویه رایج است: ما اجازه میدهیم SGBD کلیدهای اصلی را تولید کند؛
این نسخهٔ 05 از اسکریپتهای [sqlalchemy] بهوضوح نشان میدهد که تغییر از یک SGBD به دیگری چقدر آسان است: تنها کاری که لازم بود تغییر رشتهٔ اتصال در یک اسکریپت پیکربندی بود. هیچ چیز دیگری تغییر نکرده است. اگر انواع ستونها در [id, nom, prenom, age] بالا را با آنهایی که در جدول MySQL از مثال |02| هستند مقایسه کنیم، میتوانیم ببینیم که آنها متفاوت هستند. [sqlalchemy] آنها را با SGBD مورد استفاده تطبیق میدهد. این قابلیت تطبیق با SGBD جدید، دلیل کافی برای پذیرش [sqlalchemy] یا ORM دیگر است.
19.6. اسکریپتها 05: مثال کامل

مثال مورد بررسی در اینجا، بازنگری نمونهای است که در بخش |troiscouches-v01| مورد بحث قرار گرفته است. آن مثال شامل یک معماری سهسطحی [ui, métier, dao] بود که به اِنتیتها [Classe, Elève, Matière, Note] رسیدگی میکرد. این انتیتها بهصورت کد سخت در لایه [dao] قرار داشتند. اکنون آنها را در یک پایگاه داده قرار میدهیم. ما از دو SGBD استفاده خواهیم کرد: MySQL و PostgreSQL.
19.6.1. معماری برنامه
معماری برنامه به شرح زیر خواهد بود:

- در [1-3]، لایههای [ui, métier, dao] را مییابیم که از قبل در مثال |troiscouches-v01| موجود هستند. لایه [dao] اکنون با لایه [ORM] ارتباط برقرار میکند؛
- لایههای [1-5] با استفاده از کد پایتون پیادهسازی شدهاند؛
19.6.2. پایگاههای داده
ما در حال ایجاد یک پایگاه داده MySQL به نام [dbecole] هستیم که متعلق به کاربر [admecole] با رمز عبور [mdpecole] است. برای انجام این کار، ما رویهای را که در بخش |ایجاد پایگاه داده| توضیح داده شده است دنبال میکنیم:


- در [1]، پایگاه داده [dbecole] بدون جدولهای [3]؛
- در [7]، کاربر [admecole] دارای امتیازات کامل بر این پایگاه داده است؛
ما همین کار را با SGBD و PostgreSQL انجام میدهیم. ما یک پایگاه داده به نام [dbecole] را که متعلق به کاربر [admecole] است، با رمز عبور [mdpecole] ایجاد میکنیم. برای این کار، رویهای را که در بخش |ایجاد یک پایگاه داده| توضیح داده شده است، دنبال میکنیم:

- در [1]، پایگاه داده [dbecole]؛
- به [2]، کاربر [admecole]؛
- در [3-4]، پایگاه داده [dbecole] متعلق به کاربر [admecole] است؛
19.6.3. اشیاء مورد پردازش توسط برنامه
در برنامه |troiscouches v01|، اشیاء زیر مدیریت میشدند (به |entities| مراجعه کنید). این اشیاء در پایگاههای داده مذکور ذخیره خواهند شد. ما این اشیاء را در برنامه جدید تکرار نخواهیم کرد. ما آنها را از جایی که قبلاً تعریف شدهاند بازیابی خواهیم کرد.
کلاس [Classe]:
# وارداتها
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Classe(BaseEntity):
#ویژگیهای مستثنیشده از وضعیت کلاس
excluded_keys = []
# ویژگیهای کلاس
@staticmethod
def get_allowed_keys() -> list:
# id: شناسهی کلاس
# name: نام کلاس
return BaseEntity.get_allowed_keys() + ["nom"]
# گیرنده
@property
def nom(self: object) -> str:
return self.__nom
# setterها
@nom.setter
def nom(self: object, nom: str):
#نام باید یک رشتهٔ غیرخالی باشد
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")
کلاس [Elève]:
# واردات
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException
from Utils import Utils
class Elève(BaseEntity):
# ویژگیهایی که از وضعیت کلاس حذف شدهاند
excluded_keys = []
# ویژگیهای کلاس
@staticmethod
def get_allowed_keys() -> list:
# id: شناسهٔ شاگرد
# نام خانوادگی: نام خانوادگی شاگرد
# نام: نام شاگرد
# کلاس: کلاس شاگرد
return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
# گیرندهها
@property
def nom(self: object) -> str:
return self.__nom
@property
def prénom(self: object) -> str:
return self.__prénom
@property
def classe(self: object) -> Classe:
return self.__classe
# setورها
@nom.setter
def nom(self: object, nom: str) -> str:
# نام خانوادگی باید یک رشته غیرخالی باشد
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")
@prénom.setter
def prénom(self: object, prénom: str) -> str:
#نام باید یک رشته غیرخالی باشد
if Utils.is_string_ok(prénom):
self.__prénom = prénom
else:
raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")
@classe.setter
def classe(self: object, value):
try:
#انتظار یک نوع Class را دارد
if isinstance(value, Classe):
self.__classe = value
# یا نوع `dict`
elif isinstance(value,dict):
self.__classe=Classe().fromdict(value)
# یا نوع `json`
elif isinstance(value,str):
self.__classe = Classe().fromjson(value)
except BaseException as erreur:
raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")
کلاس [Matière]:
# وارداتها
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
class Matière(BaseEntity):
# ویژگیهایی که از وضعیت کلاس حذف شدهاند
excluded_keys = []
# ویژگیهای کلاس
@staticmethod
def get_allowed_keys() -> list:
# id: شناسهی موضوع
# name: نام موضوع
# وزندهی: وزندهی موضوع
return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
# گیرنده
@property
def nom(self: object) -> str:
return self.__nom
@property
def coefficient(self: object) -> float:
return self.__coefficient
# متدهای تنظیم
@nom.setter
def nom(self: object, nom: str):
# نام باید یک رشته غیرخالی باشد
if Utils.is_string_ok(nom):
self.__nom = nom
else:
raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")
@coefficient.setter
def coefficient(self, coefficient: float):
#ضریب باید یک عدد حقیقی ≥ 0 باشد
erreur = False
if isinstance(coefficient, (int, float)):
if coefficient >= 0:
self.__coefficient = coefficient
else:
erreur = True
else:
erreur = True
# خطا؟
if erreur:
raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
کلاس [Note]:
# وارداتها
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException
class Note(BaseEntity):
# ویژگیهای مستثنیشده از وضعیت کلاس
excluded_keys = []
# ویژگیهای کلاس
@staticmethod
def get_allowed_keys() -> list:
# id: شناسهی نمرهی
# value: خود نمره
# دانشآموز: دانشآموز (از نوع Student) که نمره به او مربوط میشود
# موضوع: موضوع (از نوع Subject) که نمره به آن مربوط میشود
# بنابراین شیء «نمره» نشاندهندهی نمرهٔ یک شاگرد در یک درس است
return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
# گیرندهها
@property
def valeur(self: object) -> float:
return self.__valeur
@property
def élève(self: object) -> Elève:
return self.__élève
@property
def matière(self: object) -> Matière:
return self.__matière
# گیرندهها
@valeur.setter
def valeur(self: object, valeur: float):
# نمرات باید یک عدد اعشاری بین ۰ و ۲۰ باشد
if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
self.__valeur = valeur
else:
raise MyException(31,
f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")
@élève.setter
def élève(self: object, value):
try:
# نوع مورد انتظار 'Student' است
if isinstance(value, Elève):
self.__élève = value
# یا نوع 'dict'
elif isinstance(value, dict):
self.__élève = Elève().fromdict(value)
# یا نوع 'json'
elif isinstance(value, str):
self.__élève = Elève().fromjson(value)
except BaseException as erreur:
raise MyException(32,
f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")
@matière.setter
def matière(self: object, value):
try:
# نوع مورد انتظار 'Subject' است
if isinstance(value, Matière):
self.__matière = value
# یا نوع `dict`
elif isinstance(value, dict):
self.__matière = Matière().fromdict(value)
# یا یک نوع JSON
elif isinstance(value, str):
self.__matière = Matière().fromjson(value)
except BaseException as erreur:
raise MyException(33,
f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")
19.6.4. پیکربندی

پیکربندی در چندین فایل تقسیم شده است:
- پیکربندی عمومی در [config.py]: این فایل مسیر پایتون (Python Path) برنامه را تنظیم میکند و لایههای معماری را نمونهسازی مینماید؛
- پیکربندی [sqlalchemy] در [config_database]: این فایل نگاشتهای کلاس/جدول را انجام میدهد؛
- لایههای کاربردی در [config_layers] پیکربندی شدهاند؛
فایل [config] به شرح زیر است:
def configure(config: dict) -> dict:
import os
# مرحله ۱ ---
# مسیر پایتون برنامه را تنظیم کنید
# مسیر مطلق پوشهای که این اسکریپت را در خود دارد
script_dir = os.path.dirname(os.path.abspath(__file__))
# مسیر مطلق که بهعنوان مرجع برای مسیرهای نسبی در پیکربندی استفاده میشود
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
#مسیرهای مطلق وابستگیها
absolute_dependencies = [
#BaseEntity، MyException
f"{root_dir}/classes/02/entities",
#پروژه سهلایه v01
f"{root_dir}/troiscouches/v01/interfaces",
f"{root_dir}/troiscouches/v01/services",
f"{root_dir}/troiscouches/v01/entities",
#اسناد مربوط به این پروژه
script_dir,
f"{script_dir}/../services",
]
#بهروزرسانی syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# مرحله ۲ ------
#پیکربندی پایگاه داده
import config_database
config = config_database.configure(config)
# مرحله ۳ ------
# مصداقسازی لایههای کاربردی
import config_layers
config = config_layers.configure(config)
#اعمال پیکربندی
return config
- خطوط ۴–۲۷: ساخت مسیر پایتون برنامه؛
- خطوط ۲۹–۳۲: پیکربندی [sqlalchemy];
- خطوط ۳۴–۳۷: پیکربندی لایههای کاربردی؛
فایل [config_database] به شرح زیر است:
def configure(config: dict) -> dict:
#config['sgbd'] نام SGBD است که استفاده میشود
# MySQL: MySQL
# PostgreSQL: PostgreSQL
#پیکربندی SQLAlchemy
from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine
from sqlalchemy.orm import mapper, relationship, sessionmaker
#رشتههای اتصال برای پایگاههای دادهای که در حال استفاده هستند
engines = {
'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
}
#رشته اتصال برای پایگاه داده در حال استفاده
engine = create_engine(engines[config['sgbd']])
# مِتادیتا
metadata = MetaData()
# جدولهای پایگاه داده
tables = {}
#کلاسهای نگاشتشده
from Classe import Classe
from Elève import Elève
from Note import Note
from Matière import Matière
# جدول کلاسها
tables['classes'] = classes_table = \
Table("classes", metadata,
Column('id', Integer, primary_key=True),
Column('nom', String(30), nullable=False),
)
mapper(Classe, tables['classes'], properties={
'شناسه: classes_table.c.id,
'نام: classes_table.c.nom
})
# جدول دانشآموزان
tables['élèves'] = élèves_table = \
Table("élèves", metadata,
Column('id', Integer, primary_key=True),
Column('nom', String(30), nullable=False),
Column('prénom', String(30), nullable=False),
# یک دانشآموز به یک کلاس تعلق دارد
Column('classe_id', Integer, ForeignKey('classes.id')),
)
# نقشهبرداری
mapper(Elève, tables['élèves'], properties={
'شناسه: élèves_table.c.id,
'نام خانوادگی: élèves_table.c.nom,
'first_name': élèves_table.c.prénom,
'class: relationship(Class, backref="pupils", lazy="select")
})
# فهرست مطالب
tables['matières'] = matières_table = \
Table("matières", metadata,
Column('id', Integer, primary_key=True),
Column('nom', String(30), nullable=False),
Column('coefficient', Float, nullable=False)
)
# نقشهبرداری
mapper(Matière, tables['matières'], properties={
'id': matières_table.c.id,
'نام: matières_table.c.nom,
"coefficient": matières_table.c.coefficient
})
#جدول نمرات
tables['notes'] = notes_table = \
Table("notes", metadata,
Column('id', Integer, primary_key=True),
Column('valeur', Float, nullable=False),
#یک نمره نمرهٔ دانشآموز است
Column('élève_id', Integer, ForeignKey('élèves.id')),
# یک نمره برای یک درس است
Column('matière_id', Integer, ForeignKey('matières.id')),
)
#نگاشت
mapper(Note, tables['notes'], properties={
'id': notes_table.c.id,
'value': notes_table.c.valeur,
'student': relationship(Student, backref="notes", lazy="select"),
''موضوع': relationship(Subject, backref="grades", lazy="select")
})
#پیکربندی موجودیت [BaseEntity]
Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
Classe.excluded_keys = ['_sa_instance_state', 'élèves']
Matière.excluded_keys = ['_sa_instance_state', 'notes']
Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']
# کارخانه جلسه
Session = sessionmaker()
Session.configure(bind=engine)
#یک جلسه
session = Session()
# اطلاعات خاصی در فرهنگ لغت پیکربندی ذخیره میشود
config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
#پیکربندی رندر میشود
return config
توضیحات
- خطوط ۱–۴: تابع [configure] یک دیکشنری را بهعنوان پارامتر میپذیرد. تنها کلید [sgbd] استفاده میشود. اگر پایگاه داده یک پایگاه داده MySQL باشد، مقدار آن روی [mysql] و اگر پایگاه داده یک پایگاه داده PostgreSQL باشد، روی [pgres] تنظیم میشود؛
- خطوط ۶–۹: وارد کردن عناصر از [sqlalchemy]. اسکریپت [config_database] جداول پایگاهداده [dbecole] را به انتیتههای [Classes, Elève, Matière, Note] نگاشت میکند. در جدول، دادههای انتیته در یک سطر جایگذاری شدهاند. در کد پایتون، آنها در یک شیء محصور شدهاند. از این رو نام ORM (نقشهبردار شیء-رابطهای) انتخاب شده است: ORM یک نگاشت (یک پیوند) بین سطرهای یک پایگاه داده رابطهای و اشیاء برقرار میکند. در این برنامه، ما چهار موجودیت [Classe, Elève, Matière, Note] داریم که به چهار جدول [classes, élèves, matières, notes] متصل خواهند شد. توجه داشته باشید که نام جدولها ممکن است شامل حروف دارای علامت باشند؛
- خطوط ۱۱–۱۷: رشته اتصال به پایگاه داده مورد استفاده. این بستگی به عنصر config[‘sgbd’] دارد؛
- خطوط ۲۴–۲۸: انتیتهای کاربردی که از طریق [sqlalchemy] نگاشت خواهند شد. هنگامی که این خطوط اجرا میشوند، مسیر پایتون (Python Path) قبلاً توسط اسکریپت [config] تنظیم شده است؛
- خطوط ۳۰–۴۰: نگاشت بین موجودیت [Classe] و جدول [classes]؛
- خطوط ۳۰–۳۵: جدول [classes] با استفاده از کلاس [Table] از [sqlalchemy] تعریف شده است. ما مشخص میکنیم که این جدول دو ستون دارد:
- ستون [id] که کلید اصلی است و شماره کلاس را نشان میدهد، خط ۳۳؛
- ستون [nom] که حاوی نام کلاس است، خط ۳۴؛
- خطوط ۳۱–۳۲: توجه داشته باشید که سینتکس
x=y=zدر پایتون معتبر است: مقدارzبهyو سپس مقدارyبهxاختصاص داده میشود؛ - خطوط ۳۷–۴۰: نگاشتهای بین ستونهای جدول [classes] و ویژگیهای موجوده [Classe] فهرست شدهاند؛
- خطوط ۴۲–۵۷: نگاشت بین انتیت [Elève] و جدول [élèves]؛
- خطوط ۵۱–۵۷: جدول [élèves] با استفاده از کلاس [Table] از [sqlalchemy] تعریف میشود. ما مشخص میکنیم که این جدول چهار ستون دارد:
- ستون [id] که کلید اصلی است و شماره شاگرد را نشان میدهد، سطر ۴۵؛
- ستون [nom]، که حاوی نام خانوادگی شاگرد است، سطر ۴۶؛
- ستون [prénom] که حاوی نام کوچک شاگرد است، سطر ۴۷. توجه داشته باشید که نام یک ستون ممکن است حاوی حروف دارای علامت باشد؛
- ردیف ۴۹، ستون [classe_id]، که حاوی شماره کلاسی خواهد بود که شاگرد به آن تعلق دارد. این به عنوان یک کلید خارجی شناخته میشود. [élèves.classe_id] یک کلید خارجی (ForeignKey) بر روی ستون [classes.id] است. این بدان معناست که مقدار [élèves.classe_id] باید در ستون [classes.id] وجود داشته باشد؛
- خطوط ۵۱–۵۷: نگاشتهای بین ستونهای جدول [élèves] و ویژگیهای انتیت [Elève] فهرست شدهاند:
- خطوط ۵۳–۵۵ ساده و قابلدرک هستند؛
- خط ۵۶ پیچیدهتر است: این خط مقدار ویژگی [Elève.classe] را به عنوان مقداری که توسط رابطه کلید خارجی بین جداول [élèves] و [classes] محاسبه میشود، تعریف میکند. پارامترهای تابع [relationship] به شرح زیر است:
- [Classe]: این نام انتشایی است که انتشایی [Elève] با آن رابطه کلید خارجی دارد. این باید در جدول [élèves] با وجود یک کلید خارجی که به جدول [classes] ارجاع میدهد، منعکس شود. ما میدانیم که این وجود دارد؛
- [backref="élèves"]: نام خصوصیتی است که به انتیت [Classe] اضافه خواهد شد. [Classe.élèves] فهرست تمام دانشآموزان کلاس خواهد بود. این ویژگی نباید از قبل وجود داشته باشد. اگر از قبل وجود دارد، به سادگی برای [backref] نام متفاوتی را در اینجا انتخاب کنید. توسعهدهنده نیازی به مدیریت این ویژگی ندارد. [sqlalchemy] این کار را انجام خواهد داد. آنها فقط باید بدانند که این ویژگی وجود دارد، که توسط [sqlalchemy] اضافه شده است، و اینکه میتوانند از آن در کد خود استفاده کنند؛
- [lazy=’select’]: این بدان معناست که ORM نباید بلافاصله تلاش کند تا مقداری را به ویژگی [Elève.classe] اختصاص دهد. این ویژگی تنها زمانی باید مقدار خود را بازیابی کند که کد صراحتاً آن را درخواست کند. بنابراین:
- اگر کد فهرستی از همه دانشآموزان را درخواست کند، آنها بازگردانده میشوند اما ویژگی [classe] آنها محاسبه نخواهد شد؛
- کمی بعد، کد بر روی یک دانشآموز خاص [e] تمرکز میکند و به کلاس او [e.classe] ارجاع میدهد. این ارجاع سپس [sqlalchemy] را مجبور میکند تا پرسوجوی پایگاه داده را برای بازیابی کلاس شاگرد، به شیوهای که برای توسعهدهنده شفاف است، انجام دهد؛
- گنجاندن [lazy=’select’] نیز با هدف جلوگیری از پرسوجوهای غیرضروری پایگاه داده انجام شده است؛
- خط ۵۶: وقتی ORM یک سطر را از جدول [élèves] بازیابی میکند، اطلاعات را از [id, nom, prénom, classe_id] استخراج میکند. از آنجا باید یک شیء Student (id, surname, first name, class) بسازد. برای ویژگیهای [id, nom, prénom]، این موضوع هیچ دشواریای ایجاد نمیکند. برای ویژگی [classe]، پیچیدهتر است. مقدار آن یک مرجع شیء از نوع [Classe] است. با این حال،
ORMتنها حاوی یک مورد اطلاعاتی است:**[élèves.classe\_id]. از آنجایی که [élèves.classe_id] یک کلید خارجی روی ستون [classes.id] است، ما در اینجا به آن دستور میدهیم که از این رابطه برای بازیابی سطر با شناسه [élèves.classe_id] از جدول [classes] استفاده کند. (که باید وجود داشته باشد) و از روی این سطر، شیء [Classe] مورد نیاز برای ویژگی [Elève.classe]** را ایجاد کند؛ - خطوط ۵۹–۷۱: نگاشت بین انتیت [Matière] و جدول [matières];
- خطوط ۵۹–۶۵: تعریف جدول [sqlalchemy]، با نام [matières];
- ردههای ۶۶–۷۱: تطابق بین ستونهای جدول [matières] و ویژگیهای موجوده [Matière] فهرست شدهاند. در اینجا هیچ دشواریای وجود ندارد؛
- خطوط ۷۳–۹۰: نگاشت بین انتیت [Note] و جدول [notes]؛
- سطور ۷۳–۸۲: تعریف جدول [sqlalchemy]، با نام [notes]. این جدول دو کلید خارجی دارد:
- خط ۷۹: ستون [notes.élève_id] مقادیر خود را از ستون [élèves.id] میگیرد]. این کلید خارجی نشاندهنده این واقعیت است که یک علامت به یک دانشآموز خاص تعلق دارد؛
- ردیف ۸۱: ستون [notes.matière_id] مقادیر خود را از ستون [matières.id] میگیرد. این کلید خارجی نشاندهنده این واقعیت است که یک نمره، نمرهای در یک درس خاص است؛
- ردههای 84–90: نگاشت بین موجودیت [Note] و جدول [notes]:
- ردیف ۸۸: ویژگی [Note.élève] باید دارای مقداری از یک نمونه از نوع [Elève] باشد. در ردیف مربوط به ORM در جدول [notes] تنها ستون [notes.élève_id] وجود دارد که به ستون [élèves.id] ارجاع میدهد. دستورالعمل اینجا این است که از این رابطه کلید خارجی برای بازیابی نمونه [Elève] که نمره آن را داریم، استفاده کنیم. علاوه بر این، [relationship(Elève, backref="notes", …)] ویژگی جدید [Elève.notes] را ایجاد خواهد کرد که شامل لیست نمرات شاگرد است. این ویژگی نباید از قبل در کلاس [Elève] وجود داشته باشد؛
- خط ۸۹: ویژگی [Note.matière] باید دارای مقداری از یک نمونه از نوع [Matière] باشد. ORM در سطر جدول [notes] تنها ستون [notes.matière_id] را دارد که به ستون [matières.id] ارجاع میدهد. دستورالعمل اینجا این است که از این رابطه کلید خارجی برای بازیابی نمونه [Matière] که برای آن نمره داریم استفاده کنیم. علاوه بر این، [relationship(Matière, backref="notes", …)] ویژگی جدید [Matière.notes] را ایجاد خواهد کرد که شامل فهرست نمرات برای دانشآموز خواهد بود. این ویژگی نباید از قبل در کلاس [Matière] وجود داشته باشد؛
- خطوط ۹۲–۹۶: برای هر انتهایی که از [BaseEntity] مشتق شده است، فهرست ویژگیهایی را که باید از فرهنگ لغت ویژگیهای انتها (BaseEntity.asdict) حذف شوند، تعریف میکنیم. ما دیدهایم که [sqlalchemy] ویژگی [_sa_instance_state] را به همهٔ اشیاء نگاشتشده اضافه میکند. ما نمیخواهیم این ویژگی در فرهنگ لغت ویژگیها باشد. علاوه بر این، ما دیدهایم که نگاشتهای قبلی ویژگیهای جدیدی به اشیاء اضافه میکردند:
- [Elève.notes]: تمام نمرات شاگرد؛
- [Classe.élèves]: همه دانشآموزان کلاس؛
- [Matière.notes]: تمام نمرات مربوط به درس؛
بهطور کلی، ما نمیخواهیم این ویژگیها در وضعیت موجوده گنجانده شوند. دلیل آن این است که محاسبه مقدار آنها هزینه SQL در بر دارد و این مقدار اغلب غیرضروری است. بنابراین، اگر دانشآموز با نام «X» را بازیابی کنیم:
- (ادامه)
- ORM یک انتیتای [Elève(id, nom, prénom, classe, notes)] را بازخواهد گرداند. به دلیل [lazy=’select’]، ویژگیهای [classe, notes] که به کلیدهای خارجی در پایگاه داده پیوند دارند، محاسبه نشدهاند؛
- اکنون، اگر من رشته jSON را برای این دانشآموز نمایش دهم، میدانیم که این رشته، jSON از فرهنگ لغت این موجودیت، [asdict] خواهد بود. اگر ویژگیهای [classe] و [notes] موجود باشند، [sqlalchemy] مجبور خواهد شد برای محاسبه مقادیرشان، پرسوجوهایی برای SQL صادر کند. این کار پرهزینه است. اگر بتوان از این پرسوجوها اجتناب کرد، این امر ترجیح دارد؛
- در اینجا، تمام ویژگیهای مرتبط با کلید خارجی را حذف کردهایم؛
- خطوط ۹۸–۱۰۰: نمونهسازی و پیکربندی یک [Session factory] (کارخانه=کارخانه تولید). شیء [Session] برای ایجاد جلسات [sqlalchemy] که توسط تراکنشها پشتیبانی میشوند، استفاده میشود؛
- خطوط ۱۰۲–۱۰۳: ایجاد یک جلسه SQLAlchemy؛
- خط ۱۰۶: برخی از عناصر پیکربندی [sqlalchemy] در دیکشنری پیکربندی جهانی برنامه قرار میگیرند؛
- خط ۱۰۹: این دیکشنری بازگردانده میشود؛
فایل [config_layers] لایههای برنامه را پیکربندی میکند:
def configure(config: dict) -> dict:
# مثالسازی لایه [dao]
from DatabaseDao import DatabaseDao
dao = DatabaseDao(config)
# مثالسازی لایه [métier]
from Métier import Métier
métier = Métier(dao)
# نمونه سازی لایه [ui]
from Console import Console
ui = Console(métier)
#لایهها به پیکربندی اضافه میشوند
config['dao'] = dao
config['métier'] = métier
config['ui'] = ui
#پیکربندی بازیابی میشود
return config
- خط ۱: تابع [configure] دیکشنری پیکربندی جهانی برنامه را دریافت میکند؛
- خطوط ۲–۱۲: لایههای برنامه نمونهسازی میشوند؛
- خطوط ۱۵–۱۷: ارجاعات لایهها به پیکربندی کلی اضافه میشوند؛
- خط ۲۰: پیکربندی جدید بازگردانده میشود؛
19.6.5. لایه [dao] - ۱

مهم است که در اینجا درک کنیم که لایه [dao] [3] بالایه ORM [sqlalchemy] [4] که همانطور که در پاراگراف قبلی توصیف شد پیکربندی شده است. از میان سه لایه [ui, métier, dao] در برنامه |troiscouches v01|، تنها لایه [dao] نیاز به بازنویسی دارد. لایههای [ui, métier] حفظ میشوند.
پیادهسازی لایه [dao] در پوشه [services] قرار داده شده است:

[InterfaceDatabaseDao] رابط لایه [dao] است:
from abc import ABC, abstractmethod
from InterfaceDao import InterfaceDao
class InterfaceDatabaseDao(InterfaceDao, ABC):
# ابتداییسازی پایگاه داده
@abstractmethod
def init_database(self, data: dict):
pass
- خط ۶: رابط [InterfaceDatabaseDao] هم از کلاس [ABC] (بهعنوان یک کلاس انتزاعی) و هم از رابط [InterfaceDao] در پروژه |troiscouches v01| ارث میبرد؛
- خطوط ۸–۱۱: متد [init_database] به متدهای ارثبریشده از [InterfaceDao] اضافه میشود. نقش آن inicialize کردن پایگاه داده با دادههای فرهنگ لغت [data] است که در خط ۱۰ بهعنوان پارامتر به آن ارسال میشود؛
به عنوان یادآوری، رابط [InterfaceDao] به شرح زیر بود:
# وارداتها
from abc import ABC, abstractmethod
#رابط DAO
from Elève import Elève
class InterfaceDao(ABC):
# فهرست کلاسها
@abstractmethod
def get_classes(self: object) -> list:
pass
#فهرست دانشآموزان
@abstractmethod
def get_élèves(self: object) -> list:
pass
# فهرست دروس
@abstractmethod
def get_matières(self: object) -> list:
pass
# فهرست نمرات
@abstractmethod
def get_notes(self: object) -> list:
pass
# فهرست نمرات یک شاگرد
@abstractmethod
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
pass
#جستجوی شاگرد بر اساس شناسهٔ او
@abstractmethod
def get_élève_by_id(self: object, élève_id: int) -> Elève:
pass
پیادهسازی لایه [dao] به شرح زیر است:
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note
class DatabaseDao(InterfaceDatabaseDao):
def __init__(self, config: dict):
# پایگاه داده = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
…
- خط ۱۱: کلاس [DatabaseDao] رابط [InterfaceDatabaseDao] را پیادهسازی میکند؛
- خطوط ۱۳–۱۶: سازنده کلاس. این کلاس دیکشنری پیکربندی برنامه را بهعنوان پارامتر میپذیرد؛
- خط ۱۵: پیکربندی [sqlalchemy] ذخیره میشود؛
- خط ۱۶: جلسه [sqlalchemy]، که از طریق آن پایگاه داده دستکاری خواهد شد، ذخیره میشود؛
- خط ۱۸: متد [init_database] پایگاه داده را با دیکشنری [data] راهاندازی میکند؛
واژهنامه [data] توسط اسکریپت زیر [data.py] پیادهسازی شده است:
def configure():
from Classe import Classe
from Elève import Elève
from Matière import Matière
from Note import Note
# مثالسازی کلاسها
classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
classes = [classe1, classe2]
# درسها
matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
matières = [matière1, matière2]
# دانشآموزان
élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
élèves = [élève11, élève21, élève32, élève42]
#نمرات دانشآموزان در موضوعات مختلف
note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
notes = [note1, note2, note3, note4, note5, note6, note7, note8]
# ما همه چیز را با هم گروهبندی میکنیم
data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
# ما دادهها را بازمیگردانیم
return data
- خط ۳۴: دیکشنریای که به متد [init_database] پاس داده میشود. این دیکشنری شامل کلیدهای زیر است (خط ۳۲):
- [élèves]: فهرست دانشآموزان؛
- [classes]: فهرست کلاسها؛
- [matières]: فهرست دروس؛
- [notes]: فهرست نمرات همه دانشآموزان در همه دروس؛
بیایید به متد [init_database] بازگردیم:
def init_database(self, data: dict):
#پیکربندی پایگاه داده
database = self.database
engine = database['engine']
metadata = database['metadata']
tables = database['tables']
try:
# حذف جداول موجود
#checkfirst=True: ابتدا بررسی میکند که جدول وجود دارد
tables["notes"].drop(engine, checkfirst=True)
tables["matières"].drop(engine, checkfirst=True)
tables["élèves"].drop(engine, checkfirst=True)
tables["classes"].drop(engine, checkfirst=True)
#بازسازی جداول از روی نگاشت
metadata.create_all(engine)
# پر کردن جداول
session = self.session
#کلاسها
classes = data["classes"]
for classe in classes:
session.add(classe)
# موضوعات
matières = data["matières"]
for matière in matières:
session.add(matière)
# دانشآموزان
élèves = data["élèves"]
for élève in élèves:
session.add(élève)
# نمرهها
notes = data["notes"]
for note in notes:
session.add(note)
# تأیید
session.commit()
except (DatabaseError, InterfaceError, IntegrityError) as erreur:
#لغو جلسه
if session:
session.rollback()
# استثناء منتقل میشود
raise MyException(23, f"{erreur}")
- خطوط ۳–۶: اطلاعات از پیکربندی پایگاه داده بازیابی میشود؛
- خطوط ۹–۱۴: دیدیم که پیکربندی [sqlalchemy] چهار موجودیت را به چهار جدول [élèves, matières, classes, notes] نگاشت کرده بود. ابتدا این جدولها را در صورت وجود حذف میکنیم؛
- خطوط ۱۶–۱۷: ما چهار جدولی را که همینالان حذف کردیم، دوباره ایجاد میکنیم؛
- خطوط ۲۲–۲۵: ما همه کلاسها را به جلسه اضافه میکنیم؛
- خطوط ۲۷–۳۰: همه موضوعات به جلسه اضافه میشوند؛
- خطوط ۳۲–۳۵: اضافه کردن همه دانشآموزان به جلسه؛
- خطوط ۳۷–۴۰: ما همه نمرات را به جلسه اضافه میکنیم؛
- برای انجام این افزودنها، ما از یک ترتیب مشخص پیروی کردیم. ما با موجودیتهایی شروع کردیم که با موجودیتهای دیگر رابطهای نداشتند و با آنهایی که رابطه داشتند، پایان دادیم. بنابراین، هنگامی که دانشآموزان را به جلسه اضافه میکنیم، کلاسهایی که به آنها تعلق دارند، از قبل در جلسه وجود دارند؛
- خط ۴۳: جلسه [sqlalchemy] اعتبارسنجی شد. پس از این عملیات، میتوانیم مطمئن باشیم که تمام دادههای جلسه با پایگاه داده همگامسازی شده است. به عبارت دیگر، در جداول بارگذاری شده است. این امر با نگاشتهای تعریفشده در پیکربندی [sqlalchemy] امکانپذیر شد. [sqlalchemy] میداند که هر انتهایت چگونه باید در جداول ذخیره شود. [sqlalchemy] همچنین هر کلید خارجی که جداول ممکن است داشته باشند را تولید کرده است؛
- خطوط ۴۴–۴۹: اگر مشکلی پیش آید، جلسه [sqlalchemy] لغو میشود و در خط ۴۹، یک استثنا پرتاب میشود؛
19.6.6. ابتداییسازی پایگاه داده

اسکریپت [main_init_database] پایگاه داده را با محتویات اسکریپت [data.py] راهاندازی میکند. کد آن به شرح زیر است:
#انتظار یک پارامتر MySQL یا PostgreSQL
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# پیکربندی برنامه
import config
config = config.configure({'sgbd': sgbd})
#مسیر سیستم پیکربندی شده است – اکنون میتوان عملیات وارداتی را انجام داد
from MyException import MyException
#بازیابی دادههایی که باید در پایگاه داده بارگذاری شوند
import data
data = data.configure()
#بازیابی لایه [dao]
dao = config["dao"]
#------------ اصلی
try:
# ایجاد و инициализация جداول پایگاه داده
dao.init_database(data)
except MyException as ex:
#خطا نمایش داده میشود
print(f"L'erreur suivante s'est produite : {ex}")
finally:
#آزادسازی منابع مورد استفاده توسط برنامه
import shutdown
shutdown.execute(config)
#پایان
print("Travail terminé...")
- خطوط ۱–۱۱: این اسکریپت منتظر یک پارامتر [mysql] یا [pgres] است، بسته به اینکه آیا میخواهید یک پایگاه داده به نام MySQL یا PostgreSQL را اولیه کنید؛
- خطوط ۱۳–۱۵: برنامه برای پارامتر SGBD پیکربندی شده است؛
- خطوط ۲۰–۲۲: دادههایی که باید در پایگاه داده بارگذاری شوند، بازیابی میشوند؛
- خط ۲۵: لایه [dao] قبلاً نمونه سازی شده و در پیکربندی برنامه قابل دسترسی است؛
- خط ۳۰: پایگاه داده اولیه میشود؛
- خطوط ۳۴–۳۷: صرفنظر از اینکه خطایی رخ دهد یا خیر، منابع برنامه با استفاده از ماژول [shutdown] آزاد میشوند؛
ماژول [shutdown.py] به شرح زیر است:
def execute(config: dict):
# آزادسازی منابع مورد استفاده توسط برنامه
sqlalchemy_session = config['database']['session']
if sqlalchemy_session:
sqlalchemy_session.close()
تابع [shutdown.execute] جلسه [sqlalchemy] را که برای راهاندازی پایگاه داده استفاده شده بود، میبندد.
ما یک پیکربندی اجرای اولیه (به |پیکربندی اجرا| مراجعه کنید) ایجاد میکنیم تا [main_init_database] را با SGBD و MySQL اجرا کنیم:

نتایج اجرای این پیکربندی در phpMyAdmin به شرح زیر است:



برای SGBD و [PostgreSQL]، از پیکربندی اجرای زیر استفاده میکنیم:

پس از اجرا، نتایج در [pgAdmin] به شرح زیر است:



توجه کنید که چقدر به راحتی توانستیم به SGBD تغییر دهیم.
19.6.7. لایه [dao] – ۲
ما به کلاس [DatabaseDao] بازمیگردیم که لایه [dao] را پیادهسازی میکند. تا اینجا تنها پیادهسازی متد [init_database] را نشان دادهایم. اکنون پیادهسازی سایر متدها را نشان میدهیم:
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note
class DatabaseDao(InterfaceDatabaseDao):
def __init__(self, config: dict):
# database = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
self.database = config['database']
self.session = self.database['session']
def init_database(self, data: dict):
…
# فهرست تمام کلاسها
def get_classes(self: object) -> list:
# پرسوجو
return self.session.query(Classe).all()
# فهرست همه دانشآموزان
def get_élèves(self: object) -> list:
# پرسوجو
return self.session.query(Elève).all()
# فهرست همه موضوعات
def get_matières(self: object) -> list:
# پرسوجو
return self.session.query(Matière).all()
#فهرست نمرات همهٔ دانشآموزان
def get_notes(self: object) -> list:
# پرسوجو
return self.session.query(Note).all()
# فهرست نمرات یک دانشآموز خاص
def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
#جستجو برای دانشآموز – در صورت عدم وجود، یک استثنا پرتاب میشود
# اجازه میدهد درخواست ادامه یابد
élève = self.get_élève_by_id(élève_id)
# نمرههای او را بازیابی کن (بارگذاری تنبل)
notes = élève.notes
# یک دیکشنری بازمیگرداند
return {"élève": élève, "notes": notes}
# دانشآموزی که با شمارهاش شناسایی شده
def get_élève_by_id(self, élève_id: int) -> Elève:
#جستجوی شاگرد
élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
#آیا آنها را پیدا کردهایم؟
if élèves:
return élèves[0]
else:
raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")
# دانشآموزی که با نامش شناسایی میشود
def get_élève_by_name(self, élève_name: str) -> Elève:
# در جستجوی شاگرد
élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
#آیا آنها را پیدا کردهایم؟
if élèves:
return élèves[0]
else:
raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")
# کلاسی که با شمارهاش شناسایی شده است
def get_classe_by_id(self, classe_id: int) -> Classe:
#در جستجوی کلاس
classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
#آیا آن را پیدا کردهایم؟
if classes:
return classes[0]
else:
raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")
#یک کلاس شناساییشده با نامش
def get_classe_by_name(self, classe_name: str) -> Classe:
# ما در جستجوی کلاس هستیم
classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
#آیا آن را پیدا کردهایم؟
if classes:
return classes[0]
else:
raise MyException(14, f"La classe de nom {classe_name} n'existe pas")
#یک موضوع شناساییشده با شمارهٔ آن
def get_matière_by_id(self, matière_id: int) -> Matière:
#در جستجوی موضوع
matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
#آیا آن را پیدا کردهایم؟
if matières:
return matières[0]
else:
raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")
# یک ماده شناساییشده با نامش
def get_matière_by_name(self, matière_name: str) -> Matière:
# ما در جستجوی آن ماده هستیم
matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
#آیا آن را پیدا کردهایم؟
if matières:
return matières[0]
else:
raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
- خطوط 21–24: متد [get_classes] باید لیست کلاسهای مدرسه را بازگرداند. در خط 20، از یک پرسوجو که قبلاً با آن مواجه شدهایم استفاده میکنیم؛
- خطوط ۲۶–۳۹: سه متد مشابه دیگر برای بازیابی فهرستهای دانشآموزان، دروس و نمرات؛
- خطوط ۵۱–۵۹: متد [get_élève_by_id] باید یک دانشآموز را با شمارهٔ او شناسایی کند. اگر دانشآموز وجود نداشته باشد، یک استثنا (exception) ایجاد میکند؛
- خط ۵۴: از یک پرسوجوی فیلترشده استفاده میکنیم. ما یا یک لیست خالی یا لیستی شامل یک عنصر دریافت میکنیم؛
- خط ۵۷: اگر لیست بازیابیشده خالی نباشد، عنصر اول لیست بازگردانده میشود؛
- در غیر این صورت، در خط ۵۹، یک استثنا پرتاب میشود؛
- خطوط ۴۱–۴۹: متد [get_notes_for_élève_by_id] باید نمرات یک دانشآموز را که با شمارهٔ او شناسایی شده است، بازگرداند:
- خط ۴۵: متد [get_élève_by_id] برای بازیابی انتیت «Student» دانشآموز استفاده میشود؛
- خط ۴۷: از خاصیت [Elève.notes] استفاده میشود که توسط نگاشت بین انتیت [Note] و جدول [notes] (به بخش | پیکربندی SQLAlchemy| مراجعه کنید) ایجاد شده است، که نمرات دانشآموز را نشان میدهد؛
- خط ۴۹: یک دیکشنری بازگردانده میشود؛
- خطوط ۶۱–۱۰۹: مجموعهای از روشهای مشابه که به ما امکان میدهند:
- پیدا کردن یک شاگرد بر اساس نام، خطوط ۶۱–۶۹؛
- جستجوی کلاس، خطوط ۷۱–۸۹؛
- پیدا کردن یک درس، خطوط ۹۱–۱۰۹؛
19.6.8. اسکریپت [main_joined_queries]

اسکریپت [main_joined_queries] به این نام نامگذاری شده است زیرا هدف آن برجسته کردن پرسوجوهایی است که به طور ضمنی توسط [sqlalchemy] برای بازیابی اطلاعات از چندین جدول انجام میشود. این پرسوجوها که از دید برنامهنویس پنهان هستند، هرگاه یک ویژگی از یک موجودیت در نگاشت آن با تابع [relationship] مرتبط شود، اجرا میشوند. برای مثال:
#نقشهبرداری
mapper(Note, tables['notes'], properties={
'شناسه': notes_table.c.id,
'مقدار: notes_table.c.valeur,
'student': relationship(Student, backref="notes", lazy="select"),
'subject': relationship(Subject, backref="grades", lazy="select")
})
در بالا، نگاشت بین موجودیت [Note] و جدول [notes]:
- خط ۵: هنگامی که ویژگی [élève] یک موجودیت [Note] برای اولین بار درخواست میشود، آن از جدول [élèves] از طریق یک پرسوجو SQL بازیابی خواهد شد. تا زمانی که این ویژگی درخواست نشود، نامشخص باقی میماند (بارگذاری تنبل). پس از بازیابی، مقدار آن در حافظه ORM باقی میماند. هنگامی که برای بار دوم به آن ارجاع داده شود، ORM فوراً مقدار آن را بدون ارسال یک پرسوجوی جدید SQL بازمیگرداند. تمام این موارد برای توسعهدهنده شفاف است؛
- همین امر در مورد ویژگی معکوس [Elève.notes] (backref)، خط ۵؛ صادق است؛
- همین امر در مورد ویژگی [Note.matière] و ویژگی معکوس آن [Matière.notes] (backref)، خط ۶ نیز صدق میکند؛
اسکریپت [main_joined_queries] به شرح زیر است:
#انتظار میرود یک پارامتر MySQL یا PostgreSQL
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
#پیکربندی برنامه
import config
config = config.configure({"sgbd": sgbd})
#مسیر سیستم پیکربندی شده است – اکنون میتوان واردات را انجام داد
from MyException import MyException
#لایه [dao]
dao = config["dao"]
try:
# دانشآموز بر اساس شناسه
print("élève id=11 -----------")
élève = dao.get_élève_by_id(11)
print(f"élève={élève}")
#کلاس شاگرد (بارگذاری تنبل)
classe = élève.classe
print(f"classe de l'élève : {classe}")
# دانشآموزان در یک کلاس (بارگذاری تنبل)
print("élèves dans la même classe :")
for élève in classe.élèves:
print(f"élève={élève}")
# یک شاگرد بر اساس نام
print("élève nom='nom2' -----------")
print(f"élève={dao.get_élève_by_name('nom2')}")
# کلاس آنها (بارگذاری تنبل)
print(f"classe de l'élève : {élève.classe}")
#نمرات یک شاگرد
print("notes de l'élève id=11 -----------")
# ابتدا شاگرد
élève = dao.get_élève_by_id(11)
# سپس نمرات آنها (بارگذاری تنبل)
for note in élève.notes:
# نمرتا
print(f"note={note}, "
# موضوع نمره (بارگذاری تنبل)
f"matière={note.matière}")
# دانشآموزان در یک کلاس
print("élèves de la classe nom='classe1' -----------")
#ابتدا کلاس
classe = dao.get_classe_by_name('classe1')
#سپس دانشآموزان (بارگذاری تنبل)
for élève in classe.élèves:
print(élève)
# همین موضوع در مورد [classe2] نیز صدق میکند
print("élèves de la classe de nom 'classe2' -----------")
classe = dao.get_classe_by_name('classe2')
for élève in classe.élèves:
print(élève)
# نمرهها در یک درس
print("matière de nom='matière1' -----------")
# ابتدا موضوع
matière = dao.get_matière_by_name('matière1')
print(f"matière={matière}")
#سپس نمرات در آن درس (بارگذاری تنبل)
print("Notes dans la matière : ")
for note in matière.notes:
print(note)
# همین موضوع برای subject2 نیز صدق میکند
print("matière de nom='matière2' -----------")
matière = dao.get_matière_by_name('matière2')
print(f"matière={matière}")
print("Notes dans la matière : ")
for note in matière.notes:
print(f"note={note}")
except MyException as ex1:
#خطا را نمایش میدهد
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
# خطا را نمایش میدهد
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
#آزادسازی منابع
import shutdown
shutdown.execute(config)
توضیحات برای درک کد کافی هستند.
ما یک پیکربندی اجرا برای MySQL ایجاد میکنیم:

نتایج اجرای برنامه به شرح زیر است:
برای درک این نتایج، در نظر داشته باشید که برخی ویژگیها از فرهنگ لغت انتیت حذف شدهاند (به |configuration| مراجعه کنید):
#پیکربندی واحد [BaseEntity]
Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
Classe.excluded_keys = ['_sa_instance_state', 'élèves']
Matière.excluded_keys = ['_sa_instance_state', 'notes']
Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']
بنابراین، هنگامی که [print(f"élève={élève}")] را در خط ۲۶ کد وارد میکنیم، خط ۱ بالا به ما میگوید که ویژگیهای ['_sa_instance_state', 'notes', 'classe'] نمایش داده نخواهند شد. این همان چیزی است که در خط ۳ نتایج میبینیم. تمام ویژگیهای دیگر نمایش داده میشوند. بنابراین، همچنان در خط ۳، ما یک ویژگی جدید به نام [classe_id] را کشف میکنیم که در ابتدا در موجودیت [Elève] وجود نداشت. این ویژگی مستقیماً با ستون [classe_id] در جدول [élèves] مطابقت دارد. بنابراین، [sqlalchemy] ویژگیهای زیر را به انتیت [Elève] اضافه کرده است: [classe_id, _sa_instance_state, notes]. آگاهی از این موضوع مهم است، بهویژه اینکه این ویژگیها نباید از قبل در انتیت نگاشتشده وجود داشته باشند.
ویژگیهایی که از فرهنگ لغت انتیت (entity dictionary) مستثنی شدهاند، مهم هستند. اگر، برای مثال، ویژگیهای [notes, élève] از انتیت [Elève] مستثنی نشوند، آنگاه عملیات [print(f"élève={élève}")] آنها را نمایش خواهد داد و بنابراین، همانطور که توضیح داده شد، این امر باعث راهاندازی پرسوجوهای ضمنی SQL (بارگذاری تنبل) برای بازیابی مقادیر این ویژگیها میشود. اگر، مانند این مورد، فهرستی از دانشآموزان نمایش داده میشود، عملیات ضمنی SQL برای هر دانشآموز انجام میشود. این امر از یک سو ممکن است غیرضروری و از سوی دیگر قطعاً از نظر زمان اجرای برنامه پرهزینه باشد.
برای اجرای اسکریپت با استفاده از پایگاه داده PostgreSQL، پیکربندی اجرای زیر را ایجاد کنید:

اجرا نتایج مشابهی با MySQL دارد.
19.6.9. اسکریپت [main_stats_for_élève]

اسکریپت [main_stats_for_élève] همان اسکریپتی است که در برنامه |troiscouches v01] استفاده شده است. سپس با نام [main] نامگذاری شد. این یک برنامه کنسول است که معیارهای خاصی را که به نمرات یک دانشآموز مربوط میشود بازیابی میکند: [moyenne pondérée, min, max, liste]. این برنامه در معماری زیر قرار میگیرد:

در این معماری لایهای، تنها لایه [dao] بین اپلیکیشن |troiscouches v01| و این یکی تغییر کرده است. از آنجا که لایه جدید [dao] مطابق با رابط [InterfaceDao] لایه قدیمی [dao] است، لایههای [ui, métier] نیازی به تغییر ندارند. بنابراین میتوانیم به استفاده از لایههایی که در برنامه |troiscouches v01| تعریف شدهاند ادامه دهیم.
اسکریپت [main_stats_for_élève] لایه [main] را از نمودار بالا به شرح زیر پیادهسازی میکند:
# منتظر یک پارامتر MySQL یا PostgreSQL
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# پیکربندی برنامه
import config
config = config.configure({'sgbd': sgbd})
#مسیر سیستم پیکربندی شده است – اکنون میتوان واردات را انجام داد
from MyException import MyException
#لایه [ui]
ui = config["ui"]
try:
# اجرای لایه [ui]
ui.run()
except MyException as ex1:
#خطا نمایش داده میشود
print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
#خطا نمایش داده میشود
print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
#منابع آزاد میشوند
import shutdown
shutdown.execute(config)
- خط ۲۰: یک ارجاع به لایه [ui] از پیکربندی برنامه بازیابی میشود؛
- خط 24: دیالوگ کاربر با استفاده از تنها متد لایه [ui] آغاز میشود؛
یک پیکربندی زمان اجرا برای PostgreSQL به شرح زیر خواهد بود:

در اینجا مثالی از اجرای این پیکربندی آورده شده است:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_stats_for_élève.py pgres
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"prénom": "prénom1", "id": 11, "classe_id": 1, "nom": "nom1"}, notes=[10.0 6.0], max=10.0, min=6.0, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[11, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *
Process finished with exit code 0