برنامه‌نویسی پایتون فهرست مطالب
فصل ۳۲

ORM و SQLAlchemy

در دو فصل گذشته SQL نوشتیم و بعد همان SQL را از داخل پایتون اجرا کردیم. حالا یک قدم بالاتر می‌رویم: دیگر SQL نمی‌نویسیم. با شیء کار می‌کنیم و کتابخانه، SQL را به جای ما می‌سازد. برای اینکه این جادو راز نماند، اول یک ORM کوچک را خودمان می‌سازیم و بعد سراغ SQLAlchemy می‌رویم.

در این فصل می‌خوانید
  1. ORM یعنی چه
  2. یک کار، دو نوشتن
  3. کلاس پایه: قلب ORM دست‌ساز
  4. خواندن و نوشتن در کلاس پایه
  5. اولین مدل: Student
  6. مدل دوم تقریباً رایگان است
  7. برنامه‌ای بدون یک خط SQL
  8. SQLAlchemy: نصب و موتور
  9. تعریف مدل‌ها
  10. نشست: درج داده
  11. خواندن، تغییر و حذف
  12. رابطه‌ها و حذف آبشاری
  13. تراکنش و IntegrityError
  14. کی ORM، کی SQL خام

۱. ORM یعنی چه

ORM کوتاه‌شده Object Relational Mapper است: نگاشت‌دهنده شیء به رابطه. کار آن یک ترجمه ساده است و کل ایده در سه سطر جا می‌شود.

در پایگاه دادهدر پایتون
جدولکلاس
سطرشیء (نمونه کلاس)
ستونصفت شیء
کلید خارجیارجاع مستقیم به شیء دیگر
INSERTساختن شیء و ذخیره کردنش
SELECTصدا زدن یک متد جست‌وجو
UPDATEتغییر دادن یک صفت
DELETEصدا زدن delete()

یعنی همه چیزهایی که در فصل بیست و یکم تا بیست و چهارم درباره شیءگرایی یاد گرفتید، حالا به کارتان می‌آید. یک ORM چیزی جز یک لایه شیءگرای هوشمند روی sqlite3 نیست.

۲. یک کار، دو نوشتن

بهترین راه دیدن فایده ORM این است که یک کار کاملاً یکسان را دو بار بنویسیم. نمونه 01_why_orm/app.py دقیقاً همین کار را می‌کند: یک دانشجو را درج می‌کند و بعد پیدایش می‌کند.

def with_raw_sql() -> None:
    '''The way of session 31.

    Problems:
      * the SQL is spread all over the program;
      * a row is a tuple, so we read it with row[2] and nobody knows what
        column 2 is;
      * every table needs its own copy-pasted insert/select/update/delete;
      * changing a column name means hunting through the whole project.
    '''
    conn = sqlite3.connect(DB)
    cursor = conn.cursor()
    cursor.execute('''CREATE TABLE IF NOT EXISTS students (
                        student_id INTEGER PRIMARY KEY AUTOINCREMENT,
                        first_name TEXT NOT NULL,
                        last_name  TEXT NOT NULL,
                        email      TEXT UNIQUE NOT NULL)''')

    cursor.execute(
        'INSERT OR IGNORE INTO students (first_name, last_name, email) '
        'VALUES (?, ?, ?)',
        ('Ali', 'Rezaei', 'ali@kadoosedu.ir'),
    )
    conn.commit()

    cursor.execute('SELECT * FROM students WHERE email = ?',
                   ('ali@kadoosedu.ir',))
    row = cursor.fetchone()
    print('raw SQL  ->', row, '| the name is', row[1])   # row[1]... which one?

    conn.close()

روش فصل قبل: نمونه 01_why_orm/app.py

def with_an_orm() -> None:
    '''The same thing, with the tiny ORM of folder 02.

    The SQL lives in ONE place (the model), and the rest of the program only
    ever talks about objects and attributes.
    '''
    from models import Student

    Student.create_table()

    student = Student.get_or_create(
        email='ali@kadoosedu.ir',
        first_name='Ali',
        last_name='Rezaei',
    )
    print('ORM      ->', student, '| the name is', student.first_name)

همان کار با ORM

شرح مثال

ORM جادو نیست

