Django и база данных с write-Instance + несколько реплик для чтения - выполнение заданий Celery
У меня есть приложение django, работающее в продакшене. Его база данных имеет основной экземпляр для записи и несколько реплик для чтения. Я использую DATABASE_ROUTERS
для маршрутизации между экземпляром записи и репликами чтения в зависимости от того, нужно ли мне читать или писать.
Я столкнулся с ситуацией, когда мне нужно выполнить некоторую асинхронную обработку объекта по запросу пользователя. Порядок действий следующий:
- User submits a request via HTTPS/REST.
- The view creates an Object and saves it to the DB.
- Trigger a celery job to process the object outside of the request-response cycle and passing the object ID to it.
- Sending an OK response to the request.
Теперь задание celery может запуститься через 10 мс или 10 минут в зависимости от очереди. Когда оно, наконец, включается, задание celery сначала пытается загрузить объект на основе предоставленного ID. Изначально у меня были проблемы с выполнением my_obj = MyModel.objects.get(pk=given_id)
, потому что в этот момент будет использоваться реплика чтения, если очередь пуста и задание celery запускается сразу после срабатывания, объект может еще не распространиться на реплику чтения.
Я решил эту проблему, заменив my_obj = MyModel.objects.get(pk=given_id)
на my_obj = MyModel.objects.using('default').get(pk=given_id)
- это гарантирует, что объект считывается из моей write-db-инстанции и всегда доступен.
однако, теперь у меня есть еще одна проблема, которую я не ожидал.
вызов my_obj.certain_many_to_many_objects.all()
вызывает еще один вызов к базе данных, поскольку ORM ленив. Этот вызов IS выполняется на read-реплике. Я надеялся, что он будет придерживаться базы данных, которую я определил с помощью using
, но это не так. Есть ли способ заставить все объекты подэлементов использовать один и тот же write-db-instance?
Для изменения реплики базы данных можно использовать менеджеры моделей и ссылку на API QuerySet
Существует способ указать, какое соединение с БД использовать в Django. Для каждого менеджера моделей класс Django BaseManager
использует приватное свойство self._db
для хранения соединения с БД, вы можете указать и другое значение.
class MyModelRelationQuerySet(models.QuerySet):
def filter_on_my_obj(self, given_id):
# preform the base query set you want
return self.filter(relation__fk=given_id)
class MyModelManager(models.Manager):
# bypass self._db on BaseManager class
def get_queryset(self):
# proper way to pass "using" would be using=self._db
# for your case you may pass your 'master db connection'
return MyModelRelationQuerySet (self.model, using=your_write_replica)
def my_obj_filter(self, given_id):
return self.get_queryset().get_my_obj(given_id)
# pass the model manager to model
class MyModel(models.Model):
# ...
objects = MyModelManager()
документы по созданию пользовательских QuerySet для менеджеров моделей в Django.
и чтение исходного кода Django models.Manger и исходного кода QuerySet может быть полезным для таких продвинутых вопросов с запросами к данным bese.
Разве использование my_obj.certain_many_to_many_objects.all().using('default')
не
работает?
.all()
возвращает кверисет, поэтому вы должны быть в состоянии добавить часть .using(..)
для него, и она будет работать.
Я подозреваю, что ваш пользовательский маршрутизатор базы данных нуждается в настройке. Поведение по умолчанию без пользовательского маршрутизатора должно обеспечить требуемую вами липкость базы данных
Схема маршрутизации по умолчанию гарантирует, что объекты остаются "прилипшими" к своей исходной базе данных (т.е. объект, извлеченный из базы данных базы данных foo, будет сохранен в той же базе данных). [...] Вам не нужно ничего делать, чтобы активировать схему маршрутизации по умолчанию - она предоставляется "из коробки" в каждом проекте Django.
.
От Автоматическая маршрутизация БД
Так что ваш маршрутизатор DB просто должен предложить такое поведение заранее, как, вероятно, являющееся Right Thing To Do в 99.9% случаев.
def db_for_read(model, **hints):
instance = hints.get('instance')
if instance is not None and instance._state.db:
return instance._state.db
# else return your read replica
return 'read-only' # or whatever it's called
Смотрите django/db/utils.py
Первый шаг к использованию более чем одной базы данных в Django - сообщить Django о серверах баз данных, которые вы будете использовать. Это делается с помощью параметра DATABASES. Эта настройка сопоставляет псевдонимы баз данных, которые являются способом обращения к конкретной базе данных в Django, со словарем настроек для этого конкретного соединения.
Исключением из этого правила является команда makemigrations. Она проверяет историю миграций в базах данных, чтобы выявить проблемы с существующими файлами миграций (которые могут быть вызваны их редактированием) перед созданием новых миграций. По умолчанию она проверяет только базу данных по умолчанию, но обращается к методу allow_migrate() маршрутизаторов, если таковые установлены.
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
db_for_read(model, **hints) Предложите базу данных, которая должна использоваться для операций чтения для объектов типа model.
Если операция с базой данных может предоставить какую-либо дополнительную информацию, которая может помочь в выборе базы данных, она будет предоставлена в словаре подсказок. Подробности о допустимых подсказках приведены ниже.
Возвращает None, если предложение отсутствует.