Использование нескольких баз данных

Этот документ описывает функционал Django для работы с несколькими базами данных. Большая часть документации Django предполагает, что вы работаете с единственной базой данных. Если вам требуется работать с несколькими базами данных, вам понадобится выполнить ряд дополнительных действий.

Определение ваших баз данных

Первым шагом к использованию нескольких баз данных с Django будет определение серверов БД, которые вы планируете использовать. Это выполняется с помощью параметра конфигурации DATABASES. Этот параметр привязывает к базам данных псевдонимы, по которым эти базы будут доступны в Django и словари параметров с характеристиками подключения к ним. Эти дополнительные параметры полностью описаны в документации на DATABASES.

Базам данных можно назначать любой псевдоним. Тем не менее, псевдоним default имеет особое значение. Django использует базу данных с псевдонимом default, если явно не указано использование другой базы данных.

Далее показан пример settings.py, в котором определяются две базы данных – стандартная БД PostgreSQL и БД MySQL с псевдонимом users:

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 в конфигурации баз данных, но словарь его параметров можно оставить пустым, если вы не планируете его использовать. Вы должны настроить 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

Если не хотите, чтобы модели приложения синхронизировались в конкретную базу данных, вы можете определить роутер, который будет управлять размещением моделей по базам данных

Использование других команд

Остальные команды django-admin, работающие с базой данных, аналогичны migrate – т.е., только с одной базой данных за раз. Добавляйте --database, чтобы указать, какую базу данных следует использовать.

Автоматический роутинг для баз данных

Простейшим способом использования нескольких баз данных является настройка схемы роутинга. Стандартная схема роутинга проверяет, что объекты привязаны к их оригинальной базе данных (т.е., объект, полученный из базы данных foo, будет сохранён в ту же базу данных). Стандартная схема роутинга проверяет, что если база данных не указана, то все запросы направляются к базе данных default.

Для активации стандартной схемы роутинга делать ничего не надо. Она уже настроена для каждого проекта Django. Тем не менее, если вы желаете реализовать более сложное поведение при выборе базы данных, вы можете определить и установить собственные роутеры для баз данных.

Роутеры баз данных

Роутер базы данных является классом, который предоставляет четыре метода:

db_for_read(model, **hints)

Выбирает базу данных, которая должна использоваться для операций чтения для объектов типа model.

Если операция над базой данных может предоставить любую дополнительную информацию, которая может помочь в выборе базы данных, она будет предоставлена в словаре hints (подсказки). Подробности смотрите далее.

Возвращает None, если ничего не может предложить.

db_for_write(model, **hints)

Выбирает базу данных, которая должна использоваться для операций записи объектов типа model.

Если операция над базой данных может предоставить любую дополнительную информацию, которая может помочь в выборе базы данных, она будет предоставлена в словаре hints (подсказки). Подробности смотрите далее.

Возвращает None, если ничего не может предложить.

allow_relation(obj1, obj2, **hints)

Возвращает True, если связь между obj1 и obj2 должна быть разрешена, False, если связь запрещена или None, если у роутера нет идей на этот счёт. Это обычная операция проверки, использующаяся операциями с внешними ключами и M2M, для определения возможности организации связи между двумя объектами.

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

Определяет должна ли выполняться миграция для базы данных с именем db. Возвращает True, если миграция должна быть выполнена, False, если нет или None - если у роутера нет идей.

Позиционный аргумент app_label является меткой приложения, которое подвергается миграции.

model_name устанавливается большинством миграционных операций в значение model._meta.model_name (строчная версия атрибута модели __name__) модели, над которой производится миграция. Значение будет None для операций RunPython и RunSQL, если не будет указано, что используются подсказки.

hints используются определёнными операциями для передачи дополнительной информации роутеру.

Когда model_name установлен, hints обычно содержит класс модели в ключе 'model'. Следует отметить, что он может быть исторической моделью и, следовательно, не иметь определённых атрибутов, методов или менеджеров. Вам следует руководствоваться информацией из _meta.