هر ORM در نهایت همان INSERT و SELECT را می‌سازد و به پایگاه داده می‌فرستد. اگر SQL بلد نباشید، ORM را هم نمی‌توانید درست به کار ببرید و وقتی کند شود نمی‌فهمید چرا. به همین دلیل فصل سی‌ام قبل از این فصل آمده است.

۳. کلاس پایه: قلب ORM دست‌ساز

حالا خودمان یک ORM کوچک می‌سازیم. فکر اصلی این است: هر مدل فقط باید بگوید نامش چیست و چه ستون‌هایی دارد؛ بقیه کارها را یک کلاس پایه انجام می‌دهد که همه از آن ارث می‌برند.

'''The shared part of every model.

This is the heart of the tiny ORM: the connection handling and the generic
CRUD helpers live here ONCE, and every model inherits them.
'''

import sqlite3

DATABASE_PATH = 'database.db'


class Model:
    '''Base class of every model.

    A subclass only has to define:
        table       the name of the table
        pk          the name of its primary key column
        columns     the other columns, in order
        SCHEMA      the CREATE TABLE statement
    '''

    table: str = ''
    pk: str = 'id'
    columns: list[str] = []
    SCHEMA: str = ''

    @classmethod
    def connect(cls) -> sqlite3.Connection:
        'Return a configured connection. Everything goes through here.'
        conn = sqlite3.connect(DATABASE_PATH)
        conn.execute('PRAGMA foreign_keys = ON')
        conn.row_factory = sqlite3.Row      # rows readable by column name
        return conn

    @classmethod
    def create_table(cls) -> None:
        'Create the table of this model.'
        with cls.connect() as conn:
            conn.executescript(cls.SCHEMA)

کلاس پایه: نمونه 02_simple_orm/models/base.py

شرح مثال

۴. خواندن و نوشتن در کلاس پایه

حالا چهار عمل اصلی را یک بار برای همیشه در همین کلاس پایه می‌نویسیم. اول خواندن:

    @classmethod
    def all(cls) -> list['Model']:
        'Return every row as a list of objects.'
        conn = cls.connect()
        try:
            rows = conn.execute(
                f'SELECT * FROM {cls.table} ORDER BY {cls.pk}'
            ).fetchall()
        finally:
            conn.close()
        return [cls.from_row(row) for row in rows]

    @classmethod
    def get(cls, **where) -> 'Model | None':
        '''Return the first row matching the given columns, or None.

        Example:  Student.get(email='ali@kadoosedu.ir')
        '''
        # The column NAMES come from our own code, the VALUES from the caller,
        # so only the values become '?' parameters.
        conditions = ' AND '.join(f'{column} = ?' for column in where)
        values = tuple(where.values())

        conn = cls.connect()
        try:
            row = conn.execute(
                f'SELECT * FROM {cls.table} WHERE {conditions}', values
            ).fetchone()
        finally:
            conn.close()

        return cls.from_row(row) if row else None

    @classmethod
    def from_row(cls, row: sqlite3.Row) -> 'Model':
        'Build an object out of a database row.'
        obj = cls.__new__(cls)                 # create it without __init__
        for key in row.keys():
            setattr(obj, key, row[key])
        return obj

خواندن: ادامه base.py

شرح مثال

و حالا نوشتن:

    def save(self) -> None:
        'Insert this object, and store the id it received.'
        columns = ', '.join(self.columns)
        placeholders = ', '.join('?' for _ in self.columns)
        values = tuple(getattr(self, column) for column in self.columns)

        with self.connect() as conn:
            cursor = conn.execute(
                f'INSERT INTO {self.table} ({columns}) VALUES ({placeholders})',
                values,
            )
            setattr(self, self.pk, cursor.lastrowid)

    def update(self, **changes) -> None:
        '''Change some columns of this object AND of its row.'''
        for column in changes:
            if column not in self.columns:
                raise ValueError(f'unknown column: {column}')

        assignments = ', '.join(f'{column} = ?' for column in changes)
        values = tuple(changes.values()) + (getattr(self, self.pk),)

        with self.connect() as conn:
            conn.execute(
                f'UPDATE {self.table} SET {assignments} WHERE {self.pk} = ?',
                values,
            )

        # Keep the object and the row in agreement.
        for column, value in changes.items():
            setattr(self, column, value)

    def delete(self) -> None:
        'Delete the row of this object.'
        with self.connect() as conn:
            conn.execute(
                f'DELETE FROM {self.table} WHERE {self.pk} = ?',
                (getattr(self, self.pk),),
            )

