Skip to main content

多个数据库

本主题指南介绍了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.db.utils.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() 方法(如果有)。

Changed in Django 1.10:

添加了迁移一致性检查。在1.10.1中添加了基于数据库路由器的检查。

自动数据库路由

使用多个数据库的最简单的方法是设置数据库路由方案。默认路由方案确保对象保持对其原始数据库的“粘性”(即,从 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。这是纯粹的验证操作,由外键和许多操作使用以确定是否应允许两个对象之间的关系。

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 验证迁移历史记录时,它会跳过不允许应用程序迁移的数据库。

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

提示

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

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

使用路由器

数据库路由器使用 DATABASE_ROUTERS 设置安装。此设置定义类名称列表,每个类名称指定应由主路由器(django.db.router)使用的路由器。

主路由器由Django的数据库操作用于分配数据库使用。每当查询需要知道使用哪个数据库时,它会调用主路由器,提供一个模型和提示(如果可用)。 Django然后依次尝试每个路由器,直到可以找到数据库建议。如果没有找到建议,它尝试提示实例的当前 _state.db。如果未提供提示实例,或实例当前没有数据库状态,则主路由器将分配 default 数据库。

一个例子

仅供参考!

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

如果 myapp 中的任何模型包含与 other 数据库外部的模型的关系,则此示例将不起作用。 跨数据库关系 引入了Django当前无法处理的参照完整性问题。

所描述的主/副本(被一些数据库称为主/副本)配置也是有缺陷的 - 它不提供用于处理复制滞后的任何解决方案(即,由于写传播所花费的时间而引入的查询不一致复制)。它也不考虑事务与数据库利用率策略的交互。

所以,这是什么意思在实践中?让我们考虑另一个示例配置。这将有几个数据库:一个用于 auth 应用程序和所有其他应用程序使用主/副本设置与两个只读副本。以下是指定这些数据库的设置:

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

现在我们需要处理路由。首先,我们需要一个知道将 auth 应用程序的查询发送到 auth_db 的路由器:

class AuthRouter(object):
    """
    A router to control all database operations on models in the
    auth application.
    """
    def db_for_read(self, model, **hints):
        """
        Attempts to read auth models go to auth_db.
        """
        if model._meta.app_label == 'auth':
            return 'auth_db'
        return None

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

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if a model in the auth app is involved.
        """
        if obj1._meta.app_label == 'auth' or \
           obj2._meta.app_label == 'auth':
           return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Make sure the auth app only appears in the 'auth_db'
        database.
        """
        if app_label == 'auth':
            return db == 'auth_db'
        return None

我们还需要一个路由器,将所有其他应用程序发送到主/副本配置,并随机选择要从中读取的副本:

import random

class PrimaryReplicaRouter(object):
    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_list = ('primary', 'replica1', 'replica2')
        if obj1._state.db in db_list and obj2._state.db in db_list:
            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 数据库留空,并且不想定义一个全捕获数据库路由器来处理未另行指定的所有应用程序,则您的路由器必须在迁移之前处理 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 数据库中使用,则在保存 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.

第二个选项是使用 save()force_insert 选项来确保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(MultiDBModelAdmin, self).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(MultiDBModelAdmin, self).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(MultiDBModelAdmin, self).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(MultiDBTabularInline, self).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(MultiDBTabularInline, self).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(MultiDBTabularInline, self).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
cursor = connections['my_db_alias'].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 在同一个数据库中。

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

  • 默认 Site

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

  • 每个模型的三个 Permission (包括那些不存储在该数据库中的模型)。

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

警告

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