多個資料庫

本主題指南描述了 Django 對於與多個資料庫互動的支援。 Django 大部分的文件都假設您正在與單一資料庫互動。 如果您想要與多個資料庫互動,您需要採取一些額外的步驟。

另請參閱

請參閱 多資料庫支援,以取得有關使用多個資料庫進行測試的資訊。

定義您的資料庫

在 Django 中使用多個資料庫的第一步是告訴 Django 您將要使用的資料庫伺服器。 這是使用 DATABASES 設定完成的。 此設定會將資料庫別名 (一種在整個 Django 中引用特定資料庫的方式) 對應到該特定連線的設定字典。 內部字典中的設定在 DATABASES 文件中有完整描述。

資料庫可以有您選擇的任何別名。 但是,別名 default 具有特殊意義。 當未選擇其他資料庫時,Django 會使用別名為 default 的資料庫。

以下是一個範例 settings.py 片段,定義了兩個資料庫 - 一個預設的 PostgreSQL 資料庫和一個名為 users 的 MySQL 資料庫

DATABASES = {
    "default": {
        "NAME": "app_data",
        "ENGINE": "django.db.backends.postgresql",
        "USER": "postgres_user",
        "PASSWORD": "s3krit",
    },
    "users": {
        "NAME": "user_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "priv4te",
    },
}

如果 default 資料庫的概念在您的專案中沒有意義,您需要小心始終指定您想要使用的資料庫。 Django 要求必須定義 default 資料庫項目,但如果不會使用,則可以將參數字典留空。 為此,您必須為所有應用程式的模型設定 DATABASE_ROUTERS,包括您正在使用的任何 contrib 和第三方應用程式中的模型,以便沒有查詢被路由到預設資料庫。 以下是一個範例 settings.py 片段,定義了兩個非預設資料庫,並有意將 default 條目留空

DATABASES = {
    "default": {},
    "users": {
        "NAME": "user_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "superS3cret",
    },
    "customers": {
        "NAME": "customer_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_cust",
        "PASSWORD": "veryPriv@ate",
    },
}

如果您嘗試存取您未在 DATABASES 設定中定義的資料庫,Django 將會引發 django.utils.connection.ConnectionDoesNotExist 例外。

同步您的資料庫

migrate 管理命令一次操作一個資料庫。 預設情況下,它會對 default 資料庫進行操作,但是透過提供 --database 選項,您可以告訴它同步不同的資料庫。 因此,為了將所有模型同步到上面第一個範例中的所有資料庫,您需要呼叫

$ ./manage.py migrate
$ ./manage.py migrate --database=users

如果您不希望每個應用程式都同步到特定的資料庫,您可以定義一個 資料庫路由器,該路由器實作一個限制特定模型可用性的政策。

如果像上面第二個範例一樣,您將 default 資料庫留空,則每次執行 migrate 時,都必須提供資料庫名稱。 省略資料庫名稱將會引發錯誤。 對於第二個範例

$ ./manage.py migrate --database=users
$ ./manage.py migrate --database=customers

使用其他管理命令

大多數其他與資料庫互動的 django-admin 命令的操作方式與 migrate 相同 - 它們一次只對一個資料庫進行操作,並使用 --database 來控制使用的資料庫。

此規則的例外是 makemigrations 命令。 它會驗證資料庫中的移轉歷史記錄,以在建立新的移轉之前,捕獲現有移轉檔案的問題(可能是因為編輯它們而造成的)。 預設情況下,它只會檢查 default 資料庫,但如果安裝了任何 路由器,則會查詢其 allow_migrate() 方法。

自動資料庫路由

使用多個資料庫最簡單的方法是設定資料庫路由方案。 預設路由方案可確保物件保持「黏性」到其原始資料庫(也就是說,從 foo 資料庫擷取的物件將會儲存在相同的資料庫中)。預設路由方案可確保如果未指定資料庫,所有查詢都會回退到 default 資料庫。

您不需要執行任何操作來啟動預設路由方案 - 它在每個 Django 專案中都是「開箱即用」的。但是,如果您想要實作更有趣的資料庫配置行為,您可以定義並安裝自己的資料庫路由器。

資料庫路由器

資料庫路由器是一個類別,它最多提供四個方法

db_for_read(model, **hints)

建議應該用於 model 類型物件的讀取操作的資料庫。