نوشتن: ادامه base.py

شرح مثال

ساختن شیء با ذخیره کردنش فرق دارد

Student(...) فقط یک شیء در حافظه می‌سازد و هیچ چیزی در پایگاه داده نمی‌نویسد. تا وقتی save() را صدا نزنید، چیزی ذخیره نشده است. همین جدایی را در SQLAlchemy هم می‌بینید، آنجا با نام add() و commit().

۵. اولین مدل: Student

حالا ببینید یک مدل واقعی چقدر کم کد دارد. هر چه تا اینجا نوشتیم، اینجا رایگان به ارث می‌رسد.

'''The Student model: CRUD operations on the students table.'''

from .base import Model


class Student(Model):
    'One row of the students table.'

    table = 'students'
    pk = 'student_id'
    columns = ['first_name', 'last_name', 'email']

    SCHEMA = '''
        CREATE TABLE IF NOT EXISTS students (
            student_id INTEGER PRIMARY KEY AUTOINCREMENT,
            first_name TEXT NOT NULL,
            last_name  TEXT NOT NULL,
            email      TEXT UNIQUE NOT NULL
        );
    '''

    def __init__(self,
                 email: str,
                 first_name: str = 'New',
                 last_name: str = 'Student') -> None:
        # Creating the OBJECT does not touch the database; save() does.
        self.student_id: int | None = None
        self.email = email
        self.first_name = first_name
        self.last_name = last_name

    def __str__(self) -> str:
        return f'{self.first_name} {self.last_name} <{self.email}>'

    def __repr__(self) -> str:
        return f'Student({self.email!r})'

    @property
    def full_name(self) -> str:
        'The two names together.'
        return f'{self.first_name} {self.last_name}'

مدل دانشجو: نمونه 02_simple_orm/models/students.py

شرح مثال

۶. مدل دوم تقریباً رایگان است

'''The Teacher model.

Notice how little code a new model needs: everything else is inherited.
'''

from .base import Model


class Teacher(Model):
    'One row of the teachers table.'

    table = 'teachers'
    pk = 'teacher_id'
    columns = ['first_name', 'last_name', 'department']

    SCHEMA = '''
        CREATE TABLE IF NOT EXISTS teachers (
            teacher_id INTEGER PRIMARY KEY AUTOINCREMENT,
            first_name TEXT NOT NULL,
            last_name  TEXT NOT NULL,
            department TEXT DEFAULT 'General'
        );
    '''

    def __init__(self,
                 first_name: str,
                 last_name: str,
                 department: str = 'General') -> None:
        self.teacher_id: int | None = None
        self.first_name = first_name
        self.last_name = last_name
        self.department = department

    def __str__(self) -> str:
        return f'{self.first_name} {self.last_name} ({self.department})'

مدل استاد: نمونه 02_simple_orm/models/teachers.py

این مدل حدود سی خط است و با همان سی خط، متدهای all و get و save و update و delete و get_or_create را دارد. اگر بدون ORM می‌نوشتیم، باید همه آنها را برای جدول دوم دوباره کپی می‌کردیم. این پاسخ تمرین چهارم این فصل است.

'''The models package: one module per table, re-exported here.'''

from .base import Model
from .students import Student
from .teachers import Teacher

__all__ = ['Model', 'Student', 'Teacher']

بسته مدل‌ها: models/__init__.py

ساختار بسته دقیقاً همان چیزی است که در فصل هفدهم یاد گرفتید: هر جدول یک ماژول، و __init__.py همه را در یک جا در معرض دید می‌گذارد تا بقیه برنامه فقط بنویسد from models import Student.

۷. برنامه‌ای بدون یک خط SQL

'''A menu-driven program using our own tiny ORM.

Not one line of SQL in this file: everything goes through the models.
'''

from models import Student, Teacher


