多个数据库

本主题指南介绍了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

使用其他管理命令

Most other django-admin commands that interact with the database operate in the same way as migrate – they only ever operate on one database at a time, using --database to control the database used.

这个规则的例外是makemigrations命令。 它会验证数据库中的迁移历史记录,以便在创建新的迁移之前捕获现有迁移文件(可能由于编辑它们而导致)的问题。 默认情况下,它只检查default数据库,但是它会查询routersallow_migrate()方法。

自动数据库路由

使用多个数据库最简单的方法是建立一个数据库路由方案。 默认的路由方案确保对象对原始数据库保持“粘性”(即,从foo数据库检索的对象将被保存在相同的数据库中)。 默认路由方案确保如果没有指定数据库,所有查询都会回到default数据库。

您无需执行任何操作即可激活默认的路由方案 - 每个Django项目都提供“开箱即用”功能。 但是,如果要实现更有趣的数据库分配行为,则可以定义和安装自己的数据库路由器。

数据库路由器

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

db_for_readmodel** hints

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

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

如果没有建议,则返回None

db_for_writemodel** hints

建议应该用于写入Model类型的对象的数据库。

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

如果没有建议,则返回None

allow_relationobj1obj2**提示

如果应该允许obj1obj2之间的关系,则返回True,如果应该阻止关系,则返回False如果路由器没有意见,那么无所谓None 这纯粹是一个验证操作,由外键和许多操作使用来确定是否允许两个对象之间的关系。

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

确定迁移操作是否允许在别名为db的数据库上运行。 如果该操作应该运行,则返回True;如果不应该运行,则返回False;如果路由器没有意见,返回None

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

model_name is set by most migration operations to the value of model._meta.model_name (the lowercased version of the model __name__) of the model being migrated. 对于RunPythonRunSQL操作,它的值是None,除非它们使用提示来提供它。

hints are used by certain operations to communicate additional information to the router.

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

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

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

A router doesn’t have to provide all these methods – it may omit one or more of them. 如果其中一个方法被省略,Django将在执行相关检查时跳过该路由器。

提示¶ T0>

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

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

使用路由器

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

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

一个例子

仅用于示例目的!

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

如果myapp中的任何模型包含与other数据库之外的模型的关系,则此示例将不起作用。 Cross-database relationships introduce referential integrity problems that Django can’t currently handle.

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

那么 - 这在实践中意味着什么? 让我们考虑另一个示例配置。 这将有几个数据库:一个用于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:
    """
    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:
    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设置中列出的顺序进行查询。 在这个例子中,在PrimaryReplicaRouter之前处理AuthRouter,因此,有关auth被制成。 If the DATABASE_ROUTERS setting listed the two routers in the other order, PrimaryReplicaRouter.allow_migrate() would be processed first. 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() takes a single argument: the alias of the database on which you want to run the query. 例如:

>>> # 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')

If you don’t specify using, the save() method will save into the default database allocated by the routers.

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

如果您已经将实例保存到一个数据库,那么使用save(using=...)作为将实例迁移到新数据库的方法可能是诱人的。 但是,如果你没有采取适当的措施,这可能会带来一些意想不到的后果。

考虑下面的例子:

>>> p = Person(name='Fred')
>>> p.save(using='first')  # (statement 1)
>>> p.save(using='second') # (statement 2)

在语句1中,一个新的Person对象被保存到first数据库中。 At this time, p doesn’t have a primary key, so Django issues an SQL INSERT statement. 这将创建一个主键,并且Django将该主键分配给p

When the save occurs in statement 2, p already has a primary key value, and Django will attempt to use that primary key on the new database. 如果主键值在second数据库中没有被使用,那么你将不会有任何问题 - 对象将被复制到新的数据库中。

However, if the primary key of p is already in use on the second database, the existing object in the second database will be overridden when p is saved.

你可以用两种方法来避免这种情况。 首先,您可以清除实例的主键。 如果一个对象没有主键,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

To specify the database from which a model will be deleted, pass a using keyword argument to the Model.delete() method. This argument works just like the using keyword argument to save().

例如,如果要将用户从legacy_users数据库迁移到new_users数据库,则可以使用以下命令:

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

使用具有多个数据库的经理

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

例如,假设您有一个触及数据库的自定义管理器方法 - User.objects.create_user() Because create_user() is a manager method, not a QuerySet method, you can’t do User.objects.using('new_users').create_user(). create_user()方法只能在管理器的User.objects上使用,而不能在从管理器派生的QuerySet对象上使用。 解决方法是使用db_manager(),如下所示:

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

db_manager() returns a copy of the manager bound to the database you specify.

对多个数据库使用get_queryset()

If you’re overriding get_queryset() on your manager, be sure to either call the method on the parent (using super()) or do the appropriate handling of the _db attribute on the manager (a string containing the name of the database to use).

例如,如果你想从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 objects have five methods that require customization for multiple-database support:

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 objects can be handled in a similar fashion. 他们需要三个定制的方法:

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)

这个例子设置了两个管理站点。 On the first site, the Author and Publisher objects are exposed; Publisher objects have a tabular inline showing books published by that publisher. 第二个网站只公开出版商,没有内联。

对多个数据库使用原始游标

如果您使用多个数据库,则可以使用django.db.connections来获取特定数据库的连接(和光标)。 django.db.connections是一个类似字典的对象,允许您使用其别名检索特定的连接:

from django.db import connections
cursor = connections['my_db_alias'].cursor()

多个数据库的限制

跨数据库关系

Django目前不提供对跨越多个数据库的外键或多对多关系的支持。 如果您已经使用路由器将模型分区到不同的数据库,则由这些模型定义的任何外键和多对多关系必须位于单个数据库的内部。

这是因为参照完整性。 为了维护两个对象之间的关系,Django需要知道相关对象的主键是有效的。 如果主键存储在单独的数据库中,则无法轻松评估主键的有效性。

如果您将Postgres,Oracle或MySQL与InnoDB一起使用,则会在数据库完整性级别强制执行 - 数据库级别的关键约束会阻止创建无法验证的关系。

但是,如果您在MyISAM表中使用SQLite或MySQL,则不存在强制的参照完整性;因此,您可能能够“伪造”跨数据库外键。 但是,这种配置并不是由Django官方支持的。

contrib应用程序的行为

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

  • 每一个contenttypes.ContentTypesessions.Sessionsites.Site都可以存储在任何数据库中,给出合适的路由器。
  • auth models — User, Group and Permission — are linked together and linked to ContentType, so they must be stored in the same database as ContentType.
  • admin依赖auth,所以它的模型必须和auth在同一个数据库中。
  • flatpages and redirects depend on sites, so their models must be in the same database as sites.

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

  • 一个默认的Site
  • 每个模型的一个ContentType(包括没有存储在那个数据库中的)
  • 每个模型的三个Permission(包括那些没有存储在那个数据库中的)。

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

警告

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