Этот метод также может быть использован для определения доступности модели в текущей базе данных.

Следует отметить, что миграции будут тихо проигнорированы для модели, которая получит False. Это может привести к сломанным внешним ключам, к появлению лишних таблиц или к отсутствию таблиц, если значение для модели поменяется после применения миграций к базе данных.

Изменено в Django 1.8:

Сигнатура allow_migrate значительно изменилась со времени прошлых версий. Обратитесь к заметкам по старью для получения подробностей.

Роутер не должен предоставлять все эти методы. Если некоторые методы не определенны, Django пропустит этот роутер, выполняя соответствующую операцию.

Подсказки

В аргументах hints могут передаваться данные, помогающие определить какую базу данных использовать для запроса.

Сейчас передается только instance - объект связанный с операциями записи или чтения. Это может быть создаваемый объект, или объект добавляемый через связь многое-ко-многим. В некоторых случаях объект не будет передаваться. Роутер определяет наличие такого объекта и решает передавать его или нет при вызове метода.

Использование роутеров

Роутеры для базы данных можно использовать, указав в настройке DATABASE_ROUTERS. Вы должны указать список путей для импорта классов роутера, которые будут использоваться главным роутером (django.db.router).

Мастер роутеров используются Django для определения какую БД использовать для запросов. Если необходимо определить для какой БД выполнять запрос, Django вызывает мастер роутеров передавая модель и “hint”(подсказки) (если они есть). Django проверяет каждый роутер пока не получит ответ о том, какую БД использовать. Если ответ не был найден, Django проверяет _state.db объекта “hint”. Если “hint” не был передан, или не содержит информацию какую БД использовать, мастер роутеров укажет использовать БД из настроек default.

Пример

Только в целях примера!

Этот пример показывает как можно использовать роутеры для управления несколькими БД. В нем намеренно игнорируются некоторые проблемы такой конфигурации, основная цель - продемонстрировать возможности роутеров.

Этот пример не будет работать, если какая-либо модель из myapp содержит связи с моделями не из other БД. Раздел Связи между разными БД описывает проблемы таких связей, которые Django пока не может обрабатывать.

Описанная настройка primary/replice (также известная в некоторых базах данных как master/slave) не учитывает некоторые проблемы. Она не предоставляет никакого решения для обработки задержек в репликации (т.е. нарушение целостности запросов из-за времени, затраченного на рассылку данных на подчинённые базы данных). Она также не рассматривает взаимодействие транзакций со стратегией использования базы данных.

Итак, что же это означает на практике? Давайте рассмотрим другую конфигурацию. В ней будет несколько баз данных: одна для приложения auth, все остальные приложения используют 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=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 и случайно выбирать реплику для чтения из неё:

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=None, **hints):
        """
        All non-auth models end up in this pool.
        """
        return True

Наконец, в файле конфигурации мы добавим следующее (подставим path.to с реальным путём к модулю(-ям), в которых определены роутеры):

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

Порядок применения роутеров имеет важное значение. Роутеры вызываются в порядке, в котором они перечислены в параметре конфигурации DATABASE_ROUTERS. В этом примере AuthRouter применяется до PrimaryReplicaRouter и в результате, решение по модели в auth принимается до всех остальных решений. Если же поменять порядок роутеров, то PrimaryReplicaRouter.allow_migrate() будет применено первым. Сущность “принимаем всё” реализации этого роутера приведёт к тому, что все модели будут доступны во всех базах данных.

Используя вышеприведённую конфигурацию, выполним следующий код:

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

Ручное указание базы данных

Django также предоставляет API, которое позволяет вам получить полный контроль над использованием баз данных в вашем коде. Ручное указание базы данных имеет приоритет над действиями роутера.

Ручное выбор базы данных для QuerySet

Вы можете выбрать базу данных для QuerySet в любой точке его цепочки. Для этого надо применить метод using(), который изменит базу данных, к которой будет выполнен запрос.

Метод 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')

