Django多数据库

...

Posted by 呆贝斯 on August 12, 2021

简介

Django 当前不提供对跨多数据库的外键或多对多关系任何支持。如果已经使用路由来分隔模型到不同数据库, 那么通过这些模型来定义的任何外键和多对多关系必须在单一数据库内。这是因为参照完整性。为了维护两个对象之间的关系, Djagno 需要知道这个相关对象的外键是否是合法的。如果外键被保存在单独的数据库上,则无法轻松评价外键的合法性。

如果你正在使用 Postgres,Oracle,或支持 InnoDB 的 MySQL,这是在数据库完整性级别上强制执行的——数据库级别的键约束防止创建无法验证的关系。 然而,如果你正在使用 SQLite 或支持 MyISAM 表的MySQL,这就不会强制参照完整性;因此,你可以伪造跨数据库的外键。 尽管 Django 并没有正式支持这个设置。

定义数据库

首先告知 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",
    },
    "slave_1": {
        "NAME": "user_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "priv4te",
    },
}

如果 default 数据库的设计在项目中没有使用,那么你需要特别注意始终指定你所使用的数据库。Django 需要定义 default 数据库, 但如果没有使用数据库的话,参数字典可以置空。这样,你必须为所有的模型,包括你所使用的任何 contrib 和第三方 app 设置 DATABASE_ROUTERS, 所以不会有任何查询路由到默认数据库。下面示例来讲在默认数据库为空的情况下,如何定义两个非默认数据库:

DATABASES = {
    "default": {},
    "master": {
        "NAME": "user_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_user",
        "PASSWORD": "superS3cret",
    },
    "slave_1": {
        "NAME": "customer_data",
        "ENGINE": "django.db.backends.mysql",
        "USER": "mysql_cust",
        "PASSWORD": "veryPriv@ate",
    },
}

同步数据库

migrate管理命令每次只对一个数据库进行操作。默认情况下,它在默认的数据库上操作,但是通过提供–数据库选项,你可以告诉它去同步一个不同的数据库。 因此,在上面的第一个例子中,如果要将所有模型同步到所有数据库,你需要调用:

./manage.py migrate
./manage.py migrate --database=slave_1

如果不想每个应用同步到特定数据库,可以定义 database router ,它实施限制特定模型可用性的策略。

如果像上面的第二个例子一样,你把默认的数据库留空了,那么每次运行migrate时都必须提供一个数据库名称。省略数据库名称将引发一个错误。 对于第二个例子:

./manage.py migrate --database=master
./manage.py migrate --database=slave_1

自动数据库路由

使用多数据库最简单的方式就是设置数据库路由方案。默认路由方案确保对象对原始数据库保持粘性 (比如,从 foo 数据库检索到的对象将被保持到同一个数据库)。默认路由方案确保当数据库没有指定时,所有查询回退到 default 数据库。 你无需执行任何操作来激活默认路由——在每个 Django 项目上是开箱即用的。然而,如果想实现更多有趣的数据库分配行为,可以定义和安装自己的数据库路由。

class DjangoRouter:
    def db_for_read(self, model, **hints):
        return 'slave_1'

    def db_for_write(self, model, **hints):
        return 'default'

    def allow_relation(self, obj1, obj2, **hints):
        return None

    def allow_syncdb(self, db, model):
        return None

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

DATABASE_ROUTERS = ["path.to.AuthRouter", "path.to.PrimaryReplicaRouter"]

处理路由的顺序非常重要。路由将按照 DATABASE_ROUTERS 里设置的顺序查询。在这个例子里, AuthRouter 将在 PrimaryReplicaRouter 前处理。

手动选择数据库

Django也提供允许在代码中完全控制数据库的API。手工指定数据库分配将优先于路由分配的数据库。你可以在查询集链的任一点为查询集选择数据库。调用查询集上的 using() 就可以获取使用指定数据库的其他查询集。

手动为查询选择数据库

你可以在查询集链的任一点为查询集选择数据库。调用查询集上的 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")

为保存选择数据库

使用 using 关键字来 Model.save() 到指定的数据保存的数据库。

>>> my_object.save(using="legacy_users")

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

如果已经保存实例到数据库,它可能使用 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 数据库中存在的对象将被覆盖。

选择要删除的数据库

默认情况下,删除现有对象的调用将在首先用于检索该对象的同一数据库上执行。指定将要删除模型的数据库,传递using关键字参数到 Model.delete() 方法。 这个参数的工作方式与用关键字参数 save() 是一样的。

u = User.objects.using("legacy_users").get(username="fred")
u.delete()  # will delete from the `legacy_users` database
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()。 db_manager() 返回绑定到指定数据库的管理器副本。

User.objects.db_manager("new_users").create_user(...)
  • 将 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 类,这个类将指示管理后台使用指定数据库的内容。

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
        )

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)

这个例子设置了两个管理长点。在第一个站点上,Author 和 Publisher 对象是显式的;Publisher 对象有一个表格行来显示出版者的书籍。第二个站点只显示出版者,不显示内嵌。

将原始游标用于多个数据库

如果正在使用不止一个数据库,可以使用 django.db.connections 来获得链接指定的数据库。django.db.connections 是一个类字典对象,它允许你通过链接别名来获取指定连接:

from django.db import connections

with connections["my_db_alias"].cursor() as cursor:
    ...

contrib应用程序的行为

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

在给定合适的路由器的情况下,contenttypes.ContentType,sessions.Sessionsites.Site中的每一个都可以存储在任何数据库中。auth 模型 - User,GroupPermission - 链接在一起并链接到ContentType,因此它们必须与ContentType存储在同一个数据库中。admin依赖于auth,所以它的模型必须和auth在同一个数据库中。flatpagesredirects依赖于sites,所以他们的模型必须和sites在同一个数据库中。此外,一些对象在以下之后自动创建:djadmin:migrate创建一个表以将它们保存在数据库中:

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

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