کار با چند دیتابیس در Django: پیکربندی، مهاجرت‌ها، مسیریابی خودکار و Database Routerها

این مقاله نحوهٔ استفاده از چند دیتابیس در Django را توضیح می‌دهد: از تعریف دیتابیس‌ها در تنظیمات، اجرای migrate روی دیتابیس‌های مختلف، رفتار دستورات مدیریتی، تا مسیریابی خودکار کوئری‌ها و پیاده‌سازی Database Routerها. همچنین نحوهٔ کنترل عملیات خواندن، نوشتن، روابط و مهاجرت‌ها با استفاده از متدهای db_for_read، db_for_write، allow_relation و allow_migrate بررسی می‌شود.

چند دیتابیس، database router، db_for_read، db_for_writeallow_migrate، allow_relationDjango ORM

~16 دقیقه مطالعه • بروزرسانی ۱۹ اسفند ۱۴۰۴

۱. مقدمه

Django به‌طور پیش‌فرض برای یک دیتابیس طراحی شده، اما پشتیبانی کامل از چند دیتابیس را نیز فراهم می‌کند. برای استفاده از چند دیتابیس باید آن‌ها را در تنظیمات تعریف کنید و در صورت نیاز، مسیریابی سفارشی برای کوئری‌ها ایجاد کنید.

۲. تعریف چند دیتابیس در تنظیمات

تمام دیتابیس‌ها در تنظیمات DATABASES تعریف می‌شوند. هر دیتابیس یک alias دارد. alias default اهمیت ویژه‌ای دارد و زمانی استفاده می‌شود که دیتابیس دیگری مشخص نشده باشد.

مثال با دو دیتابیس:


DATABASES = {
    "default": {...},
    "users": {...},
}

وقتی دیتابیس default معنی ندارد

می‌توانید default را خالی بگذارید، اما باید DATABASE_ROUTERS را طوری تنظیم کنید که هیچ کوئری به default نرود.

اگر دیتابیسی را که در DATABASES تعریف نشده صدا بزنید، Django خطای ConnectionDoesNotExist می‌دهد.

۳. همگام‌سازی دیتابیس‌ها با migrate

دستور migrate فقط روی یک دیتابیس اجرا می‌شود. به‌طور پیش‌فرض روی default، اما می‌توانید دیتابیس را مشخص کنید:


./manage.py migrate --database=users

برای همگام‌سازی همهٔ دیتابیس‌ها باید migrate را برای هر دیتابیس جداگانه اجرا کنید.

اگر default خالی باشد، همیشه باید --database را مشخص کنید.

۴. سایر دستورات مدیریتی

بیشتر دستورات مدیریتی که با دیتابیس کار می‌کنند، مانند migrate، فقط روی یک دیتابیس اجرا می‌شوند و با --database کنترل می‌شوند.

استثنا: makemigrations این دستور فقط تاریخچهٔ migration دیتابیس default را بررسی می‌کند، مگر اینکه routerها رفتار دیگری تعیین کنند.

۵. مسیریابی خودکار دیتابیس‌ها

Django یک سیستم مسیریابی پیش‌فرض دارد که:

  • آبجکت‌ها را به دیتابیسی که از آن آمده‌اند «چسبنده» نگه می‌دارد
  • در صورت عدم تعیین دیتابیس، از default استفاده می‌کند

برای رفتارهای پیچیده‌تر، باید Database Router سفارشی تعریف کنید.

۶. Database Router چیست؟

Router کلاسی است که می‌تواند چهار متد زیر را پیاده‌سازی کند:

۶.۱ db_for_read(model, **hints)

مشخص می‌کند عملیات خواندن روی کدام دیتابیس انجام شود.

۶.۲ db_for_write(model, **hints)

مشخص می‌کند عملیات نوشتن روی کدام دیتابیس انجام شود.

۶.۳ allow_relation(obj1, obj2, **hints)

مشخص می‌کند آیا رابطهٔ بین دو آبجکت مجاز است یا نه.

اگر همهٔ routerها None برگردانند، فقط روابط داخل یک دیتابیس مجاز هستند.

۶.۴ allow_migrate(db, app_label, model_name=None, **hints)

