多个数据库¶
本主题指南介绍了 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.utils.connection.ConnectionDoesNotExist
异常。
同步您的数据库¶
migrate
管理命令一次在一个数据库上操作。默认情况下,它在 default
数据库上操作,但是通过提供 --database
选项,您可以告诉它同步不同的数据库。因此,要将所有模型同步到上面第一个示例中的所有数据库,您需要调用
$ ./manage.py migrate
$ ./manage.py migrate --database=users
如果您不希望每个应用程序都同步到特定数据库,您可以定义一个 数据库路由器,该路由器实现限制特定模型可用性的策略。
如果,如上面的第二个示例中所示,您已将 default
数据库留空,则必须在每次运行 migrate
时提供数据库名称。省略数据库名称将引发错误。对于第二个示例
$ ./manage.py migrate --database=users
$ ./manage.py migrate --database=customers
使用其他管理命令¶
大多数其他与数据库交互的 django-admin
命令的工作方式与 migrate
相同——它们一次只在一个数据库上操作,使用 --database
来控制使用的数据库。
此规则的一个例外是 makemigrations
命令。它验证数据库中的迁移历史记录,以便在创建新迁移之前捕获现有迁移文件的问题(这可能是由编辑它们引起的)。默认情况下,它只检查 default
数据库,但如果安装了任何 路由器,它会咨询其 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)¶
如果应允许
obj1
和obj2
之间的关联,则返回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__
的小写版本),该模型正在迁移。对于RunPython
和RunSQL
操作,除非它们使用提示提供值,否则其值为None
。hints
用于某些操作向路由器传达附加信息。当设置
model_name
时,hints
通常在键'model'
下包含模型类。请注意,它可能是历史模型,因此可能没有任何自定义属性、方法或管理器。你应该只依赖_meta
。此方法也可用于确定给定数据库上模型的可用性。
makemigrations
始终为模型更改创建迁移,但如果allow_migrate()
返回False
,则在db
上运行migrate
时,model_name
的任何迁移操作都将被静默跳过。更改已具有迁移的模型的allow_migrate()
的行为可能会导致外键损坏、额外表或缺失表。当makemigrations
验证迁移历史记录时,它会跳过不允许任何应用程序迁移的数据库。
路由器不必提供所有这些方法——它可以省略其中一个或多个方法。如果省略其中一种方法,Django 将在执行相关检查时跳过该路由器。
提示¶
数据库路由器收到的提示可用于确定哪个数据库应接收给定的请求。
目前,唯一提供的提示是instance
,这是一个与正在进行的读或写操作相关的对象实例。这可能是正在保存的实例,也可能是正在许多对多关系中添加的实例。在某些情况下,根本不会提供任何实例提示。路由器检查实例提示是否存在,并确定是否应使用该提示来更改路由行为。
使用路由器¶
数据库路由器使用DATABASE_ROUTERS
设置进行安装。此设置定义了一个类名列表,每个类名都指定了基路由器(django.db.router
)应使用的路由器。
Django 的数据库操作使用基路由器来分配数据库使用情况。每当查询需要知道使用哪个数据库时,它都会调用基路由器,提供模型和提示(如果可用)。基路由器依次尝试每个路由器类,直到一个返回数据库建议。如果没有路由器返回建议,基路由器将尝试提示实例的当前instance._state.db
。如果没有提供提示实例,或instance._state.db
为None
,基路由器将分配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",
},
}
现在我们需要处理路由。首先,我们希望一个路由器知道将auth
和contenttypes
应用程序的查询发送到auth_db
(auth
模型链接到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
最后,在 settings 文件中,我们添加以下内容(将path.to.
替换为定义路由器的模块的实际 Python 路径)
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,允许你在代码中完全控制数据库使用情况。手动指定的数据库分配将优先于路由器分配的数据库。
手动为QuerySet
选择数据库¶
你可以在QuerySet
“链”中的任何点选择QuerySet
的数据库。在QuerySet
上调用using()
以获取另一个使用指定数据库的QuerySet
。
using()
接受一个参数:要在其上运行查询的数据库的别名。例如
>>> # This will run on the 'default' database.
>>> Author.objects.all()
>>> # So will this.
>>> Author.objects.using("default")
>>> # This will run on the 'other' database.
>>> Author.objects.using("other")
为save()
选择数据库¶
使用using
关键字到Model.save()
以指定应将数据保存到哪个数据库。
例如,要将对象保存到legacy_users
数据库,可以使用以下代码
>>> my_object.save(using="legacy_users")
如果不指定using
,则save()
方法将保存到路由器分配的默认数据库中。
将对象从一个数据库移动到另一个数据库¶
如果已将实例保存到一个数据库,则可能很想使用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()
的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的admin界面中公开多个数据库¶
Django的admin没有对多个数据库的任何显式支持。如果要为路由器链以外的数据库上的模型提供admin界面,则需要编写自定义ModelAdmin
类,这些类将指示admin使用特定数据库的内容。
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定义后,可以将其注册到任何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)
此示例设置了两个admin站点。在第一个站点上,公开了Author
和Publisher
对象;Publisher
对象包含一个表格内联,显示该出版商出版的书籍。第二个站点仅公开出版商,不包含内联。
在多个数据库中使用原始游标¶
如果使用多个数据库,可以使用django.db.connections
获取特定数据库的连接(和游标)。django.db.connections
是一个类似字典的对象,允许您使用其别名检索特定连接。
from django.db import connections
with connections["my_db_alias"].cursor() as cursor:
...
多个数据库的限制¶
跨数据库关系¶
Django目前不提供任何跨越多个数据库的外部键或多对多关系的支持。如果使用路由器将模型划分到不同的数据库,则这些模型定义的任何外部键和多对多关系必须位于单个数据库内部。
这是因为引用完整性。为了维护两个对象之间的关系,Django需要知道相关对象的主键是否有效。如果主键存储在单独的数据库中,则无法轻松评估主键的有效性。
如果使用的是Postgres、SQLite、Oracle或带有InnoDB的MySQL,则会在数据库完整性级别强制执行此操作——数据库级别的键约束会阻止创建无法验证的关系。
但是,如果使用的是带有MyISAM表的MySQL,则没有强制执行引用完整性;因此,您可能能够“伪造”跨数据库外键。但是,Django官方不支持此配置。
contrib应用程序的行为¶
几个contrib应用程序包含模型,有些应用程序依赖于其他应用程序。由于跨数据库关系是不可能的,这会对如何在数据库之间拆分这些模型造成一些限制。
给定合适的路由器,
contenttypes.ContentType
、sessions.Session
和sites.Site
中的每一个都可以存储在任何数据库中。auth
模型——User
、Group
和Permission
——相互关联并与ContentType
关联,因此它们必须与ContentType
存储在同一个数据库中。admin
依赖于auth
,因此其模型必须与auth
存储在同一个数据库中。flatpages
和redirects
依赖于sites
,因此它们的模型必须与sites
位于同一个数据库中。
此外,在 migrate
创建数据库表以存储它们之后,会自动创建一些对象。
一个默认的
Site
对象;每个模型(包括那些未存储在该数据库中的模型)的
ContentType
对象;每个模型(包括那些未存储在该数据库中的模型)的
Permission
对象。
对于具有多个数据库的常见设置,在多个数据库中拥有这些对象并没有用处。常见的设置包括主/从复制和连接到外部数据库。因此,建议编写一个 数据库路由器,只允许将这三个模型同步到一个数据库。对于那些不需要在多个数据库中拥有其表的 contrib 和第三方应用程序,也使用相同的方法。
警告
如果您将内容类型同步到多个数据库,请注意它们的主键在数据库之间可能不匹配。这可能会导致数据损坏或数据丢失。