Multiple databases

本主题指南介绍了Django对与多个数据库交互的支持。Django其余的大部分文档都假设您正在与单个数据库交互。如果要与多个数据库交互,则需要采取一些其他步骤。

参见

多数据库支持 有关使用多个数据库进行测试的信息。

定义数据库

在django中使用多个数据库的第一步是告诉django您将要使用的数据库服务器。这是用 DATABASES 设置。此设置将数据库别名映射到特定连接的设置字典中,数据库别名是在Django中引用特定数据库的一种方法。内部字典中的设置在 DATABASES 文档。

数据库可以有您选择的任何别名。但是,别名 default 具有特殊意义。Django使用别名为的数据库 default 当没有选择其他数据库时。

下面是一个例子 settings.py 定义两个数据库的代码段——一个默认的PostgreSQL数据库和一个名为 users ::

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",
    },
}

如果A的概念 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

如果不希望每个应用程序都同步到特定的数据库上,可以定义 database router 它实现了一个限制特定模型可用性的策略。

如果像上面的第二个示例一样,您已经将 default 数据库为空,则必须在每次运行时提供数据库名称 migrate 。省略数据库名称将引发错误。对于第二个示例:

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

使用其他管理命令

大多数其他 django-admin 与数据库交互的命令的操作方式与 migrate --它们一次只能在一个数据库上操作,使用 --database 控制使用的数据库。

此规则的一个例外是 makemigrations 命令。它验证数据库中的迁移历史记录,以便在创建新迁移之前捕获现有迁移文件的问题(可能是由编辑这些文件引起的)。默认情况下,它只检查 default 数据库,但它查询 allow_migrate() 方法 routers 如果安装了。

自动数据库路由

使用多个数据库的最简单方法是设置数据库路由方案。默认路由方案确保对象与原始数据库保持“粘性”(即从 foo 数据库将保存在同一数据库中)。默认路由方案确保如果未指定数据库,则所有查询都返回到 default 数据库。

您不必做任何事情来激活默认的路由方案——它在每个Django项目中都是“开箱即用”的。但是,如果希望实现更有趣的数据库分配行为,可以定义和安装自己的数据库路由器。

数据库路由器

数据库路由器是一个提供最多四种方法的类:

db_for_read(model, **hints)

建议应用于类型为的对象的读取操作的数据库 model .

如果数据库操作能够提供任何可能有助于选择数据库的附加信息,则将在 hints 字典。提供了有效提示的详细信息 below .

返回 None 如果没有建议。

db_for_write(model, **hints)

建议用于写入类型为model的对象的数据库。

如果数据库操作能够提供任何可能有助于选择数据库的附加信息,则将在 hints 字典。提供了有效提示的详细信息 below .

返回 None 如果没有建议。

allow_relation(obj1, obj2, **hints)

返回 True 如果 obj1obj2 应该被允许, 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__ )正在迁移的模型。它的价值在于 None 对于 RunPythonRunSQL 操作,除非它们使用提示提供。

hints 被某些操作用来向路由器传递附加信息。

什么时候? model_name 已设置, hints 通常包含键下的模型类 'model' . 请注意,它可能是 historical model ,因此没有任何自定义属性、方法或管理器。你只能依靠 _meta .

此方法还可以用于确定给定数据库上模型的可用性。

makemigrations 总是为模型更改创建迁移,但如果 allow_migrate() 收益率 False ,的任何迁移操作 model_name 运行时将自动跳过 migratedb . 改变的行为 allow_migrate() 对于已经进行迁移的模型,可能会导致外键断开、额外的表或表丢失。什么时候? makemigrations 验证迁移历史记录,它跳过不允许任何应用程序迁移的数据库。

路由器不必提供 all 这些方法——它可以省略其中的一个或多个。如果省略其中一个方法,Django将在执行相关检查时跳过该路由器。

提示

数据库路由器接收到的提示可用于决定哪个数据库应接收给定的请求。

目前,将提供的唯一提示是 instance ,与正在进行的读或写操作相关的对象实例。这可能是正在保存的实例,也可能是正在添加到多对多关系中的实例。在某些情况下,根本不会提供实例提示。路由器检查是否存在实例提示,并确定是否应使用该提示来更改路由行为。

使用路由器