مشخص می‌کند آیا عملیات migration روی دیتابیس مشخص‌شده مجاز است یا نه.

این متد تعیین می‌کند یک مدل روی کدام دیتابیس وجود داشته باشد.

۷. Hints در Router

Routerها یک دیکشنری hints دریافت می‌کنند که اطلاعات اضافی دربارهٔ عملیات جاری می‌دهد. مهم‌ترین hint:

  • instance — آبجکتی که عملیات روی آن انجام می‌شود

Router می‌تواند از این اطلاعات برای تصمیم‌گیری استفاده کند.

۸. مثال یک Router سفارشی


class UserRouter:
    def db_for_read(self, model, **hints):
        if model._meta.app_label == "users":
            return "users"
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == "users":
            return "users"
        return None

    def allow_relation(self, obj1, obj2, **hints):
        return obj1._state.db == obj2._state.db

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == "users":
            return db == "users"
        return None

جمع‌بندی

Django امکانات قدرتمندی برای کار با چند دیتابیس ارائه می‌دهد: از تعریف دیتابیس‌ها و اجرای migrate روی هرکدام، تا مسیریابی خودکار و سفارشی‌سازی رفتار خواندن، نوشتن، روابط و مهاجرت‌ها. با استفاده از Database Routerها می‌توانید معماری‌های پیچیدهٔ چند دیتابیس را به‌سادگی مدیریت کنید.

۱. نصب و استفاده از Routerها

Routerها با تنظیم DATABASE_ROUTERS فعال می‌شوند. این تنظیم شامل لیستی از کلاس‌هایی است که Django برای تصمیم‌گیری دربارهٔ انتخاب دیتابیس از آن‌ها استفاده می‌کند.

هر بار که Django بخواهد بداند یک کوئری روی کدام دیتابیس اجرا شود، base router را صدا می‌زند. base router به ترتیب Routerهای تعریف‌شده را بررسی می‌کند تا یکی از آن‌ها دیتابیس مناسب را پیشنهاد دهد.

اگر هیچ Router پیشنهادی ندهد:

  • اگر hint شامل instance باشد → از instance._state.db استفاده می‌شود
  • در غیر این صورت → دیتابیس default انتخاب می‌شود

۲. مثال عملی: معماری چند دیتابیس

در این مثال چند دیتابیس داریم:

  • یک دیتابیس جدا برای auth و contenttypes
  • یک معماری primary/replica برای سایر اپ‌ها

تنظیمات دیتابیس‌ها:


DATABASES = {
    "default": {},
    "auth_db": {...},
    "primary": {...},
    "replica1": {...},
    "replica2": {...},
}

۳. Router مخصوص auth

این Router تضمین می‌کند که مدل‌های auth و contenttypes فقط روی دیتابیس auth_db ذخیره و خوانده شوند.


class AuthRouter:
    route_app_labels = {"auth", "contenttypes"}

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def allow_relation(self, obj1, obj2, **hints):
        if (
            obj1._meta.app_label in self.route_app_labels
            or obj2._meta.app_label in self.route_app_labels
        ):
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label in self.route_app_labels:
            return db == "auth_db"
        return None

۴. Router برای primary/replica

این Router:

  • خواندن‌ها را به‌صورت تصادفی بین replicaها پخش می‌کند
  • نوشتن‌ها را همیشه به primary می‌فرستد
  • روابط را فقط بین دیتابیس‌های primary/replica مجاز می‌داند

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        return random.choice(["replica1", "replica2"])

    def db_for_write(self, model, **hints):
        return "primary"

    def allow_relation(self, obj1, obj2, **hints):
        db_set = {"primary", "replica1", "replica2"}
        if obj1._state.db in db_set and obj2._state.db in db_set:
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        return True

۵. ثبت Routerها در تنظیمات


DATABASE_ROUTERS = [
    "path.to.AuthRouter",
    "path.to.PrimaryReplicaRouter",
]

ترتیب Routerها بسیار مهم است. در این مثال AuthRouter قبل از PrimaryReplicaRouter بررسی می‌شود، بنابراین مدل‌های auth همیشه به درستی مسیریابی می‌شوند.

۶. مثال اجرای واقعی کد