如果資料庫操作能夠提供任何可能有助於選擇資料庫的額外資訊,則會將其提供在 hints 字典中。 有效提示的詳細資訊在 下面 提供。

如果沒有建議,則傳回 None

db_for_write(model, **hints)

建議應該用於 Model 類型物件的寫入操作的資料庫。

如果資料庫操作能夠提供任何可能有助於選擇資料庫的額外資訊,則會將其提供在 hints 字典中。 有效提示的詳細資訊在 下面 提供。

如果沒有建議,則傳回 None

allow_relation(obj1, obj2, **hints)

如果應該允許 obj1obj2 之間的關係,則傳回 True;如果應該阻止該關係,則傳回 False;如果路由器沒有意見,則傳回 None。 這純粹是一個驗證操作,由外來索引和多對多操作使用,以判斷是否應該允許兩個物件之間的關係。

如果沒有路由器有意見(即所有路由器都傳回 None),則僅允許在同一資料庫內的關係。

allow_migrate(db, app_label, model_name=None, **hints)

判斷是否允許在別名為 db 的資料庫上執行移轉操作。 如果應該執行操作,則傳回 True;如果不應該執行,則傳回 False;如果路由器沒有意見,則傳回 None

app_label 位置參數是要移轉的應用程式的標籤。

model_name 由大多數移轉操作設定為 model._meta.model_name 的值(模型的 __name__ 的小寫版本),正在移轉的模型。 對於 RunPythonRunSQL 操作,除非它們使用提示提供,否則其值為 None

hints 由某些操作使用,以將額外資訊傳達給路由器。

當設定 model_name 時,hints 通常會包含以鍵 'model' 儲存的模型類別。請注意,它可能是一個歷史模型,因此不具有任何自訂的屬性、方法或管理器。您應該只依賴 _meta

此方法也可用於判斷特定資料庫上模型的可用性。

makemigrations 總是會為模型變更建立遷移,但如果 allow_migrate() 回傳 False,則當在 db 上執行 migrate 時,針對 model_name 的任何遷移操作都會被靜默跳過。針對已經有遷移的模型變更 allow_migrate() 的行為,可能會導致外鍵損壞、多餘的表格或遺失的表格。當 makemigrations 驗證遷移歷史記錄時,它會跳過任何應用程式都不允許遷移的資料庫。

路由不必提供所有這些方法,它可以省略其中一個或多個。如果省略了其中一個方法,Django 在執行相關檢查時將會跳過該路由。

提示

資料庫路由接收到的提示可用於決定哪個資料庫應接收給定的請求。

目前,唯一會提供的提示是 instance,一個與正在進行的讀取或寫入操作相關的物件實例。這可能是正在儲存的實例,或是正在新增到多對多關係中的實例。在某些情況下,根本不會提供實例提示。路由會檢查實例提示是否存在,並判斷是否應使用該提示來變更路由行為。

使用路由

資料庫路由使用 DATABASE_ROUTERS 設定安裝。此設定定義了一個類別名稱列表,每個類別都指定了應由基礎路由 (django.db.router) 使用的路由。

Django 的資料庫操作會使用基礎路由來分配資料庫的使用。每當查詢需要知道要使用哪個資料庫時,它會呼叫基礎路由,並提供模型和提示(如果有的話)。基礎路由會依次嘗試每個路由類別,直到其中一個傳回資料庫建議。如果沒有任何路由傳回建議,則基礎路由會嘗試提示實例的目前 instance._state.db。如果沒有提供提示實例,或 instance._state.dbNone,則基礎路由將會分配 default 資料庫。

範例

僅用於範例目的!

此範例旨在示範如何使用路由基礎結構來變更資料庫的使用。它故意忽略了一些複雜的問題,以示範如何使用路由。

如果 myapp 中的任何模型包含與 other 資料庫以外的模型之間的關係,則此範例將無法運作。跨資料庫關係會引入 Django 目前無法處理的參照完整性問題。

所描述的主要/複本(某些資料庫稱為主/從)組態也有缺陷 - 它沒有提供任何解決方案來處理複製延遲(也就是說,由於寫入傳播到複本所需的時間而引入的查詢不一致)。它也沒有考慮事務與資料庫使用策略的相互作用。

那麼,這在實務上意味著什麼?讓我們考慮另一個範例組態。這個組態將有多個資料庫:一個用於 auth 應用程式,以及所有其他使用具有兩個讀取複本的主要/複本設定的應用程式。以下是指定這些資料庫的設定