def students_section() -> None:
    'Everything about the students.'
    while True:
        clear()
        student_menu()
        choice = input('Enter the number: ').strip()

        if choice == '1':
            students = Student.all()
            if not students:
                print('(no student yet)')
            for student in students:
                print(f'{student.student_id:<5}{student}')
            pause()

        elif choice == '3':
            email = input('Enter the email: ').strip()
            if Student.get(email=email) is not None:
                print('That email is already used.')
                pause()
                continue

            student = Student(email=email,
                              first_name=input('First name: ').strip() or 'New',
                              last_name=input('Last name: ').strip() or 'Student')
            student.save()
            print(f'Student added with the id {student.student_id}.')
            pause()

        elif choice == '4':
            student = Student.get(email=input('Enter the email: ').strip())
            if student is None:
                print('No student with that email.')
            else:
                student.update(first_name=input('New first name: ').strip())
                print('Updated:', student)
            pause()

        elif choice == '9':
            return

برنامه منویی: نمونه 02_simple_orm/app.py

در کل این فایل حتی یک کلمه SELECT یا INSERT پیدا نمی‌کنید. این همان هدف ORM است: لایه رابط کاربر درباره پایگاه داده چیزی نمی‌داند. اگر فردا تصمیم بگیرید داده‌ها را در PostgreSQL نگه دارید، فقط کلاس پایه عوض می‌شود و این فایل دست نمی‌خورد.

فایل‌های تمرینی بخش اول

پوشه S32-orm-and-sqlalchemy/02_simple_orm را باز کنید و app.py را اجرا کنید. بعد سعی کنید یک مدل سوم به نام Course اضافه کنید و ببینید چقدر کم کد لازم دارد.

۸. SQLAlchemy: نصب و موتور

ORM دست‌ساز ما برای فهمیدن ایده عالی بود، ولی برای یک پروژه واقعی کم می‌آورد: پیوند، بارگذاری تنبل، مهاجرت ساختار جدول‌ها و پشتیبانی از پایگاه‌های داده دیگر. ابزار استاندارد دنیای پایتون SQLAlchemy است.

pip install -r requirements.txt

# requirements.txt
SQLAlchemy==2.0.50
from sqlalchemy import create_engine, event
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker

from models import Base, Course, Enrollment, Student, Teacher

DATABASE_URL = 'sqlite:///database.db'

# The engine is the connection pool: ONE per application.
# echo=True prints every SQL statement - switch it on to see what the ORM does.
engine = create_engine(DATABASE_URL, echo=False, future=True)


@event.listens_for(engine, 'connect')
def set_sqlite_pragma(dbapi_connection, connection_record) -> None:
    '''Turn the foreign keys on for every new connection.

    Exactly the "PRAGMA foreign_keys = ON" of session 31, but done once here
    instead of in every function.
    '''
    cursor = dbapi_connection.cursor()
    cursor.execute('PRAGMA foreign_keys=ON')
    cursor.close()


# A factory that produces sessions. A Session is one "conversation" with the
# database: it collects the changes and writes them at commit().
SessionLocal = sessionmaker(bind=engine, future=True)

موتور و کارخانه نشست: نمونه 03_sqlalchemy/app.py

شرح مثال

۹. تعریف مدل‌ها

در SQLAlchemy نسخه ۲، مدل‌ها را با راهنمای نوع تعریف می‌کنیم. کتابخانه از روی Mapped[int] و Mapped[str] نوع ستون را می‌فهمد. همان راهنمای نوعی که در فصل سیزدهم خواندید، اینجا کار واقعی انجام می‌دهد.

from typing import List, Optional

from sqlalchemy import Float, ForeignKey, String
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
    relationship,
)


class Base(DeclarativeBase):
    'Every model inherits from this; it collects the metadata of the tables.'


class Teacher(Base):
    __tablename__ = 'teacher'

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)

    # One teacher -> many courses.
    # back_populates links the two sides so that both stay in agreement.
    courses: Mapped[List['Course']] = relationship(
        back_populates='teacher',
        cascade='all, delete-orphan',   # deleting a teacher deletes the courses
        passive_deletes=True,           # let the DATABASE do the cascade
    )

    def __repr__(self) -> str:
        return f'<Teacher(id={self.id}, name={self.name!r})>'


class Course(Base):
    __tablename__ = 'course'

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200), nullable=False)

    # The foreign key column itself ...
    teacher_id: Mapped[int] = mapped_column(
        ForeignKey('teacher.id', ondelete='CASCADE'), nullable=False
    )
    # ... and the Python side of the relation.
    teacher: Mapped['Teacher'] = relationship(back_populates='courses')

    enrollments: Mapped[List['Enrollment']] = relationship(
        back_populates='course',
        cascade='all, delete-orphan',
        passive_deletes=True,
    )