# این کوئری روی auth_db اجرا می‌شود
fred = User.objects.get(username="fred")

fred.first_name = "Frederick"
fred.save()  # روی auth_db

# این کوئری روی یکی از replicaها اجرا می‌شود
dna = Person.objects.get(name="Douglas Adams")

# آبجکت جدید دیتابیس مشخصی ندارد
mh = Book(title="Mostly Harmless")

# نسبت دادن author باعث می‌شود mh به دیتابیس dna چسبیده شود
mh.author = dna

# ذخیره باعث می‌شود mh روی primary ذخیره شود
mh.save()

# اما بازیابی دوباره از replica انجام می‌شود
mh = Book.objects.get(title="Mostly Harmless")

۷. انتخاب دستی دیتابیس

می‌توانید دیتابیس را به‌صورت دستی برای QuerySet انتخاب کنید. این انتخاب همیشه از Routerها مهم‌تر است.


Author.objects.all()              # روی default
Author.objects.using("default")   # روی default
Author.objects.using("other")     # روی دیتابیس other

جمع‌بندی

Routerها ابزار قدرتمندی برای مدیریت چند دیتابیس در Django هستند. با آن‌ها می‌توانید کنترل کنید که هر مدل روی کدام دیتابیس خوانده، نوشته، migrate یا مرتبط شود. همچنین می‌توانید با using() دیتابیس را به‌صورت دستی انتخاب کنید. این امکانات امکان ساخت معماری‌های پیچیده مانند primary/replica یا دیتابیس‌های جدا برای اپ‌های مختلف را فراهم می‌کند.

۱. انتخاب دیتابیس برای save()

برای ذخیره‌کردن یک آبجکت روی دیتابیس مشخص، از پارامتر using در متد save() استفاده می‌شود:


my_object.save(using="legacy_users")

اگر using مشخص نشود، Django دیتابیس را بر اساس Routerها انتخاب می‌کند (معمولاً default).

۲. جابه‌جایی آبجکت بین دیتابیس‌ها

ممکن است وسوسه شوید که با save(using=...) یک آبجکت را از یک دیتابیس به دیتابیس دیگر منتقل کنید، اما این کار می‌تواند خطرناک باشد.

مثال:


p = Person(name="Fred")
p.save(using="first")     # INSERT → ایجاد primary key
p.save(using="second")    # استفاده از همان primary key روی دیتابیس دوم

اگر primary key در دیتابیس دوم وجود داشته باشد، رکورد قبلی overwrite می‌شود.

دو راه‌حل ایمن:

راه‌حل ۱: پاک‌کردن primary key


p = Person(name="Fred")
p.save(using="first")
p.pk = None
p.save(using="second")  # ایجاد رکورد جدید بدون خطر

راه‌حل ۲: استفاده از force_insert


p = Person(name="Fred")
p.save(using="first")
p.save(using="second", force_insert=True)

در این حالت Django مجبور به INSERT می‌شود و اگر primary key تکراری باشد، خطا می‌دهد.

۳. انتخاب دیتابیس برای delete()

به‌طور پیش‌فرض، حذف روی همان دیتابیسی انجام می‌شود که آبجکت از آن بازیابی شده است:


u = User.objects.using("legacy_users").get(username="fred")
u.delete()   # حذف از legacy_users

برای حذف از دیتابیس دیگر:


user_obj.save(using="new_users")
user_obj.delete(using="legacy_users")

۴. استفاده از Managerها در چند دیتابیس

متدهای Manager (مثل create_user()) را نمی‌توان با using() هدایت کرد، چون این متدها متعلق به QuerySet نیستند.

راه‌حل: استفاده از db_manager():


User.objects.db_manager("new_users").create_user(...)

db_manager() یک نسخهٔ جدید از Manager برمی‌گرداند که به دیتابیس مشخص‌شده متصل است.

۵. پیاده‌سازی صحیح get_queryset() در محیط چند دیتابیس

اگر get_queryset() را override می‌کنید، باید مطمئن شوید که QuerySet از دیتابیس صحیح استفاده می‌کند. برای این کار از self._db استفاده کنید:


class MyManager(models.Manager):
    def get_queryset(self):
        qs = CustomQuerySet(self.model)
        if self._db is not None:
            qs = qs.using(self._db)
        return qs

