多个数据库 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/db/multi-db
跳转至:导航、​搜索

多个数据库

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

也可以看看

有关使用多个数据库进行测试的信息,请参阅 多数据库支持 [X30X]。


定义您的数据库

在 Django 中使用多个数据库的第一步是告诉 Django 您将使用的数据库服务器。 这是使用 :setting:`DATABASES` 设置完成的。 此设置将数据库别名(这是在整个 Django 中引用特定数据库的一种方式)映射到该特定连接的设置字典。 内部词典中的设置在 :setting:`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 数据库条目,但如果不使用,参数字典可以留空。 为此,您必须为所有应用程序的模型设置 :setting:`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'
    }
}

如果你试图访问一个你没有在你的 :setting:`DATABASES` 设置中定义的数据库,Django 会抛出一个 django.utils.connection.ConnectionDoesNotExist 异常。


同步您的数据库

:djadmin:`migrate` 管理命令一次操作一个数据库。 默认情况下,它在 default 数据库上运行,但是通过提供 --database 选项,您可以告诉它同步不同的数据库。 因此,要将所有模型同步到上面第一个示例中的所有数据库,您需要调用:

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

如果您不希望每个应用程序都同步到一个特定的数据库,您可以定义一个 数据库路由器 来实现一个策略来限制特定模型的可用性。

如果,如上面的第二个示例,您将 default 数据库留空,则每次运行 :djadmin:`migrate` 时都必须提供一个数据库名称。 省略数据库名称会引发错误。 对于第二个例子:

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

使用其他管理命令

大多数其他与数据库交互的 django-admin 命令的操作方式与 :djadmin:`migrate` 相同——它们一次只操作一个数据库,使用 --database ] 来控制所使用的数据库。

此规则的一个例外是 :djadmin:`makemigrations` 命令。 在创建新迁移之前,它会验证数据库中的迁移历史记录以发现现有迁移文件的问题(这可能是由编辑它们引起的)。 默认情况下,它只检查 default 数据库,但如果安装了 routers,它会参考 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

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

:djadmin:`makemigrations` 始终为模型更改创建迁移,但如果 allow_migrate() 返回 False,则将静默跳过 model_name 的任何迁移操作在 db 上运行 :djadmin:`migrate` 时。 为已经有迁移的模型更改 allow_migrate() 的行为可能会导致外键损坏、额外表或丢失表。 当 :djadmin:`makemigrations` 验证迁移历史时,它会跳过不允许应用迁移的数据库。

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

提示

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

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


使用路由器

使用 :setting:`DATABASE_ROUTERS` 设置安装数据库路由器。 这个设置定义了一个类名列表,每个类名指定了一个应该被主路由器使用的路由器(django.db.router)。

Django 的数据库操作使用主路由器来分配数据库使用。 每当查询需要知道要使用哪个数据库时,它就会调用主路由器,提供模型和提示(如果可用)。 然后 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_dbauth 模型链接到 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']

处理路由器的顺序很重要。 路由器将按照它们在 :setting:`DATABASE_ROUTERS` 设置中列出的顺序进行查询。 在本例中,AuthRouterPrimaryReplicaRouter 之前处理,因此,在做出任何其他决定之前,先处理与 auth 中的模型有关的决定。 如果 :setting:`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 数据库留空,并且不想定义一个包罗万象的数据库路由器来处理所有未另行指定的应用程序,则您的路由器必须处理 :setting:` 中所有应用程序的名称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').all()

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

为 save() 选择数据库

使用 using 关键字到 Model.save() 指定数据应保存到哪个数据库。

例如,要将对象保存到 legacy_users 数据库,您可以使用以下命令:

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

如果不指定usingsave()方法将保存到路由器分配的默认数据库中。

将对象从一个数据库移动到另一个数据库

如果您已将一个实例保存到一个数据库中,则可能很想使用 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 数据库上使用,则 second 数据库中的现有对象将在 p 被覆盖时保存。

您可以通过两种方式避免这种情况。 首先,您可以清除实例的主键。 如果一个对象没有主键,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 属性进行适当的处理manager(包含要使用的数据库名称的字符串)。

例如,如果你想从 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、Oracle 或 MySQL 与 InnoDB 一起使用,则这是在数据库完整性级别强制执行的 - 数据库级别键约束可防止创建无法验证的关系。

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


贡献应用程序的行为

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

  • contenttypes.ContentTypesessions.Sessionsites.Site 中的每一个都可以存储在任何数据库中,给定合适的路由器。
  • auth 模型 — UserGroupPermission — 链接在一起并链接到 ContentType,因此它们必须存储在相同的数据库为 ContentType
  • admin依赖于auth,所以它的模型必须和auth在同一个数据库中。
  • flatpagesredirects依赖于sites,所以它们的模型必须和sites在同一个数据库中。

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

  • 默认 Site
  • 每个模型的 ContentType(包括未存储在该数据库中的模型),
  • 每个模型的 Permission(包括未存储在该数据库中的模型)。

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

警告

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