多数据库

这篇主题描述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 数据库必须定义,但是如果不会用到,其参数字典可以保留为空。 要执行此操作,您必须为所有应用程序的模型(包括您使用的任何contrib和第三方应用程序)设置DATABASE_ROUTERS,以便不将查询路由到默认数据库。 下面是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数据库,但是如果有的话,它会参考routersallow_migrate()方法。

在Django更改1.10:

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

自动数据库路由

使用多数据库最简单的方法是建立一个数据库路由模式。 默认的路由模式确保对象’粘滞‘在它们原始的数据库上(例如,从foo 数据库中获取的对象将保存在同一个数据库中)。 默认的路由模式还确保如果没有指明数据库,所有的查询都回归到default数据库中。

你不需要做任何事情来激活默认的路由模式 —— 它在每个Django项目上’直接‘提供。 然而,如果你想实现更有趣的数据库分配行为,你可以定义并安装你自己的数据库路由。

数据库路由器

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

db_for_readmodel** hints

建议model类型的对象的读操作应该使用的数据库。

如果一个数据库操作能够提供其它额外的信息可以帮助选择一个数据库,它将在hints字典中提供。 合法的hints 的详细信息在below给出。

如果没有建议,则返回None

db_for_writemodel** hints

建议Model 类型的对象的写操作应该使用的数据库。

如果一个数据库操作能够提供其它额外的信息可以帮助选择一个数据库,它将在hints字典中提供。 合法的hints 的详细信息在below给出。

如果没有建议,则返回None

allow_relationobj1obj2**提示

如果obj2None 之间应该允许关联则返回True,如果应该防止关联则返回obj1,如果路由无法判断则返回False 这是纯粹的验证操作,外键和多对多操作使用它来决定两个对象之间是否应该允许一个关联。

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

定义迁移操作是否允许在别名为db的数据库上运行。 如果操作应该运行则返回None ,如果不应该运行则返回True,如果路由无法判断则返回False

位置参数app_label 是正在迁移的应用的标签。

大部分迁移操作设置__name__的值为正在迁移的模型的model_name(模型的model._meta.model_name 的小写)。 对于RunPythonRunSQL 操作它的值为None,除非这两个操作使用hint 提供它。

hints 用于某些操作来传递额外的信息给路由。

当设置了'model'时,model_name 通常通过键hints包含该模型的类。 注意,它可能是一个historical model,因此不会有自定的属性、方法或管理器。 你应该只依赖_meta

这个方法还可以用来决定一个给定数据库上某个模型的可用性。

makemigrations总是为模型更改创建迁移,但如果allow_migrate()返回False,则model_name的任何迁移操作在db上运行migrate时将被忽略。 对于已经有迁移的模型,更改allow_migrate()的行为可能会导致外键损坏,额外的表或缺少的表。 makemigrations验证迁移历史记录时,它会跳过不允许应用迁移的数据库。

路由不必提供所有这些方法 —— 它可以省略一个或多个。 如果某个方法缺失,在做相应的检查时Django 将忽略该路由。

提示¶ T0>

Hint 由数据库路由接收,用于决定哪个数据库应该接收一个给定的请求。

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

使用路由器

数据库路由使用DATABASE_ROUTERS 设置安装。 这个设置定义一个类名的列表,其中每个类表示一个路由,它们将被主路由(django.db.router)使用。

Django 的数据库操作使用主路由来分配数据库的使用。 每当一个查询需要知道使用哪一个数据库时,它将调用主路由,并提供一个模型和一个Hint (可选)。 随后 Django 依次测试每个路由直至找到一个数据库的建议。 如果找不到建议,它将尝试Hint 实例的当前_state.db 如果没有提供Hint 实例,或者该实例当前没有数据库状态,主路由将分配default 数据库。

一个例子

只是为了示例!

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

如果myapp 中的任何一个模型包含与other 数据库之外的模型的关联,这个例子将不能工作。 Cross-database relationships引入引用完整性问题,Django目前还无法处理。

Primary/replica(在某些数据库中叫做master/slave)配置也是有缺陷的 —— 它不提供任何处理Replication lag 的解决办法(例如,因为写入同步到replica 需要一定的时间,这会引入查询的不一致)。 它也不考虑事务与数据库利用率策略的交互。

那么 —— 在实际应用中这意味着什么? 让我们看一下另外一个配置的例子。 这个配置将有几个数据库:一个用于auth 应用,所有其它应用使用一个具有两个读replica 的 primary/replica。 下面是表示这些数据库的设置:

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