DATABASES = {
    "default": {},
    "auth_db": {
        "NAME": "auth_db_name",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "swordfish",
    },
    "primary": {
        "NAME": "primary_name",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "spam",
    },
    "replica1": {
        "NAME": "replica1_name",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "eggs",
    },
    "replica2": {
        "NAME": "replica2_name",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "bacon",
    },
}

現在我們需要處理路由。首先,我們需要一個知道將 authcontenttypes 應用程式的查詢傳送到 auth_db 的路由(auth 模型連結到 ContentType,因此它們必須儲存在同一個資料庫中)

class AuthRouter:
    """
    A router to control all database operations on models in the
    auth and contenttypes applications.
    """

    route_app_labels = {"auth", "contenttypes"}

    def db_for_read(self, model, **hints):
        """
        Attempts to read auth and contenttypes models go to auth_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def db_for_write(self, model, **hints):
        """
        Attempts to write auth and contenttypes models go to auth_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return "auth_db"
        return None

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if a model in the auth or contenttypes apps is
        involved.
        """
        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):
        """
        Make sure the auth and contenttypes apps only appear in the
        'auth_db' database.
        """
        if app_label in self.route_app_labels:
            return db == "auth_db"
        return None

我們也需要一個將所有其他應用程式傳送到主要/複本組態,並隨機選擇一個複本進行讀取的路由

import random


class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        """
        Reads go to a randomly-chosen replica.
        """
        return random.choice(["replica1", "replica2"])

    def db_for_write(self, model, **hints):
        """
        Writes always go to primary.
        """
        return "primary"

    def allow_relation(self, obj1, obj2, **hints):
        """
        Relations between objects are allowed if both objects are
        in the primary/replica pool.
        """
        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):
        """
        All non-auth models end up in this pool.
        """
        return True

最後,在設定檔案中,我們新增以下內容(將 path.to. 替換為定義路由的模組的實際 Python 路徑)

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

處理路由的順序非常重要。將按照它們在 DATABASE_ROUTERS 設定中列出的順序查詢路由。在此範例中,AuthRouter 會在 PrimaryReplicaRouter 之前處理,因此,關於 auth 中模型的決策會在做出任何其他決策之前處理。如果 DATABASE_ROUTERS 設定以其他順序列出兩個路由,則會先處理 PrimaryReplicaRouter.allow_migrate()。PrimaryReplicaRouter 實作的全面性質意味著所有模型都可在所有資料庫上使用。

安裝此設定並根據 同步您的資料庫 遷移所有資料庫後,讓我們執行一些 Django 程式碼

>>> # This retrieval will be performed on the 'auth_db' database
>>> fred = User.objects.get(username="fred")
>>> fred.first_name = "Frederick"

>>> # This save will also be directed to 'auth_db'
>>> fred.save()

>>> # These retrieval will be randomly allocated to a replica database
>>> dna = Person.objects.get(name="Douglas Adams")

>>> # A new object has no database allocation when created
>>> mh = Book(title="Mostly Harmless")

>>> # This assignment will consult the router, and set mh onto
>>> # the same database as the author object
>>> mh.author = dna

>>> # This save will force the 'mh' instance onto the primary database...
>>> mh.save()

>>> # ... but if we re-retrieve the object, it will come back on a replica
>>> mh = Book.objects.get(title="Mostly Harmless")

此範例定義了一個路由來處理與 auth 應用程式中的模型互動,以及其他路由來處理與所有其他應用程式的互動。如果您將 default 資料庫留空,並且不想定義一個全面的資料庫路由來處理所有未另行指定的應用程式,則您的路由必須在您遷移之前處理 INSTALLED_APPS 中的所有應用程式名稱。有關必須在一個資料庫中一起使用的 contrib 應用程式的資訊,請參閱 Contrib 應用程式的行為

手動選擇資料庫

Django 還提供了一個 API,可讓您完全控制程式碼中的資料庫使用。手動指定的資料庫分配將優先於路由分配的資料庫。

手動選擇 QuerySet 的資料庫

您可以在 QuerySet 「鏈」中的任何點選擇 QuerySet 的資料庫。在 QuerySet 上呼叫 using() 以取得另一個使用指定資料庫的 QuerySet

using() 採用一個參數:您想要在其上執行查詢的資料庫的別名。例如

>>> # This will run on the 'default' database.
>>> Author.objects.all()

>>> # So will this.
>>> Author.objects.using("default")

>>> # This will run on the 'other' database.
>>> Author.objects.using("other")

save() 選擇資料庫

使用 using 關鍵字來 Model.save(),以指定應將資料儲存到哪個資料庫。

例如,若要將物件儲存到 legacy_users 資料庫,您可以使用這個

>>> my_object.save(using="legacy_users")

如果您未指定 using,則 save() 方法將會儲存到路由分配的預設資料庫中。

將物件從一個資料庫移到另一個資料庫

如果您已將實例儲存到一個資料庫,則可能會很想使用 save(using=...) 作為將實例遷移到新資料庫的方式。但是,如果您不採取適當的步驟,這可能會產生一些意想不到的後果。

考慮以下範例

>>> p = Person(name="Fred")
>>> p.save(using="first")  # (statement 1)
>>> p.save(using="second")  # (statement 2)

在陳述式 1 中,一個新的 Person 物件會儲存到 first 資料庫。此時,p 沒有主鍵,因此 Django 會發出 SQL INSERT 陳述式。這會建立主鍵,而 Django 會將該主鍵指派給 p

當儲存在陳述式 2 中時,p 已經有一個主鍵值,而 Django 會嘗試在新資料庫上使用該主鍵。如果 second 資料庫未使用該主鍵值,則您不會有任何問題,物件將會複製到新資料庫。

但是,如果 p 的主鍵已在 second 資料庫中使用,則當 p 儲存時,second 資料庫中的現有物件將會被覆寫。

您可以使用兩種方式避免此問題。首先,您可以清除實例的主鍵。如果物件沒有主鍵,Django 會將其視為新物件,避免在 second 資料庫上遺失任何資料

>>> p = Person(name="Fred")
>>> p.save(using="first")
>>> p.pk = None  # Clear the primary key.
>>> p.save(using="second")  # Write a completely new object.

第二個選項是使用 force_insert 選項來 save(),以確保 Django 執行 SQL INSERT

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

這將確保名為 Fred 的人會在兩個資料庫上都擁有相同的主鍵。如果您嘗試儲存到 second 資料庫時,該主鍵已在使用中,則會引發錯誤。

選擇要刪除的資料庫

預設情況下,呼叫刪除現有物件的操作會在最初檢索該物件時所使用的同一個資料庫上執行。

>>> u = User.objects.using("legacy_users").get(username="fred")
>>> u.delete()  # will delete from the `legacy_users` database

若要指定要從哪個資料庫刪除模型,請將 using 關鍵字參數傳遞給 Model.delete() 方法。這個參數的作用方式與 save()using 關鍵字參數相同。

例如,如果您要將使用者從 legacy_users 資料庫遷移到 new_users 資料庫,您可以使用這些命令:

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

搭配多個資料庫使用管理器

在管理器上使用 db_manager() 方法,讓管理器可以存取非預設的資料庫。

舉例來說,假設您有一個自訂的管理器方法會存取資料庫,例如 User.objects.create_user()。因為 create_user() 是管理器方法,而不是 QuerySet 方法,您不能使用 User.objects.using('new_users').create_user()。(create_user() 方法僅在 User.objects (管理器) 上可用,而不在衍生自管理器的 QuerySet 物件上可用。)解決方法是使用 db_manager(),如下所示:

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

db_manager() 會傳回管理器的複本,該複本會繫結至您指定的資料庫。

搭配多個資料庫使用 get_queryset()

如果您要覆寫管理器上的 get_queryset(),請務必呼叫父類別上的方法 (使用 super()),或對管理器上的 _db 屬性 (包含要使用的資料庫名稱的字串) 進行適當的處理。

例如,如果您想從 get_queryset 方法傳回自訂的 QuerySet 類別,您可以這樣做:

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

在 Django 的管理介面中公開多個資料庫

Django 的管理介面沒有任何對多個資料庫的明確支援。如果您想為模型提供管理介面,該模型位於路由器鏈指定的資料庫之外,您需要編寫自訂的 ModelAdmin 類別,以引導管理介面使用特定的資料庫來處理內容。

ModelAdmin 物件具有下列方法,這些方法需要自訂才能支援多個資料庫:

class MultiDBModelAdmin(admin.ModelAdmin):
    # A handy constant for the name of the alternate database.
    using = "other"

    def save_model(self, request, obj, form, change):
        # Tell Django to save objects to the 'other' database.
        obj.save(using=self.using)

    def delete_model(self, request, obj):
        # Tell Django to delete objects from the 'other' database
        obj.delete(using=self.using)

    def get_queryset(self, request):
        # Tell Django to look for objects on the 'other' database.
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        # Tell Django to populate ForeignKey widgets using a query
        # on the 'other' database.
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        # Tell Django to populate ManyToMany widgets using a query
        # on the 'other' database.
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

此處提供的實作會實作多資料庫策略,其中給定類型的所有物件都儲存在特定的資料庫中 (例如,所有的 User 物件都位於 other 資料庫中)。如果您的多資料庫使用方式更複雜,您的 ModelAdmin 將需要反映該策略。

InlineModelAdmin 物件可以以類似的方式處理。它們需要三個自訂的方法:

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

    def get_queryset(self, request):
        # Tell Django to look for inline objects on the 'other' database.
        return super().get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        # Tell Django to populate ForeignKey widgets using a query
        # on the 'other' database.
        return super().formfield_for_foreignkey(
            db_field, request, using=self.using, **kwargs
        )

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        # Tell Django to populate ManyToMany widgets using a query
        # on the 'other' database.
        return super().formfield_for_manytomany(
            db_field, request, using=self.using, **kwargs
        )

一旦您編寫好模型管理員定義,就可以將它們註冊到任何 Admin 實例:

from django.contrib import admin


# Specialize the multi-db admin objects for use with specific models.
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)

這個範例會設定兩個管理網站。在第一個網站上,會公開 AuthorPublisher 物件;Publisher 物件有一個表格內嵌,會顯示該發行商出版的書籍。第二個網站僅公開發行商,不包含內嵌。

搭配多個資料庫使用原始游標

如果您使用多個資料庫,您可以使用 django.db.connections 來取得特定資料庫的連線 (和游標)。django.db.connections 是一個類似字典的物件,可讓您使用別名來擷取特定的連線:

from django.db import connections

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

多個資料庫的限制

跨資料庫關聯

Django 目前不提供任何跨多個資料庫的外鍵或多對多關係的支援。如果您已使用路由器將模型分割到不同的資料庫,則這些模型定義的任何外鍵和多對多關係都必須在單一資料庫內部。

這是因為參考完整性。為了維護兩個物件之間的關係,Django 需要知道相關物件的主鍵是否有效。如果主鍵儲存在單獨的資料庫中,則無法輕易評估主鍵的有效性。

如果您使用 Postgres、SQLite、Oracle 或搭配 InnoDB 的 MySQL,則這會在資料庫完整性層級強制執行 – 資料庫層級的索引鍵限制會阻止建立無法驗證的關係。

但是,如果您使用搭配 MyISAM 資料表的 MySQL,則沒有強制執行的參考完整性;因此,您或許可以「偽造」跨資料庫外鍵。但是,這種組態並非 Django 官方支援。

contrib 應用程式的行為

數個 contrib 應用程式包含模型,而某些應用程式則依賴其他應用程式。由於跨資料庫關係是不可能的,這會對您如何跨資料庫分割這些模型產生一些限制。

  • 在給定適當的路由器下,contenttypes.ContentTypesessions.Sessionsites.Site 中的每一個都可以儲存在任何資料庫中。

  • auth 模型 — UserGroupPermission — 會連結在一起,並連結到 ContentType,因此它們必須與 ContentType 儲存在同一個資料庫中。

  • admin 依賴 auth,因此它的模型必須與 auth 位於同一個資料庫中。

  • flatpagesredirects 依賴 sites,因此它們的模型必須與 sites 位於同一個資料庫中。

此外,有些物件會在 migrate 建立表格以將它們保留在資料庫中之後自動建立:

  • 預設的 Site

  • 每個模型 (包括未儲存在該資料庫中的模型) 的 ContentType

  • 每個模型 (包括未儲存在該資料庫中的模型) 的 Permission

對於具有多個資料庫的常見設定,在多個資料庫中具有這些物件是沒有用的。常見的設定包括主要/複本和連線到外部資料庫。因此,建議編寫一個 資料庫路由器,以允許將這三個模型同步到只有一個資料庫。對於不需要在多個資料庫中擁有表格的 contrib 和第三方應用程式,請使用相同的方法。

警告

如果您要將內容類型同步到多個資料庫,請注意它們的主鍵在不同的資料庫中可能不符。這可能會導致資料損毀或資料遺失。

回到頂端