Если вы не укажете using в методе save(), то запись будет произведена в стандартную базу данных, которую выберет роутер.

Перемещение объекта между базами данных

При сохранении экземпляра данных в одну базу данных, может показаться заманчивым использовать save(using=...) в качестве способа переноса экземпляра данных в новую базу данных. Однако, если вы не предприняли соответствующих действий, такой подход может привести к неожиданным последствиям.

Рассмотрим следующий пример:

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

В первом операторе создаётся новый объект Person в базе данных first. В этот момент p не обладает первичным ключом, поэтому Djangо выполняет SQL запрос INSERT. Запрос создаёт первичный ключ и Django назначает его для p.

При сохранении объекта во втором операторе, p уже обладает первичным ключом и Django попытается использовать его для новой базы данных. Если значение первичного ключа ещё не было использовано в базе данных 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.

Вторым способом будет использование параметра 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().

Например, если вы перемещаете пользователя из базы данных 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_queryset() с множеством баз данных

Если вы переопределяете get_queryset() в вашем менеджере, удостоверьтесь, что вызываете метод родительского объекта (через super()) или выполняйте соответствующую обработку атрибута _db в менеджере (строка, содержащая имя используемой базы данных).

Например, если требуется вернуть свой класс 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 не имеет явной поддержки для использования множества баз данных. Если вам требуется предоставить интерфейс для модели в базе данных, отличной от указанной в вашей цепочке роутеров, то потребуется написать свои классы ModelAdmin, которые будут обеспечивать доступ к требуемой базе данных.

Объекты 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(MultiDBModelAdmin, self).get_queryset(request).using(self.using)

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        # Tell Django to populate ForeignKey widgets using a query
        # on the 'other' database.
        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):
        # Tell Django to populate ManyToMany widgets using a query
        # on the 'other' database.
        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 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=None, **kwargs):
        # Tell Django to populate ForeignKey widgets using a query
        # 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):
        # Tell Django to populate ManyToMany widgets using a query
        # 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

# 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 имеют табличный инлайн, показывающий книги данного издателя. Второй сайт отображает просто издателей, без инлайнов.

Использование raw курсоров с множеством баз данных

При использовании нескольких баз данных вы можете применять 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 приложений

Несколько поставляемых приложений содержат модели, а некоторые приложения зависят от других. Так как невозможно достичь меж-БД целостности, это создаёт некоторые ограничения на распределение этих моделей между базами данных:

  • каждый элемент contenttypes.ContentType, sessions.Session и sites.Site может быть сохранён в любой базе данных, используя соответствующий роутер.

  • Модели приложения authUser, Group и Permission — соединены друг с другом и с ContentType, таким образом, они должны быть сохранены в той же базе данных, что и ContentType.

  • Приложение admin зависит от приложения auth, таким образом, его модели должны располагаться в той же базе данных, что и модели приложения auth.

  • Приложения flatpages и redirects зависят от приложения sites, таким образом их модели должны располагаться в той же базе данных, что и модели приложения sites.

В дополнение, некоторые объекты автоматически создаются после того как migrate создаёт таблицу для их хранения в базе данных:

  • стандартный Site,

  • ContentType для каждой модели (включая те, что не сохранены в базе данных),

  • три Permission для каждой модели (включая те, что не сохранены в базе данных).

В стандартных конфигурациях с множеством баз данных, не будет полезным распределять эти объекты по разным базам данных. Стандартные конфигурации включают в себя главную базу данных и реплики, а также подключение к внешним базам данных. В этом случае рекомендуется создать роутер, который позволит синхронизировать эти три модели в одной базе данных. Используйте аналогичный подход для поставляемых (contrib) приложений и сторонних приложений, которые не требуют распределение своих моделей по нескольким базам данных.

Предупреждение

Если вы синхронизируете типы контента по нескольким базам данных, обеспечьте уникальность их первичных ключей на уровне всех баз данных. Иначе вы можете столкнуться с потерей или порчей данных.