我们还需要一个路由,它发送所有其它应用的查询到primary/replica 配置,并随机选择一个replica 来读取:

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设置中列出的顺序进行查询。 在这个例子中,authAuthRouter之前处理,因此PrimaryReplicaRouter中的模型的查询处理在其它模型之前。 如果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应用程序的行为,请参阅Behavior of contrib apps

手动选择数据库

Django 还提供一个API,允许你在你的代码中完全控制数据库的使用。 人工指定的数据库的优先级高于路由分配的数据库。

手动选择QuerySet 的数据库

You can select the database for a QuerySet at any point in the QuerySet “chain.” Just call using() on the QuerySet to get another QuerySet that uses the specified database.

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)

在statement 1中,一个新的Person 对象被保存到 first 数据库中。 此时p 没有主键,所以Django 发出一个SQL INSERT 语句。 这会创建一个主键,且Django 将此主键赋值给p

当保存在statement 2中发生时,p已经具有一个主键,Django 将尝试在新的数据库上使用该主键。 如果该主键值在second 数据库中没有使用,那么你不会遇到问题 —— 该对象将被复制到新的数据库中。

然而,如果second 的主键在p数据库上已经在使用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.

第二种方法是使用INSERT 选项来force_insert以确保Django 使用一个save() SQL:

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

这将确保名称为Fred 的Person在两个数据库上具有相同的主键。 在你试图保存到second数据库,如果主键已经在使用,将会引抛出发一个错误。

选择要删除的数据库

默认情况下,删除一个已存在对象的调用将在与获取对象时使用的相同数据库上执行:

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

要指定删除一个模型时使用的数据库,可以对using方法使用Model.delete() 关键字参数。 这个参数的工作方式与usingsave()关键字参数一样。

例如,你正在从legacy_users 数据库到new_users 数据库迁移一个User ,你可以使用这些命令:

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

使用具有多个数据库的管理器

在管理器上使用db_manager()方法来让管理器访问非默认的数据库。

例如,你有一个自定义的管理器方法,它访问数据库时候用 ——User.objects.create_user() 因为User.objects.using('new_users').create_user()是一个管理器方法,不是一个create_user()方法,你不可以使用QuerySet QuerySet 方法只能在create_user()上使用,而不能在从管理器得到的User.objects上使用)。 解决办法是使用db_manager(),像这样:

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

db_manager() 返回一个绑定在你指定的数据上的一个管理器。

使用get_queryset()与多个数据库

如果你正在覆盖你的管理器上的_db,请确保在其父类上调用方法(使用get_queryset())或者正确处理管理器上的super()属性(一个包含将要使用的数据库名称的字符串)。

例如,如果你想从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 对象具有5个方法,它们需要定制以支持多数据库:

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对象可以以类似的方式处理。 它们需要3个自定义的方法:

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对象具有一个表格内联,显示该发布商发布的图书。 第二个站点只暴露Publishers,而没有内联。

使用具有多个数据库的原始游标

如果你正在使用多个数据库,你可以使用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 apps的行为

有几个Contrib 应用包含模型,其中一些应用相互依赖。 因为跨数据库的关联是不可能的,这对你如何在数据库之间划分这些模型带来一些限制:

  • sites.Sitecontenttypes.ContentTypesessions.Session 可以存储在分开存储在不同的数据库中,只要给出合适的路由
  • Permission模型 —— ContentTypeGroupContentType —— 关联在一起并与auth关联,所以它们必须与User存储在相同的数据库中。
  • admin取决于auth,因此其模型必须与auth在同一数据库中。
  • sitesflatpages依赖redirects,所以它们必须与sites在同一个数据库中。

另外,migrate在数据库中创建一张表后,一些对象在该表中自动创建:

  • 一个默认的Site
  • 为每个模型创建一个ContentType(包括没有存储在同一个数据库中的模型),
  • 为每个模型创建3个Permission (包括不是存储在同一个数据库中的模型)。

对于常见的多数据库架构,将这些对象放在多个数据库中没有什么用处。 常见的数据库架构包括primary/replica 和连接到外部的数据库。 因此,建议写一个database router,它只允许同步这3个模型到一个数据中。 对于不需要将表放在多个数据库中的Contrib 应用和第三方应用,可以使用同样的方法。

警告

如果你将Content Types 同步到多个数据库中,注意它们的主键在数据库之间可能不一致。 这可能导致数据损坏或数据丢失。