使用多个数据库¶
大多数其他文档都假设使用单一数据库,本文主要讨论如何在 Django 中使用多个数据库。使用多个数据库,要增加一些步骤。
定义你的数据库¶
使用多数据库的第一步是通过 DATABASES 设置要使用的数据库服务。这个设置用于映射数据库别名和特定的联结设置字典,这是 Django 定义数据库一贯的手法。字典内部的设置参见 DATABASES 文档。
数据库可以使用任何别名,但是 default 有特殊意义。当没有选择其他数据库时, Django 总是使用别名为 default 的数据库。因此,如果你没有定义一个名为 default 的数据库时,你应当小心了,在使用数据库前要指定你想用的数据库。
以下是一个定义两个数据库的 settings.py 代码片断。定义了一个缺省的 PostgreSQL 数据库和一个名为 users 的 MySQL 数据库:
DATABASES = {
'default': {
'NAME': 'app_data',
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'USER': 'postgres_user',
'PASSWORD': 's3krit'
},
'users': {
'NAME': 'user_data',
'ENGINE': 'django.db.backends.mysql',
'USER': 'mysql_user',
'PASSWORD': 'priv4te'
}
}
如果你尝试访问 DATABASES 设置中没有定义的数据库, Django 会抛出一个 django.db.utils.ConnectionDoesNotExist 异常。
同步你的数据库¶
syncdb 管理命令一次只操作一个数据库。缺省情况下,它操作 default 数据库。但是加上 --database 参数,你可以让 syncdb 同步不同的数据库。所以要同步我们例子中的所有数据库的所有模型可以使用如下命令:
$ ./manage.py syncdb
$ ./manage.py syncdb --database=users
如果你不是同步所有的程序到同一个数据库中,你可定义一个 数据库路由 来为指定的模型实施特定的控制策略。
如果你要精细地控制同步,那么还有一种方式是修改 sqlall 的输出,手工在数据库中执行命令,命令如下:
$ ./manage.py sqlall sales | ./manage.py dbshell
使用其他管理命令¶
其他操作数据库的 django-admin.py 命令与 syncdb 类似,他们一次只操作一个数据库,使用 --database 来控制使用哪个数据库。
自动数据库路由¶
使用多数据库最简单的方法是设置一个数据库路由方案。缺省的路由方案确保对象 “紧贴”其原本的数据库(例如:一个对象从哪个数据库取得,就保存回哪个数据库)。缺省的路由方案还确保如果一个数据库没有指定,所有的查询都会作用于 缺省 数据库。
你不必为启动缺省路由方案作任何事,因为它是“开箱即用”的。但是,如果你要执行一些更有趣的数据库分配行为的话,你可以定义并安装你自己的数据库路由。
数据库路由¶
一个数据库路由是一个类,这个类最多有四个方法:
-
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 。这是一个纯粹的验证操作,用于外键和多对多操作中,两个对象的关系是否被允许。
-
allow_syncdb(
db,
model)
¶
-
决定 model 是否可以和 db 为别名的数据库同步。如果可以返回 True ,如果不可以返回 False ,或者没有意见时返回 None 。这个方法用于决定一个给定数据库的模型是否可用。
一个路由不必提供 所有 这些方法,可以省略其中一个或多个。如果其中一个方法被省略了,那么 Django 会在执行相关检查时跳过相应路由。
提示参数¶
数据库路由接收的“提示”参数可用于决定哪个数据库应当接收一个给定的请求。
目前,唯一可以提供的提示参数是 实例 ,即一个与读写操作相关的对象的实例。可以是一个已保存的对象的实例,也可以是一个多对多关系中添加的实例。在某些情况下,也可能没有对象的实例可以提供。路由会检查提示实例是否存在,并相应地决定是否改变路由行为。
使用路由¶
数据库路由使用 DATABASE_ROUTERS 设置来安装。这个设置定义一个类名称列表,每个类定义一个用于主路由 (django.db.router) 的路由。
主路由用于 Django 分配数据库操作。当一个查询想要知道使用哪个数据库时,会提供一个模型和一个提示(如果有的话),并调用主路由。 Django 就会按次序尝试每个路由,直到找到合适的路由建议。如果找不到路由建议就会尝试实例提示的当前的 _state.db 。如果没有提供路由提示,或者实例没有当前数据库状态,那么主路由会分配 缺省 数据库。
一个例子¶
仅用于示例目的!
这个例子仅用于展示路由如何改变数据库的使用。本例有意忽略了一些复杂的东西以便于更好的展示路由是如何工作的。
如果任何一个 myapp 中的模型包含与 另一个 数据库中模型的关系时,本例是无效的。参见 跨数据库关系 一节中介绍的 Django 引用完整性问题。
本例的主/从配置也是有缺陷的:它没有处理复制延时(比如因为把写操作传递给从数据库耗费时间而产生的查询不一致),也没有考虑与数据库使用策略的交互作用。
那么,这个例子有什么用呢?本例仅用于演示一个 myapp 存在于 other 数据库,所有其他模型之间是主/从关系,且存在于 master 、 slave1 和 slave2 数据库。本例使用了两个路由:
class MyAppRouter(object):
""" 一个控制 myapp 应用中模型的
所有数据库操作的路由 """
def db_for_read(self, model, **hints):
"myapp 应用中模型的操作指向 'other'"
if model._meta.app_label == 'myapp':
return 'other'
return None
def db_for_write(self, model, **hints):
"myapp 应用中模型的操作指向 'other'"
if model._meta.app_label == 'myapp':
return 'other'
return None
def allow_relation(self, obj1, obj2, **hints):
" 如果包含 myapp 应用中的模型则允许所有关系 "
if obj1._meta.app_label == 'myapp' or obj2._meta.app_label == 'myapp':
return True
return None
def allow_syncdb(self, db, model):
" 确保 myapp 应用只存在于 'other' 数据库 "
if db == 'other':
return model._meta.app_label == 'myapp'
elif model._meta.app_label == 'myapp':
return False
return None
class MasterSlaveRouter(object):
""" 一个设置简单主/从定义
的路由 """
def db_for_read(self, model, **hints):
" 所有读操作指向一个随机的从数据库 "
return random.choice(['slave1','slave2'])
def db_for_write(self, model, **hints):
" 所有写操作指向主数据库 "
return 'master'
def allow_relation(self, obj1, obj2, **hints):
" 允许数据库池中的两个对象间的任何关系 "
db_list = ('master','slave1','slave2')
if obj1._state.db in db_list and obj2._state.db in db_list:
return True
return None
def allow_syncdb(self, db, model):
" 显示地放置所有数据库中的模型 "
return True
然后在你的设置文件增加如下内容(把 path.to. 替换为你定义路由的模型的路径):
DATABASE_ROUTERS = ['path.to.MyAppRouter', 'path.to.MasterSlaveRouter']
这个设置中,路由的顺序是很重要的,因为查询时是按这个设置中的顺序依次查询的。上例中, MyAppRouter 先于 MasterSlaveRouter ,因此, myapp 中的模型就优先于其他模型。如果 DATABASE_ROUTERS 设置中两个路由的顺序变换了,那么 MasterSlaveRouter.allow_syncdb() 会优先执行。因为 MasterSlaveRouter 是包罗万象的,这样就会导致所有模型可以使用所有数据库。
设置好之后让我们来运行一些代码:
>>> # 从 'credentials' 数据库获得数据
>>> fred = User.objects.get(username='fred')
>>> fred.first_name = 'Frederick'
>>> # 保存到 'credentials' 数据库
>>> fred.save()
>>> # 随机从从数据库获得数据
>>> dna = Person.objects.get(name='Douglas Adams')
>>> # 新对象创建时还没有分配数据库
>>> mh = Book(title='Mostly Harmless')
>>> # 这个赋值会向路由发出请求,并把 mh 的数据库设置为与 author 对象同样的
>>> # 数据库
>>> mh.author = dna
>>> # 这会强制 'mh' 实例使用主数据库...
>>> mh.save()
>>> # ... 但如果我们重新获取对象,就会从从数据库中获取
>>> mh = Book.objects.get(title='Mostly Harmless')
手动选择数据库¶
Django 也提供一个可以让你通过代码完全控制数据库使用的 API 。手动定义数据库分配优先于路由。
为一个 查询集 手动选择一个数据库¶
你可以在 查询集 “链”中的任何点为 查询集 选择数据库。我们通过在 查询集 上调用 using() 来得到使用指定数据库的另一个 查询集 。
using() 使用一个参数:你想要运行查询的数据库的别名。例如:
>>> # 这会运行在“缺省”数据库上。
>>> Author.objects.all()
>>> # 这同样会运行在“缺省”数据库上。
>>> Author.objects.using('default').all()
>>> # 这会运行在“ other ”数据库上。
>>> Author.objects.using('other').all()
为 save() 选择一个数据库¶
在使用 Model.save() 时加上 using 关键字可以指定保存到哪个数据库。
例如,要把一个对象保存到 legacy_users 数据库应该这样做:
>>> my_object.save(using='legacy_users')
如果你不定义 using ,那么 save() 方法会根据路由分配把数据保存到缺省数据库中。
把一个对象从一个数据库移动到另一个数据库¶
当你已经在一个数据库中保存了一个对象后,你可能会使用 save(using=...) 把这个对象移动到另一个数据库中。但是,如果你没有使用恰当的方法,那么可能会出现意想不到的后果。
假设有如下的例子:
>>> p = Person(name='Fred')
>>> p.save(using='first') # (第一句)
>>> p.save(using='second') # (第二名)
在第一名中,一个新的 Person 对象被保存到 first 数据库中。这时, p 还没有一个主键,因此 Django 执行了一个 INSERT SQL 语句。这样就会创建一个主键,并将这个主键分配给 p 。
在第二句中,因为 p 已经有了一个主键,所以 Django 在保存对象时会尝试在新的数据库中使用这个主键。如果 second 数据库中没有使用这个主键,那就不会有问题,该对象会复制到新数据库。
然而,如果 p 的主键在 second 数据库中已经使用过了,那么 second 使用这个主键的已存在的对象将会被 p 覆盖。
有两种方法可以避免上述情况的发生。第一,你可以清除实例的主键。如果一个对象没有主主键,那么 Django 会把它看作一个新对象,在保存到 second 数据库中时就不会带来数据的损失:
>>> p = Person(name='Fred')
>>> p.save(using='first')
>>> p.pk = None # 清除主键。
>>> p.save(using='second') # 写入一个全新的对象。
第二种方法是在 save() 方法中使用 force_insert 选项来保证 Django 执行一个 INSERT SQL:
>>> 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() # 会从 `legacy_users` 数据库中删除
通过向 Model.delete() 方法传递 using 关键字参数可以定义在哪个数据库中删除数据。 using 的用法与 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() 是一个管理器方法,不是一个 查询集 ,所以你不能用 User.objects.using('new_users').create_user() 。( create_user() 方法只能用于 User.objects 管理器,而不能用于,管理器衍生出的 查询集 。)解决方法是使用 db_manager() ,就象下面这样:
User.objects.db_manager('new_users').create_user(...)
db_manager() 返回的是绑定到你指定的数据库的管理器的一个副本。
多数据库情况下使用 get_query_set()¶
如果你在管理器中重载了 get_query_set() ,请确保在其父类中也调用了相同的方法(使用 super() )或者正确处理管理器中的 _db 属性(一个包含要使用的数据库名称的字符串)。
例如,如果你要从 get_query_set 方法返回一个自定义 查询集 类,那么你可以这样做:
class MyManager(models.Manager):
def get_query_set(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):
# 为方便起见定义一个数据库名称常量。
using = 'other'
def save_model(self, request, obj, form, change):
# 让 Django 保存对象到 'other' 数据库。
obj.save(using=self.using)
def delete_model(self, request, obj):
# 让 Django 从 'other' 数据库中删除对象。
obj.delete(using=self.using)
def queryset(self, request):
# 让 Django 在 'other' 数据库中搜索对象。
return super(MultiDBModelAdmin, self).queryset(request).using(self.using)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
# 让 Django 基于 'other' 数据库生成外键控件。
return super(MultiDBModelAdmin, self).formfield_for_foreignkey(db_field, request=request, using=self.using, **kwargs)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
# 让 Django 基于 'other' 数据库生成多对多关系控件。
return super(MultiDBModelAdmin, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
以上代码实现了一个多数据库策略:所有给定类型的对象全部储存到一个指定的数据库(如所有 User 对象都在 other 数据库中)。如果你的多数据库应用更复杂,你就需要自定义更复杂的 ModelAdmin 。
内联也可以按上面类似的方式处理。内联处理需要三个自定义方法:
class MultiDBTabularInline(admin.TabularInline):
using = 'other'
def queryset(self, request):
# 让 Django 在 'other' 数据库中搜索内联对象。
return super(MultiDBTabularInline, self).queryset(request).using(self.using)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
# 让 Django 基于 'other' 数据库生成外键控件。
# on the 'other' database.
return super(MultiDBTabularInline, self).formfield_for_foreignkey(db_field, request=request, using=self.using, **kwargs)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
# 让 Django 基于 'other' 数据库生成多对多关系控件。
# on the 'other' database.
return super(MultiDBTabularInline, self).formfield_for_manytomany(db_field, request=request, using=self.using, **kwargs)
一旦你写好了管理模型的定义,就可被任何 Admin 实例注册使用:
from django.contrib import admin
# 指定用于给定模型的多数据库管理对象。
class BookInline(MultiDBTabularInline):
model = Book
class PublisherAdmin(MultiDBModelAdmin):
inlines = [BookInline]
admin.site.register(Author, MultiDBModelAdmin)
admin.site.register(Publisher, PublisherAdmin)
othersite = admin.Site('othersite')
othersite.register(Publisher, MultiDBModelAdmin)
这个例子设置了两个管理站点。第一个管理站点,显示作者和出版商对象,出版商对象有一个内联表格显示该出版商所出版的书。第二个管理站点只显示出版商,没有内联对象。
多数据库情况下使用原始指针¶
如果使用多个数据库,那么可以使用 django.db.connections 来获得特定数据库的连接(和指针)。 django.db.connections 是一个类似字典的对象,允许你通过别名来获得一个特定的连接:
from django.db import connections
cursor = connections['my_db_alias'].cursor()
多数据库的限制¶
跨数据库关系¶
Django 当前不支持外键或多对多关系跨数据库的情况。如果你已经通过使用路由把模型分配到不同的数据库中,那么任何外键或多对多关系涉及的模型必须放在同一个数据库中。
这里涉及到一个引用完整性问题。为了维护两个对象之间的关系, Django 需要知道相关联对象的主键是否合法。如果主键存放在另一个数据库,就不可能轻易的验证主键的合法性了。
如果你正在使用 Postgres 、 Oracle 或 带有InnoDB 的 MySQL ,那么在数据库级别就已经限制跨数据库关系。
但是,如是你正在使用的 SQLite 或只使用 MyISAM 表的 MySQL ,就没有这种限制,因此可以“假装”使用跨数据库外键,但是这种方式不被 Django 官方支持。