این کار باعث می‌شود Manager هنگام استفاده از db_manager() یا routing، رفتار صحیحی داشته باشد.

جمع‌بندی

Django ابزارهای دقیقی برای کنترل دیتابیس در عملیات save، delete و Managerها ارائه می‌دهد. با درک رفتار primary keyها، استفادهٔ صحیح از db_manager() و پیاده‌سازی درست get_queryset() می‌توانید در معماری چند دیتابیس به‌صورت ایمن و قابل‌پیش‌بینی کار کنید.

۱. پشتیبانی Django Admin از چند دیتابیس

Django Admin به‌صورت پیش‌فرض پشتیبانی مستقیم از چند دیتابیس ندارد. اگر می‌خواهید مدل‌ها را از دیتابیسی غیر از دیتابیس انتخاب‌شده توسط Routerها مدیریت کنید، باید ModelAdmin سفارشی بنویسید.

۲. سفارشی‌سازی ModelAdmin برای چند دیتابیس

برای هدایت عملیات Admin به دیتابیس مشخص، باید متدهای زیر را override کنید:


class MultiDBModelAdmin(admin.ModelAdmin):
    using = "other"

    def save_model(self, request, obj, form, change):
        obj.save(using=self.using)

    def delete_model(self, request, obj):
        obj.delete(using=self.using)

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

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

۳. سفارشی‌سازی InlineAdmin برای چند دیتابیس

Inlineها نیز نیاز به override سه متد دارند:


class MultiDBTabularInline(admin.TabularInline):
    using = "other"

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

۴. ثبت Adminهای سفارشی


class BookInline(MultiDBTabularInline):
    model = Book

class PublisherAdmin(MultiDBModelAdmin):
    inlines = [BookInline]

admin.site.register(Author, MultiDBModelAdmin)
admin.site.register(Publisher, PublisherAdmin)

othersite = admin.AdminSite("othersite")
othersite.register(Publisher, MultiDBModelAdmin)

در این مثال دو سایت Admin داریم: یکی شامل Author و Publisher (با inline کتاب‌ها)، و دیگری فقط Publisher.

۵. استفاده از Raw Cursorها در چند دیتابیس

برای اجرای SQL خام روی دیتابیس مشخص، از connections استفاده کنید:


from django.db import connections

with connections["my_db_alias"].cursor() as cursor:
    ...

۶. محدودیت‌های چند دیتابیس

۶.۱ روابط بین دیتابیس‌ها (Cross‑Database Relations)

Django از روابط بین دیتابیس‌ها پشتیبانی نمی‌کند:

  • ForeignKey
  • ManyToMany

این محدودیت به دلیل نیاز به بررسی صحت کلیدهای خارجی است.

در دیتابیس‌هایی مثل PostgreSQL، SQLite، Oracle و MySQL/InnoDB این محدودیت توسط خود دیتابیس enforced می‌شود.

در MyISAM ممکن است بتوانید «ظاهراً» رابطه ایجاد کنید، اما Django آن را پشتیبانی نمی‌کند.

۶.۲ رفتار اپلیکیشن‌های contrib

برخی اپ‌ها باید در یک دیتابیس مشترک باشند:

  • auth، contenttypes، permissions → همه باید کنار هم باشند
  • admin → وابسته به auth
  • flatpages و redirects → وابسته به sites

همچنین پس از migrate، برخی آبجکت‌ها به‌طور خودکار ایجاد می‌شوند:

  • Site پیش‌فرض
  • ContentType برای هر مدل
  • Permission برای هر مدل

در معماری چند دیتابیس معمولاً نمی‌خواهید این آبجکت‌ها در چند دیتابیس تکرار شوند. پس باید Router بنویسید که فقط یک دیتابیس را برای این مدل‌ها sync کند.

هشدار: اگر ContentTypeها را در چند دیتابیس sync کنید، primary keyها ممکن است متفاوت باشند و باعث فساد داده شود.

جمع‌بندی

