多数据库

这个主题指南描述了 Django 对多数据库交互的支持。大部分的 Django 文档假设你进行的是单数据库交互。如果你想多数据库交互,则需要执行一些其他步骤。

参见

查看 Multi-database support 获取关于多数据库测试的信息。

定义数据库

首先告知 Django,你正在使用至少2个数据库服务。通过 DATABASES 配置来将指定的数据库链接放入一个字典,以此来映射数据库别名,数据库别名是在整个Django中引用特定数据库的一种方式。

可以选择任意的数据库别名,但是``default`` 别名具有特殊意义。当没有数据库指定选择的时候,Django 使用带有 default 别名的数据库。

接下来一个 settings.py 片段,定义了2个数据库——默认的 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 数据库,但如果没有使用数据库的话,参数字典可以置空。这样,你必须为所有的模型,包括你所使用的任何 contrib 和第三方 app 设置 DATABASE_ROUTERS,所以不会有任何查询路由到默认数据库。下面示例来讲在默认数据库为空的情况下,如何定义两个非默认数据库:

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() method of routers

自动数据库路由

使用多数据库最简单的方式就是设置数据库路由方案。默认路由方案确保对象对原始数据库保持粘性(比如,从 foo 数据库检索到的对象将被保持到同一个数据库)。默认路由方案确保当数据库没有指定时,所有查询回退到 default 数据库。

你无需执行任何操作来激活默认路由——在每个 Django 项目上是开箱即用的。然而,如果想实现更多有趣的数据库分配行为,可以定义和安装自己的数据库路由。

数据库路由

数据库路由是一个类,它提供四种方法:

db_for_read(model, **hints)

建议用于读取“模型”类型对象的数据库。

如果数据库操作可以提供有助于选择数据库的任何附加信息,它将在 hints 中提供。这里 below 提供了有效提示的详细信息。

如果没有建议,则返回 None

db_for_write(model, **hints)

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

如果数据库操作可以提供有助于选择数据库的任何附加信息,它将在 hints 中提供。这里 below 提供了有效提示的详细信息。

如果没有建议,则返回 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_namehints 通常包含 'model' 下的模型类。注意它可能是 historical model ,因此没有任何自定义属性,方法或管理器。你应该只能依赖 _meta

这个方法也可以用于确定给定数据库上模型的可用性。

makemigrations 会在模型变动时创建迁移,但如果 allow_migrate() 返回 False` ,任何针对 ``model_name 的迁移操作会在运行 migrate 的时候跳过。对于已经迁移过的模型,改变 allow_migrate() 的行为,可能会破坏主键,格外表或丢失的表。当 makemigrations 核实迁移历史,它跳过不允许迁移的 app 的数据库。

路由不是必须提供所有这些方法——它也许省略它们中的一个或多个。如果某个方法被省略,Django会在执行相关检查时候,跳过这个路由。

提示

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

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

使用路由

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

Django 的数据库操作使用主路由来分配数据库使用。每当查询需要知道正在使用哪个数据库时,它会调用主路由,提供一个模型和提示(如果可用的话),然后 Django 会依次尝试每个路由直到找到数据库。如果没有找到,它试着访问提示实例的当前 _state.db。如果没有提供提示实例,或者实例没有当前数据库状态,主路由将分配默认数据库。

一个例子

仅供参考!

这个例子旨在演示如何使用路由基础结构来改变数据库使用情况。它有意忽略一些复杂的问题,为了演示如何使用路由。

如果 myapp 中的任何模型包含与其他数据库之外的模型的关系,那么这个例子将无法运行。Cross-database relationships 介绍了 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',
    },
}

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_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

最后,在配置文件中,我们添加下面的代码(用定义路由器的模块的实际 Python 路径替换 path.to. ):

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。手工指定数据库分配将优先于路由分配的数据库。

手动为查询集选择数据库

你可以在查询集链的任一点为查询集选择数据库。只要调用查询集上的 using() 就可以获取使用指定数据库的其他查询集。

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()

为保存选择数据库

使用 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.

第二个办法就是使用 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() 是一样的。

例如,如果你正在从 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 当前不提供对跨多数据库的外键或多对多关系任何支持。如果已经使用路由来分隔模型到不同数据库,那么通过这些模型来定义的任何外键和多对多关系必须在单一数据库内。

这是因为参照完整性。为了维护两个对象之间的关系,Djagno 需要知道这个相关对象的外键是否是合法的。如果外键被保存在单独的数据库上,则无法轻松评价外键的合法性。

如果你正在使用 Postgres,Oracle,或支持 InnoDB 的 MySQL,这是在数据库完整性级别上强制执行的——数据库级别的键约束防止创建无法验证的关系。

然而,如果你正在使用 SQLite 或支持 MyISAM 表的MySQL,这就不会强制参照完整性;因此,你可以伪造跨数据库的外键。尽管 Django 并没有正式支持这个设置。

contrib应用程序的行为

一些贡献应用包括模型,一些应用依赖于其他应用。 由于跨数据库关系是不可能的,因此这会对如何跨数据库拆分这些模型产生一些限制:

  • 在给定合适的路由器的情况下,contenttypes.ContentType``sessions.Session``和``sites.Site``中的每一个都可以存储在任何数据库中。
  • auth``模型 - ``UserGroup``和``Permission - 链接在一起并链接到``ContentType``,因此它们必须与``ContentType存储在同一个数据库中``。
  • admin``依赖于``auth,所以它的模型必须和``auth``在同一个数据库中。
  • flatpages``和``redirects``依赖于``sites,所以他们的模型必须和``sites``在同一个数据库中。

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

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

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

警告

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