راه
۴ کیو· سختپیشرفتهPYTHON۴ ثانیه · ۲۵۶ مگابایت

QMDB – مدل‌ها

این تمرین در اصل برای ارسال چند فایل (در یک zip) نوشته شده است. در این نسخه همه‌چیز را در همین یک فایل بنویس: اول مدل‌ها، بعد Managerها. خط‌های from models import … هم همان‌طور که در صورت سؤال آمده کار می‌کنند. داور یک پایگاه‌داده‌ی SQLite در حافظه می‌سازد، جدول‌ها را از روی Base تو درست می‌کند و متغیر session را در اختیار آزمون‌ها می‌گذارد.

در تمرین‌های این فصل، قرار است یک سیستم مدیریت پایگاه‌داده برای سامانه‌ی رتبه‌بندی فیلم کوئرا طراحی کنید. در بخش اول این پروژه، شما باید مدل‌های این سیستم را طراحی کنید. برای هر مدل، توضیحات فیلدها و ویژگی‌های لازم آورده شده‌است:


🎬 مدل Movie (فیلم‌ها)

این مدل اطلاعات مربوط به فیلم‌ها را ذخیره می‌کند.

فیلدنوعویژگی‌ها
idintکلید اصلی، autoincrement
titlestrغیرتهی
release_yearintاختیاری
genresList[Genre]رابطه‌ی چندبه‌چند با Genre از طریق مدل میانی MovieGenre
reviewsList[Review]رابطه‌ی یک‌به‌چند با Review

👤 مدل User (کاربران)

این مدل اطلاعات کاربران سیستم را ذخیره می‌کند.

فیلدنوعویژگی‌ها
idintکلید اصلی، autoincrement
namestrغیرتهی
emailstrیکتا، غیرتهی
is_verifiedboolمقدار پیش‌فرض: False
created_atdatetimeمقدار پیش‌فرض: زمان حال (datetime.now)
reviewsList[Review]رابطه‌ی یک‌به‌چند با Review

برای دسترسی به زمان حال می‌توانید از کتابخانه‌ی datetime استفاده کنید.

نکته: برای استفاده از این تابع به‌عنوان ورودی آرگومان default، نباید آن را فراخوانی کنید؛ یعنی ورودی باید به‌صورت default=datetime.now باشد.

python
from datetime import datetime
datetime.now()
# datetime.datetime(2024, 12, 25, 13, 34, 33, 510059)

🏷️ مدل Genre (ژانرها)

این مدل اطلاعات ژانرهای فیلم را ذخیره می‌کند.

فیلدنوعویژگی‌ها
idintکلید اصلی، autoincrement
namestrیکتا، غیرتهی
moviesList[Movie]رابطه‌ی چندبه‌چند با Movie از طریق مدل میانی MovieGenre

⭐ مدل Review (نظرات)

این مدل اطلاعات نظرات کاربران برای فیلم‌ها را ذخیره می‌کند.

فیلدنوعویژگی‌ها
idintکلید اصلی، autoincrement
movie_idintکلید خارجی به Movie
user_idintکلید خارجی به User
ratingintغیرتهی
commentstrاختیاری
created_atdatetimeمقدار پیش‌فرض: زمان حال (datetime.now)
updated_atdatetimeمقدار پیش‌فرض: زمان حال (datetime.now) مقدار در زمان به‌روزرسانی: زمان حال (datetime.now)

مقدار در زمان به‌روزرسانی را می‌توانید با استفاده از آرگومان ورودی onupdate تعیین کنید. وجود این آرگومان باعث می‌شود در هنگام به‌روزرسانی هر شیء از Review، مقدار این فیلد نیز به‌صورت خودکار به‌روزرسانی شود و به‌عبارتی، زمان به‌روزرسانی شیء به‌صورت خودکار ثبت شود.


🔗 مدل MovieGenre (رابط بین ژانر و فیلم)

این مدل یک جدول میانی است که رابطه‌ی چندبه‌چند بین فیلم‌ها و ژانرها را مدیریت می‌کند.

فیلدنوعویژگی‌ها
movie_idintکلید خارجی به Movie
genre_idintکلید خارجی به Genre

✅ پاسخ نهایی: کد کامل مدل‌ها

python
from typing import List
from datetime import datetime
import sqlalchemy as db
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

class Base(DeclarativeBase):
    pass

# ==================== مدل میانی ====================
class MovieGenre(Base):
    __tablename__ = 'movie_genres'

    movie_id: Mapped[int] = mapped_column(db.ForeignKey('movies.id'), primary_key=True)
    genre_id: Mapped[int] = mapped_column(db.ForeignKey('genres.id'), primary_key=True)

# ==================== مدل Movie ====================
class Movie(Base):
    __tablename__ = 'movies'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    title: Mapped[str]
    release_year: Mapped[int | None]

    # رابطه‌ی چندبه‌چند با Genre از طریق MovieGenre
    genres: Mapped[List["Genre"]] = relationship(
        secondary="movie_genres",
        back_populates="movies"
    )

    # رابطه‌ی یک‌به‌چند با Review
    reviews: Mapped[List["Review"]] = relationship(
        back_populates="movie",
        cascade="all, delete-orphan"
    )

# ==================== مدل User ====================
class User(Base):
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)
    is_verified: Mapped[bool] = mapped_column(default=False)
    created_at: Mapped[datetime] = mapped_column(default=datetime.now)

    # رابطه‌ی یک‌به‌چند با Review
    reviews: Mapped[List["Review"]] = relationship(
        back_populates="user",
        cascade="all, delete-orphan"
    )

