Подтвердить что ты не робот

Многопользовательские приложения Django: изменение подключения к базе данных по запросу?

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

Обновление/решение: Я решил это решить в новом проекте с открытым исходным кодом: см. django-db-multitenant

Цель

Моя цель - мультиплексировать запросы по мере их поступления на один сервер приложений (WSGI-интерфейс, такой как gunicorn), на основе имени хоста запроса или пути запроса (например, foo.example.com/ устанавливает соединение Django для использования базы данных foo, а bar.example.com/ использует базу данных bar).

Прецедент

Я знаю несколько существующих решений для многопользовательской аренды в Django:

  • django-tenant-schemas: это очень близко к тому, что я хочу: вы устанавливаете его промежуточное ПО с наивысшим приоритетом и отправляет команду SET search_path в db. К сожалению, это специфичный Postgres, и я застрял в MySQL.
  • django-simple-multitenant: Стратегия заключается в том, чтобы добавить внешний ключ "арендатора" ко всем моделям и настроить всю бизнес-логику приложений, чтобы отменить это. В основном каждая строка индексируется (id, tenant_id), а не (id). Я пробовал и не люблю этот подход по ряду причин: он делает приложение более сложным, он может привести к труднодоступным ошибкам и не обеспечивает изоляции на уровне базы данных.
  • Один {сервер приложений, файл настроек django с соответствующим db} для каждого арендатора. Ака бедный человек много аренда (на самом деле богатый человек, учитывая ресурсы, которые он включает). Я не хочу раскручивать новый сервер приложений для каждого арендатора, а для масштабируемости я хочу, чтобы любой сервер приложений мог отправлять запросы для любого клиента.

Идеи

Моя лучшая идея - сделать что-то вроде django-tenant-schemas: в первом промежуточном программном обеспечении, захватить django.db.connection и скриптировать с выбором базы данных, а не с схемой. Я не совсем понял, что это означает с точки зрения объединения/постоянных соединений.

Еще один тупик, который я преследовал, - это префикс таблицы, ориентированный на арендатора. Отложив в сторону, что мне нужно, чтобы они были динамическими, даже глобальный префикс таблицы нелегко достигнут в Django (см. отклоненный билет 5000, среди прочих).

Наконец, поддержка Django нескольких баз данных позволяет вам определять несколько именованных баз данных и мультиплексирование среди них на основе типа экземпляра и режима чтения/записи. Не полезно, так как нет возможности выбрать db для каждого запроса.

Вопрос

Кто-нибудь управлял чем-то подобным? Если да, то как вы его реализовали?

4b9b3361

Ответ 1

Для записи я решил реализовать вариант моей первой идеи: выпустите USE <dbname> в промежуточное ПО раннего запроса. Я также установил префикс CACHE таким же образом.

Я использую его на небольшом производственном сайте, просматривая имя арендатора из базы данных Redis на основе хоста запроса. Пока что я доволен результатами.

Я превратил его в проект github (надеюсь, редуцируемый) здесь: https://github.com/mik3y/django-db-multitenant

Ответ 2

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

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

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

class MyModel(Model):
    ....
    class Meta:
        app_label = 'customer_records'
        managed = False

Маршрутизатор базы данных помещается в цепочку settings.DATABASE_ROUTERS для маршрутизации запроса базы данных на app_label, что-то вроде этого (не полный пример):

class AppLabelRouter(object):
    def get_customer_db(self, model):
        # Route models belonging to 'myapp' to the 'shared_db' database, irrespective
        # of customer.
        if model._meta.app_label == 'myapp':
            return 'shared_db'
        if model._meta.app_label == 'customer_records':
            customer_db = thread_local_data.current_customer_db()
            if customer_db is not None:
                return customer_db

            raise Exception("No customer database selected")
        return None

    def db_for_read(self, model, **hints):
        return self.get_customer_db(model, **hints)

    def db_for_write(self, model, **hints):
        return self.get_customer_db(model, **hints)

Особенной частью этого маршрутизатора является вызов thread_local_data.current_customer_db(). Прежде чем маршрутизатор будет выполнен, вызывающий/приложение должно установить текущий клиент db в thread_local_data. Для этой цели можно использовать диспетчер контекстов Python для того, чтобы нажимать/всплывать текущую клиентскую базу данных.

Когда все это настроено, код приложения выглядит примерно так: UseCustomerDatabase - это менеджер контекста, который должен нажать/поместить текущее имя базы данных клиента в thread_local_data, чтобы thread_local_data.current_customer_db() вернул правильное имя базы данных когда маршрутизатор в конечном итоге попадает:

class MyView(DetailView):
    def get_object(self):
        db_name = determine_customer_db_to_use(self.request) 
        with UseCustomerDatabase(db_name):
            return MyModel.object.get(pk=1)

Это уже довольно сложная настройка. Он работает, но я попытаюсь обобщить то, что вижу, как преимущества и недостатки:

<сильные > Преимущества

  • Выбор базы данных является гибким. Он позволяет использовать несколько баз данных в одном запросе, как клиентские, так и общие базы данных могут использоваться в запросе.
  • Выбор базы данных является явным (не уверен, что это преимущество или недостаток). Если вы попытаетесь запустить запрос, который попадает в базу данных клиента, но приложение его не выбрало, произойдет исключение, указывающее на ошибку программирования.
  • Использование маршрутизатора базы данных позволяет различным базам данных существовать на разных хостах, а не полагаться на оператор USE db;, который предполагает, что все базы данных доступны через одно соединение.

Недостатки

  • Это сложно настроить, и для его функционирования требуется немало слоев.
  • Необходимость и использование локальных данных потока неясно.
  • Представления усеяны кодом выбора базы данных. Это может быть абстрагировано с использованием представлений на основе классов для автоматического выбора базы данных на основе параметров запроса таким же образом, как промежуточное программное обеспечение будет выбирать базу данных по умолчанию.
  • Диспетчер контекста для выбора базы данных должен быть обернут вокруг набора запросов таким образом, чтобы диспетчер контекста все еще был активен при оценке запроса.

Предложения

Если вам нужен гибкий доступ к базе данных, я бы предложил использовать маршрутизаторы базы данных Django. Используйте промежуточное программное обеспечение или представление Mixin, которое автоматически устанавливает базу данных по умолчанию для использования для подключения на основе параметров запроса. Возможно, вам придется прибегнуть к локальным данным потока, чтобы сохранить базу данных по умолчанию для использования, чтобы при попадании маршрутизатора она знала, к какой базе данных нужно переходить. Это позволяет Django использовать существующие постоянные подключения к базе данных (которая может находиться на разных хостах, если требуется), и выбирает базу данных для использования на основе маршрутизации, настроенной в запросе.

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

Ответ 3

Вы можете создать собственное промежуточное программное обеспечение, которое определило имя базы данных из вашего поддомена или что-то еще, а затем выполнило USE оператора для каждого запроса. Глядя на код схемы django-tenants-schema, это, по сути, то, что он делает. Это подклассификация psycopg2 и выдача эквивалентов postgres для USE, "set search_path XXX". Вы могли бы создать модель для управления и создания ваших арендаторов, но тогда вы переписываете большую часть схемы django-tenants.

В MySQL не должно быть ограничений производительности или ресурса для переключения схемы (имя db). Он просто устанавливает параметр сеанса для подключения.