مدل‌ها: نمونه 03_sqlalchemy/models.py

شرح مثال

جدول Enrollment نمونه مهمی است: رابطه چندبه‌چند بین دانشجو و درس، به‌علاوه یک داده اضافه یعنی نمره. هر وقت رابطه چندبه‌چند شما داده اضافه دارد، باید جدول جداگانه‌ای با مدل خودش داشته باشد.

class Enrollment(Base):
    '''The link between a student and a course.

    It is a table of its own because it carries extra data: the grade.
    '''

    __tablename__ = 'enrollment'

    id: Mapped[int] = mapped_column(primary_key=True)
    grade: Mapped[Optional[float]] = mapped_column(Float, nullable=True)

    student_id: Mapped[int] = mapped_column(
        ForeignKey('student.id', ondelete='CASCADE'), nullable=False
    )
    course_id: Mapped[int] = mapped_column(
        ForeignKey('course.id', ondelete='CASCADE'), nullable=False
    )

    student: Mapped['Student'] = relationship(back_populates='enrollments')
    course: Mapped['Course'] = relationship(back_populates='enrollments')

جدول واسط با داده اضافه

۱۰. نشست: درج داده

نشست (Session) یک گفت‌وگوی کامل با پایگاه داده است: تغییرها را جمع می‌کند و در commit() همه را با هم می‌نویسد. این همان تراکنش فصل قبل است، فقط با چهره شیءگرا.

def example_1_create_tables() -> None:
    'Example 1: create every table described by the models.'
    Base.metadata.create_all(engine)
    print('Tables created successfully.')


def example_2_insert_data() -> None:
    'Example 2: insert teachers, courses and students.'
    with SessionLocal() as session:
        t1 = Teacher(name='Dr. Smith', email='smith@edu.com')
        t2 = Teacher(name='Prof. Johnson', email='johnson@edu.com')
        session.add_all([t1, t2])

        # flush() sends the INSERTs so that the ids exist, but does NOT commit.
        session.flush()

        session.add_all([
            Course(title='Mathematics 101', teacher_id=t1.id),
            Course(title='Physics 101', teacher_id=t1.id),
            Course(title='History 101', teacher_id=t2.id),
        ])

        session.add_all([
            Student(name='Alice', email='alice@student.edu'),
            Student(name='Bob', email='bob@student.edu'),
            Student(name='Charlie', email='charlie@student.edu'),
        ])

        session.commit()
        print('Data inserted and committed.')

ساخت جدول‌ها و درج: نمونه 03_sqlalchemy/app.py

شرح مثال

یک دام رایج

برخلاف sqlite3، بستن نشست به معنی commit نیست. اگر commit() را فراموش کنید، هیچ چیزی نوشته نمی‌شود و هیچ خطایی هم نمی‌گیرید؛ فقط داده‌ها غیبشان می‌زند.

۱۱. خواندن، تغییر و حذف

def example_3_read_data() -> None:
    'Example 3: read every teacher and every student.'
    with SessionLocal() as session:
        for teacher in session.query(Teacher).all():
            print(f'  {teacher.name} ({teacher.email})')


def example_4_update_record() -> None:
    'Example 4: change the email of a student.'
    with SessionLocal() as session:
        student = session.query(Student).filter_by(name='Alice').first()

        if student is None:
            print('Alice not found.')
            return

        old_email = student.email
        # Simply assigning to the attribute is enough: the session notices it.
        student.email = 'alice.new@student.edu'
        session.commit()
        print(f'Alice email updated from {old_email} to {student.email}')


def example_5_delete_course() -> None:
    'Example 5: delete a course.'
    with SessionLocal() as session:
        course = session.query(Course).filter_by(title='History 101').first()

        if course is None:
            print('Course not found.')
            return

        session.delete(course)
        session.commit()

خواندن، تغییر و حذف

شرح مثال

۱۲. رابطه‌ها و حذف آبشاری

اینجاست که ORM واقعاً برنده می‌شود. برای برداشتن نام دانشجو از روی یک ثبت‌نام، ضرورتی ندارد JOIN بنویسید: کافی است نقطه بگذارید.