# ==================== مدل Genre ====================
class Genre(Base):
    __tablename__ = 'genres'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(unique=True)

    # رابطه‌ی چندبه‌چند با Movie از طریق MovieGenre
    movies: Mapped[List["Movie"]] = relationship(
        secondary="movie_genres",
        back_populates="genres"
    )

# ==================== مدل Review ====================
class Review(Base):
    __tablename__ = 'reviews'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    rating: Mapped[int]
    comment: Mapped[str | None]
    created_at: Mapped[datetime] = mapped_column(default=datetime.now)
    updated_at: Mapped[datetime] = mapped_column(
        default=datetime.now,
        onupdate=datetime.now
    )

    # کلیدهای خارجی
    movie_id: Mapped[int] = mapped_column(db.ForeignKey('movies.id'))
    user_id: Mapped[int] = mapped_column(db.ForeignKey('users.id'))

    # روابط
    movie: Mapped["Movie"] = relationship(back_populates="reviews")
    user: Mapped["User"] = relationship(back_populates="reviews")

📄 models.py


چرا ارتباط بین مدل‌های Movie و Genre به‌درستی برقرار نمی‌شود؟

همان‌طور که در درس‌نامه‌ی قبل گفته شد، برای برقراری ارتباط بین این دو مدل باید از روش ساده‌تر مدل میانی استفاده کنید. در این روش، مدل میانی تنها شامل دو فیلد movie_id و genre_id خواهد بود. مدل‌های Movie و Genre نیز به‌صورت زیر تعریف می‌شوند:

python
class Movie(Base):
    # ... سایر فیلدها
    genres: Mapped[List["Genre"]] = relationship(secondary="movie_genres", back_populates="movies")

class Genre(Base):
    # ... سایر فیلدها
    movies: Mapped[List["Movie"]] = relationship(secondary="movie_genres", back_populates="genres")

  • برای رابطه‌ی چندبه‌چند بین Movie و Genre از مدل میانی MovieGenre استفاده شده است
  • رابطه‌ی یک‌به‌چند بین Movie و Review با ForeignKey در Review برقرار شده است
  • رابطه‌ی یک‌به‌چند بین User و Review با ForeignKey در Review برقرار شده است
  • برای فیلد created_at از default=datetime.now استفاده شده است (بدون پرانتز)
  • برای فیلد updated_at از default=datetime.now و onupdate=datetime.now استفاده شده است
  • فیلدهای اختیاری با | None مشخص شده‌اند (مانند release_year و comment)

🚀 پس از طراحی مدل‌ها، در تمرین بعدی با نحوه‌ی ایجاد جلسه (Session) و انجام عملیات CRUD روی این مدل‌ها آشنا خواهید شد.

نمونه‌ها

ورودی نمونه ۱
sorted(Base.metadata.tables)
خروجی نمونه ۱
['genres', 'movie_genres', 'movies', 'reviews', 'users']
ورودی نمونه ۲
users = Base.metadata.tables["users"]
sorted((column.name, column.nullable) for column in users.columns)
users.c.email.unique
خروجی نمونه ۲
[('created_at', False), ('email', False), ('id', False), ('is_verified', False), ('name', False)]
True
ورودی نمونه ۳
[sorted((column.name, column.nullable) for column in Base.metadata.tables[table].columns)
 for table in ("movies", "genres", "reviews")]
خروجی نمونه ۳
[[('id', False), ('release_year', True), ('title', False)], [('id', False), ('name', False)], [('comment', True), ('created_at', False), ('id', False), ('movie_id', False), ('rating', False), ('updated_at', False), ('user_id', False)]]
ورودی نمونه ۴
sorted(fk.target_fullname for fk in Base.metadata.tables["reviews"].foreign_keys)
sorted(fk.target_fullname for fk in Base.metadata.tables["movie_genres"].foreign_keys)
خروجی نمونه ۴
['movies.id', 'users.id']
['genres.id', 'movies.id']
ورودی نمونه ۵
movie = Movie(title="Inception", release_year=2010)
genre = Genre(name="Sci-Fi")
movie.genres.append(genre)
ali = User(name="Ali", email="ali@example.com")
session.add_all([movie, ali])
session.commit()
session.add(Review(movie_id=movie.id, user_id=ali.id, rating=5, comment="great"))
session.commit()
[genre.movies[0].title, len(movie.reviews), ali.reviews[0].rating, ali.is_verified, type(ali.created_at).__name__]
خروجی نمونه ۵
['Inception', 1, 5, False, 'datetime']
ورودی نمونه ۶
import time
sara = User(name="Sara", email="sara@example.com")
up = Movie(title="Up")
session.add_all([sara, up])
session.commit()
review = Review(movie_id=up.id, user_id=sara.id, rating=3)
session.add(review)
session.commit()
before = review.updated_at
time.sleep(0.02)
review.rating = 4
session.commit()
[review.updated_at > before, review.created_at <= before, review.comment is None, up.release_year is None]
خروجی نمونه ۶
[True, True, True, True]
ورودی نمونه ۷
from sqlalchemy.exc import IntegrityError
session.add(User(name="A", email="same@example.com"))
session.commit()
session.add(User(name="B", email="same@example.com"))
try:
    session.commit()
except IntegrityError:
    print("email must be unique")
    session.rollback()
session.add(Movie(title=None))
try:
    session.commit()
except IntegrityError:
    print("title is required")
    session.rollback()
خروجی نمونه ۷
email must be unique
title is required

برای نوشتن پاسخ و دریافت امتیاز، ابتدا وارد حساب شوید.

ورود