数据库路由器是使用 DATABASE_ROUTERS 布景。此设置定义类名的列表,每个类名指定基本路由器应使用的路由器 (django.db.router )。

Django的数据库操作使用基本路由器来分配数据库使用。每当查询需要知道要使用哪个数据库时,它都会调用基本路由器,提供模型和提示(如果可用)。基本路由器依次尝试每个路由器类别,直到其中一个返回数据库建议。如果没有路由器返回建议,则基本路由器会尝试当前 instance._state.db 提示实例的。如果未提供提示实例,则为 instance._state.dbNone ,则基本路由器将分配 default 数据库。

一个例子

仅用于示例目的!

此示例旨在演示如何使用路由器基础结构来更改数据库使用情况。它故意忽略一些复杂的问题,以便演示如何使用路由器。

如果在 myapp 包含与外部模型的关系 other 数据库。 Cross-database relationships 引入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",
    },
}

Now we'll need to handle routing. First we want a router that knows to send queries for the auth and contenttypes apps to auth_db (auth models are linked to ContentType, so they must be stored in the same database):

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 设置。在这个例子中, AuthRouterPrimaryReplicaRouter 因此,有关 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 数据库为空,不希望定义一个catch all数据库路由器来处理所有未指定的应用程序,路由器必须处理中所有应用程序的名称。 INSTALLED_APPS 在你迁移之前。见 Contrib应用程序的行为 有关必须在一个数据库中组合在一起的contrib应用程序的信息。

手动选择数据库

Django还提供了一个API,允许您在代码中维护对数据库使用的完全控制。手动指定的数据库分配将优先于由路由器分配的数据库。

手动选择数据库 QuerySet

您可以为 QuerySet 在任何时候 QuerySet “锁链”呼叫 using()QuerySet 得到另一个 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 , the 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 数据库中的现有对象 secondp 被保存。

您可以通过两种方式避免这种情况。首先,您可以清除实例的主键。如果对象没有主键,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() 方法。这个论点的作用就像 using 关键字参数 save() .

例如,如果要将用户从 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() . (The create_user() 方法仅在上可用 User.objects ,经理,不在 QuerySet 从管理器派生的对象。)解决方案是使用 db_manager() ,像这样:

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

db_manager() 返回绑定到指定数据库的管理器的副本。

使用 get_queryset() 有多个数据库

如果你要超越 get_queryset() 在您的管理器上,确保调用父级的方法(使用 super() )或者做适当的处理 _db 管理器上的属性(包含要使用的数据库名称的字符串)。

例如,如果要返回自定义 QuerySet 类从 get_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、Oracle或MySQL和InnoDB,那么这将在数据库完整性级别强制执行——数据库级别的键约束会阻止创建无法验证的关系。

但是,如果将sqlite或mysql与myisam表一起使用,则没有强制的引用完整性;因此,您可能能够“伪造”跨数据库的外键。然而,Django并未正式支持此配置。

Contrib应用程序的行为

一些Contrib应用程序包括模型,而一些应用程序依赖于其他应用程序。由于跨数据库关系是不可能的,这就对如何跨数据库拆分这些模型产生了一些限制:

  • 每一个 contenttypes.ContentTypesessions.Sessionsites.Site 可以存储在任何数据库中,给定一个合适的路由器。

  • auth 模型- UserGroupPermission -链接在一起并链接到 ContentType ,因此它们必须存储在与 ContentType .

  • admin 取决于 auth ,因此其模型必须与 auth .

  • flatpagesredirects 依靠 sites ,因此它们的模型必须与 sites .

此外,一些对象会在 migrate 创建一个表以将它们保存在数据库中:

  • 默认值 Site

  • ContentType 对于每个模型(包括未存储在该数据库中的模型),

  • 这个 Permission 对于每个模型(包括未存储在该数据库中的模型)。

对于具有多个数据库的常见设置,将这些对象放在多个数据库中并不有用。常见的设置包括主/副本和连接到外部数据库。因此,建议编写一个 database router 这允许将这三个模型同步到一个数据库。对于在多个数据库中不需要表的contrib和第三方应用程序,使用相同的方法。

警告

如果要将内容类型同步到多个数据库,请注意,它们的主键可能在数据库之间不匹配。这可能导致数据损坏或数据丢失。