def example_6_enroll_students() -> None:
    'Example 6: create enrolments (many-to-many with an extra grade).'
    with SessionLocal() as session:
        alice = session.query(Student).filter_by(name='Alice').first()
        bob = session.query(Student).filter_by(name='Bob').first()
        math = session.query(Course).filter_by(title='Mathematics 101').first()
        physics = session.query(Course).filter_by(title='Physics 101').first()

        if not all([alice, bob, math, physics]):
            print('Required records missing. Run example 2 first.')
            return

        # The objects are given directly: SQLAlchemy fills the ids in for us.
        session.add_all([
            Enrollment(student=alice, course=math, grade=85.5),
            Enrollment(student=bob, course=math, grade=79.0),
            Enrollment(student=alice, course=physics, grade=92.0),
        ])
        session.commit()


def example_7_query_enrollments() -> None:
    'Example 7: read the enrolments through the relations.'
    with SessionLocal() as session:
        for e in session.query(Enrollment).all():
            # e.student and e.course are real objects: no JOIN to write.
            print(f'{e.student.name} enrolled in {e.course.title} '
                  f'- Grade: {e.grade}')

        # Navigating the other way round is just as easy.
        alice = session.query(Student).filter_by(name='Alice').first()
        if alice:
            print(f'Alice follows {len(alice.enrollments)} course(s):',
                  [e.course.title for e in alice.enrollments])

رابطه‌ها در عمل

Alice enrolled in Mathematics 101 - Grade: 85.5
Bob enrolled in Mathematics 101 - Grade: 79.0
Alice enrolled in Physics 101 - Grade: 92.0
Alice follows 2 course(s): ['Mathematics 101', 'Physics 101']

شرح مثال

مسئله N+1

راحتی بارگذاری تنبل یک هزینه دارد. اگر روی صد ثبت‌نام حلقه بزنید و هر بار e.student.name را بخوانید، ممکن است صد و یک پرس‌وجوی جداگانه به پایگاه داده برود. نامش مسئله N+1 است. راه حلش joinedload یا selectinload است که یک JOIN می‌زنند و همه چیز را یک‌جا می‌آورند. برای دیدنش، یک بار echo=True را روشن کنید و تعداد پرس‌وجوها را بشمارید.

حذف آبشاری هم بدون یک خط SQL انجام می‌شود. حذف یک دانشجو، ثبت‌نام‌هایش را هم می‌برد:

def example_8_cascade_delete_student() -> None:
    'Example 8: deleting a student also deletes their enrolments.'
    with SessionLocal() as session:
        bob = session.query(Student).filter_by(name='Bob').first()

        if bob is None:
            print('Bob not found.')
            return

        bob_id = bob.id
        before = session.query(Enrollment).filter_by(student_id=bob_id).count()
        print(f'Bob has {before} enrollment(s).')

        session.delete(bob)
        session.commit()

        after = session.query(Enrollment).filter_by(student_id=bob_id).count()
        print(f"After deletion, Bob's enrollments: {after}")

حذف آبشاری: نمونه 03_sqlalchemy/app.py

Bob has 1 enrollment(s).
After deletion, Bob's enrollments: 0
تنظیمکجا نوشته می‌شودوظیفه آن
ondelete='CASCADE'روی ForeignKeyقانون واقعی در خود پایگاه داده
cascade='all, delete-orphan'روی relationshipقانون در سمت پایتون و نشست
passive_deletes=Trueروی relationshipکار را به پایگاه داده بسپار، سطرها را بیخود نخوان
PRAGMA foreign_keys=ONدر رویداد اتصالبدون آن، SQLite اصلاً آبشار نمی‌کند

۱۳. تراکنش و IntegrityError

def example_10_transaction_rollback() -> None:
    'Example 10: a failed transaction is rolled back.'
    with SessionLocal() as session:
        try:
            # The email already belongs to Alice, and the column is UNIQUE.
            session.add(Student(name='Duplicate',
                                email='alice.new@student.edu'))
            session.commit()
        except IntegrityError:
            session.rollback()
            print('IntegrityError caught! Transaction rolled back.')

            duplicate = session.query(Student).filter_by(name='Duplicate').first()
            print(f"Duplicate student in DB? {'Yes' if duplicate else 'No'}")

برگرداندن تراکنش: نمونه 03_sqlalchemy/app.py

