Django и база данных с write-Instance + несколько реплик для чтения - выполнение заданий Celery

У меня есть приложение django, работающее в продакшене. Его база данных имеет основной экземпляр для записи и несколько реплик для чтения. Я использую DATABASE_ROUTERS для маршрутизации между экземпляром записи и репликами чтения в зависимости от того, нужно ли мне читать или писать.

Я столкнулся с ситуацией, когда мне нужно выполнить некоторую асинхронную обработку объекта по запросу пользователя. Порядок действий следующий:

  1. User submits a request via HTTPS/REST.
  2. The view creates an Object and saves it to the DB.
  3. Trigger a celery job to process the object outside of the request-response cycle and passing the object ID to it.
  4. 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, если предложение отсутствует.

Вернуться на верх