Django Admin به‌صورت پیش‌فرض چند دیتابیس را مدیریت نمی‌کند، اما با سفارشی‌سازی ModelAdmin و InlineAdmin می‌توانید کنترل کامل روی دیتابیس مقصد داشته باشید. در کنار آن، باید محدودیت‌های مهم مانند عدم پشتیبانی از روابط بین دیتابیس‌ها و رفتار اپ‌های contrib را در نظر بگیرید.

۱. پشتیبانی Django Admin از چند دیتابیس

Django Admin به‌صورت پیش‌فرض پشتیبانی مستقیم از چند دیتابیس ندارد. اگر می‌خواهید مدل‌ها را از دیتابیسی غیر از دیتابیس انتخاب‌شده توسط Routerها مدیریت کنید، باید ModelAdmin سفارشی بنویسید.

۲. سفارشی‌سازی ModelAdmin برای چند دیتابیس

برای هدایت عملیات Admin به دیتابیس مشخص، باید متدهای زیر را override کنید:


class MultiDBModelAdmin(admin.ModelAdmin):
    using = "other"

    def save_model(self, request, obj, form, change):
        obj.save(using=self.using)

    def delete_model(self, request, obj):
        obj.delete(using=self.using)

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

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

۳. سفارشی‌سازی InlineAdmin برای چند دیتابیس

Inlineها نیز نیاز به override سه متد دارند:


class MultiDBTabularInline(admin.TabularInline):
    using = "other"

    def get_queryset(self, request):
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

۴. ثبت Adminهای سفارشی


class BookInline(MultiDBTabularInline):
    model = Book

class PublisherAdmin(MultiDBModelAdmin):
    inlines = [BookInline]

admin.site.register(Author, MultiDBModelAdmin)
admin.site.register(Publisher, PublisherAdmin)

othersite = admin.AdminSite("othersite")
othersite.register(Publisher, MultiDBModelAdmin)

در این مثال دو سایت Admin داریم: یکی شامل Author و Publisher (با inline کتاب‌ها)، و دیگری فقط Publisher.

۵. استفاده از Raw Cursorها در چند دیتابیس

برای اجرای SQL خام روی دیتابیس مشخص، از connections استفاده کنید:


from django.db import connections

with connections["my_db_alias"].cursor() as cursor:
    ...

۶. محدودیت‌های چند دیتابیس

۶.۱ روابط بین دیتابیس‌ها (Cross‑Database Relations)

Django از روابط بین دیتابیس‌ها پشتیبانی نمی‌کند:

  • ForeignKey
  • ManyToMany

این محدودیت به دلیل نیاز به بررسی صحت کلیدهای خارجی است.

در دیتابیس‌هایی مثل PostgreSQL، SQLite، Oracle و MySQL/InnoDB این محدودیت توسط خود دیتابیس enforced می‌شود.

در MyISAM ممکن است بتوانید «ظاهراً» رابطه ایجاد کنید، اما Django آن را پشتیبانی نمی‌کند.

۶.۲ رفتار اپلیکیشن‌های contrib

برخی اپ‌ها باید در یک دیتابیس مشترک باشند:

  • auth، contenttypes، permissions → همه باید کنار هم باشند
  • admin → وابسته به auth
  • flatpages و redirects → وابسته به sites

همچنین پس از migrate، برخی آبجکت‌ها به‌طور خودکار ایجاد می‌شوند:

  • Site پیش‌فرض
  • ContentType برای هر مدل
  • Permission برای هر مدل

در معماری چند دیتابیس معمولاً نمی‌خواهید این آبجکت‌ها در چند دیتابیس تکرار شوند. پس باید Router بنویسید که فقط یک دیتابیس را برای این مدل‌ها sync کند.

هشدار: اگر ContentTypeها را در چند دیتابیس sync کنید، primary keyها ممکن است متفاوت باشند و باعث فساد داده شود.

جمع‌بندی

Django Admin به‌صورت پیش‌فرض چند دیتابیس را مدیریت نمی‌کند، اما با سفارشی‌سازی ModelAdmin و InlineAdmin می‌توانید کنترل کامل روی دیتابیس مقصد داشته باشید. در کنار آن، باید محدودیت‌های مهم مانند عدم پشتیبانی از روابط بین دیتابیس‌ها و رفتار اپ‌های contrib را در نظر بگیرید.

نوشته و پژوهش شده توسط دکتر شاهین صیامی