IntegrityError caught! Transaction rolled back.
Duplicate student in DB? No

شرح مثال

۱۴. کی ORM، کی SQL خام

موقعیتانتخاب بهتر
کارهای روزمره: درج، ویرایش، حذف، نمایش فهرستORM
برنامه‌ای که ممکن است پایگاه داده‌اش عوض شودORM
پروژه‌ای که چند نفر رویش کار می‌کنندORM
گزارش تحلیلی سنگین با چند پیوند و زیرپرس‌وجوSQL خام
پرس‌وجویی که باید تا آخرین ذره بهینه باشدSQL خام
یک اسکریپت ده‌خطی یک‌بارمصرفSQL خام

در عمل بیشتر پروژه‌ها هر دو را به کار می‌برند و این کاملاً طبیعی است. SQLAlchemy اجازه می‌دهد هر وقت لازم شد، با text() یک پرس‌وجوی خام را در همان نشست اجرا کنید.

این فصل کجا به کارتان می‌آید

در فصل سی و هشتم همین مدل‌ها را زیر Flask می‌بینید و در فصل سی و نهم، پروژه پایانی روی SQLAlchemy ساخته می‌شود. هر چه اینجا خوب یاد بگیرید، دو فصل آینده را ساده‌تر می‌کند.

فایل‌های تمرینی این فصل

پوشه S32-orm-and-sqlalchemy شامل سه بخش است: 01_why_orm برای مقایسه، 02_simple_orm برای ORM دست‌ساز، و 03_sqlalchemy برای کتابخانه واقعی. بخش سوم نصب لازم دارد: pip install -r requirements.txt. اگر خواستید از صفر شروع کنید، فایل database.db را پاک کنید.

خلاصه فصل

ابزاروظیفه آن
ORMجدول به کلاس، سطر به شیء، ستون به صفت
create_engine(url)موتور، یکی برای کل برنامه
echo=Trueنمایش SQL تولیدشده، برای یادگیری و اشکال‌زدایی
DeclarativeBaseکلاس پایه همه مدل‌ها
__tablename__نام جدول
mapped_column(...)تعریف یک ستون
ForeignKey('t.id')کلید خارجی
relationship(back_populates=...)رابطه دوطرفه بین دو مدل
Base.metadata.create_all(engine)ساخت همه جدول‌ها
sessionmaker(bind=engine)کارخانه نشست
session.add و add_allسپردن شیء به نشست
session.flush()گرفتن شناسه‌ها بدون بستن تراکنش
session.commit()نوشتن همه تغییرها
session.rollback()برگرداندن تراکنش شکست‌خورده
query(M).filter_by(...)پرس‌وجو با شرط
first() و all() و count()گرفتن نتیجه
session.delete(obj)حذف یک رکورد
cascade='all, delete-orphan'حذف آبشاری فرزندان
IntegrityErrorنقض محدودیت. باید گرفته شود

تمرین‌های فصل سی و دو

  1. به ORM دست‌ساز یک مدل Course با ستون‌های title و credit و teacher_id اضافه کنید.
  2. متد Student.enroll(course) را بنویسید که یک سطر در جدول enrollments بسازد.
  3. متد کلاسی Student.search(text) را بنویسید که با LIKE و پارامتر ? جست‌وجو کند.
  4. توضیح دهید وقتی مدل چهارم را اضافه می‌کنید، کلاس پایه Model دقیقاً چند متد را برایتان ذخیره کرده است.
  5. در بخش SQLAlchemy یک مدل Department بسازید و Teacher را با رابطه یک‌به‌چند به آن وصل کنید.
  6. پرس‌وجویی بنویسید که همه درس‌های یک استاد مشخص را برگرداند.
  7. پرس‌وجویی بنویسید که میانگین نمره هر درس را بدهد.
  8. روی موتور echo=True بگذارید و SQL تولیدشده برای تمرین هفتم را بخوانید و توضیح دهید.
  9. یک استاد را حذف کنید و بگویید بر سر درس‌هایش چه می‌آید و چرا.
  10. روی Enrollment یک محدودیت یکتا روی زوج (student_id, course_id) بگذارید و IntegrityError ثبت‌نام تکراری را بگیرید.
فصل قبل: sqlite3 در پایتون فصل بعد: الگوهای طراحی